Skip to content

Commit

Permalink
Add kind and class suffix for polymorphic relations
Browse files Browse the repository at this point in the history
  • Loading branch information
loicknuchel committed Jan 25, 2024
1 parent 81daa10 commit 4822dd0
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 88 deletions.
20 changes: 10 additions & 10 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azimutt",
"version": "0.0.29",
"version": "0.0.30",
"description": "Export database schema from relational or document databases. Import it to https://azimutt.app",
"keywords": [
"database",
Expand Down Expand Up @@ -44,7 +44,7 @@
"dry-publish": "npm run build && npm test && npm pack"
},
"dependencies": {
"@azimutt/gateway": "^0.0.17",
"@azimutt/gateway": "^0.0.18",
"chalk": "4.1.2",
"clear": "0.1.0",
"commander": "11.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '0.0.29'
export const version = '0.0.30'
2 changes: 1 addition & 1 deletion frontend/src/Libs/Ned.elm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Libs.Ned exposing (Ned, build, buildMap, find, from, fromDict, fromList, fromNel, fromNelMap, get, map, merge, singleton, singletonMap, size, toDict, values)
module Libs.Ned exposing (Ned, build, buildMap, find, from, fromDict, fromList, fromNel, fromNelMap, get, map, merge, singleton, singletonMap, size, toDict, toList, values)

import Dict exposing (Dict)
import Libs.Dict as Dict
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/Libs/String.elm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Libs.String exposing
( filterStartsWith
( capitalize
, filterStartsWith
, hashCode
, inflect
, nonEmpty
Expand Down Expand Up @@ -64,6 +65,21 @@ stripRight suffix str =
str


capitalize : String -> String
capitalize str =
str
|> String.toList
|> List.indexedMap
(\i c ->
if i == 0 then
Char.toUpper c

else
Char.toLower c
)
|> String.fromList


orElse : String -> String -> String
orElse other str =
if str == "" then
Expand Down
41 changes: 40 additions & 1 deletion frontend/src/Models/Project/Column.elm
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
module Models.Project.Column exposing (Column, ColumnLike, NestedColumns(..), decode, encode, flatten, getColumn)
module Models.Project.Column exposing (Column, ColumnLike, NestedColumns(..), decode, encode, findColumn, flatten, getColumn, nestedColumns)

import Dict exposing (Dict)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
import Libs.Json.Decode as Decode
import Libs.Json.Encode as Encode
import Libs.List as List
import Libs.Maybe as Maybe
import Libs.Ned as Ned exposing (Ned)
import Libs.Nel as Nel exposing (Nel)
Expand Down Expand Up @@ -52,13 +54,50 @@ flattenNested path (NestedColumns cols) =
cols |> Ned.values |> Nel.toList |> List.concatMap (\col -> path |> ColumnPath.child col.name |> (\p -> [ { path = p, column = col } ] ++ (col.columns |> Maybe.mapOrElse (flattenNested p) [])))


nestedColumns : Column -> List Column
nestedColumns col =
col.columns |> Maybe.mapOrElse (\(NestedColumns cols) -> cols |> Ned.values |> Nel.toList) []


getColumn : ColumnPath -> Column -> Maybe Column
getColumn path column =
column.columns
|> Maybe.andThen (\(NestedColumns cols) -> cols |> Ned.get path.head)
|> Maybe.andThen (\col -> path.tail |> Nel.fromList |> Maybe.mapOrElse (\next -> getColumn next col) (Just col))


findColumn : (ColumnPath -> Column -> Bool) -> Column -> Maybe ( ColumnPath, Column )
findColumn predicate column =
let
path : ColumnPath
path =
ColumnPath.root column.name
in
if predicate path column then
Just ( path, column )

else
column.columns |> Maybe.andThen (findColumnInner predicate path)


findColumnInner : (ColumnPath -> Column -> Bool) -> ColumnPath -> NestedColumns -> Maybe ( ColumnPath, Column )
findColumnInner predicate path (NestedColumns cols) =
cols
|> Ned.toList
|> List.findMap
(\( name, col ) ->
path
|> ColumnPath.child name
|> (\p ->
if predicate p col then
Just ( p, col )

else
col.columns |> Maybe.andThen (findColumnInner predicate p)
)
)


encode : Column -> Value
encode value =
Encode.notNullObject
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/Models/Project/ColumnPath.elm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Models.Project.ColumnPath exposing (ColumnPath, ColumnPathStr, child, decode, decodeStr, encode, encodeStr, fromString, get, isRoot, merge, name, parent, rootName, separator, show, startsWith, toString, update, withName)
module Models.Project.ColumnPath exposing (ColumnPath, ColumnPathStr, child, decode, decodeStr, encode, encodeStr, fromString, get, isRoot, merge, name, parent, root, rootName, separator, show, startsWith, toString, update, withName)

import Dict exposing (Dict)
import Json.Decode as Decode exposing (Decoder, Value)
Expand Down Expand Up @@ -39,7 +39,12 @@ show path =
path |> Nel.toList |> String.join "."


withName : ColumnPath -> String -> String
root : ColumnName -> ColumnPath
root n =
Nel n []


withName : ColumnPath -> ColumnName -> String
withName column text =
text ++ "." ++ show column

Expand Down
19 changes: 17 additions & 2 deletions frontend/src/Models/Project/Table.elm
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
module Models.Project.Table exposing (Table, TableLike, decode, encode, getColumn, new)
module Models.Project.Table exposing (Table, TableLike, decode, encode, findColumn, getColumn, getPeerColumns, new)

import Dict exposing (Dict)
import Json.Decode as Decode
import Json.Encode as Encode exposing (Value)
import Libs.Dict as Dict
import Libs.Json.Decode as Decode
import Libs.Json.Encode as Encode
import Libs.List as List
import Libs.Maybe as Maybe
import Libs.Nel as Nel
import Models.Project.Check as Check exposing (Check)
import Models.Project.Column as Column exposing (Column, ColumnLike)
import Models.Project.ColumnName exposing (ColumnName)
import Models.Project.ColumnPath exposing (ColumnPath)
import Models.Project.ColumnPath as ColumnPath exposing (ColumnPath)
import Models.Project.Comment as Comment exposing (Comment)
import Models.Project.Index as Index exposing (Index)
import Models.Project.PrimaryKey as PrimaryKey exposing (PrimaryKey)
Expand Down Expand Up @@ -62,6 +63,20 @@ getColumn path table =
|> Maybe.andThen (\col -> path.tail |> Nel.fromList |> Maybe.mapOrElse (\next -> Column.getColumn next col) (Just col))


getPeerColumns : ColumnPath -> Table -> List Column
getPeerColumns path table =
(path |> ColumnPath.parent)
|> Maybe.map (\p -> table |> getColumn p |> Maybe.mapOrElse Column.nestedColumns [])
|> Maybe.withDefault (table.columns |> Dict.values)


findColumn : (ColumnPath -> Column -> Bool) -> Table -> Maybe ( ColumnPath, Column )
findColumn predicate table =
table.columns
|> Dict.toList
|> List.findMap (\( _, col ) -> Column.findColumn predicate col)


encode : Table -> Value
encode value =
Encode.notNullObject
Expand Down
77 changes: 35 additions & 42 deletions frontend/src/Services/Analysis/MissingRelations.elm
Original file line number Diff line number Diff line change
Expand Up @@ -58,39 +58,39 @@ guessRelations tableNames tables relationBySrc table { path, column } =
colRef =
{ table = table.id, column = path, kind = column.kind }

columnWords : List String
columnWords =
colWords : List String
colWords =
column.name |> String.splitWords

targetColumnName : ColumnName
targetColumnName =
columnWords |> List.last |> Maybe.withDefault column.name |> String.singular
colLastWord : ColumnName
colLastWord =
colWords |> List.last |> Maybe.withDefault column.name |> String.singular
in
(if targetColumnName == "id" && List.length columnWords > 1 then
(if colLastWord == "id" && List.length colWords > 1 then
let
tableHint : List String
tableHint =
columnWords |> List.dropRight 1
targetTableHint : List String
targetTableHint =
colWords |> List.dropRight 1

suggestedRelations : List SuggestedRelation
suggestedRelations =
getTypeColumn table path
getPolymorphicColumn table path
|> Maybe.andThen
(\typeCol ->
typeCol.column.values
(\polymorphicCol ->
polymorphicCol.values
|> Maybe.map
(Nel.toList
>> List.map
(\value ->
{ src = colRef
, ref = getTargetColumn tableNames tables table.schema (value |> String.splitWords) targetColumnName
, when = Just { column = typeCol.path, value = value }
, ref = getTargetColumn tableNames tables table.schema (value |> String.splitWords) colLastWord
, when = Just { column = polymorphicCol.path, value = value }
}
)
>> List.filter (\rel -> rel.ref /= Nothing)
)
)
|> Maybe.withDefault [ { src = colRef, ref = getTargetColumn tableNames tables table.schema tableHint targetColumnName, when = Nothing } ]
|> Maybe.withDefault [ { src = colRef, ref = getTargetColumn tableNames tables table.schema targetTableHint colLastWord, when = Nothing } ]
in
suggestedRelations

Expand All @@ -103,7 +103,7 @@ guessRelations tableNames tables relationBySrc table { path, column } =
in
[ { src = colRef, ref = [ column.name, "id" ] |> List.findMap (getTargetColumn tableNames tables table.schema tableHint), when = Nothing } ]

else if List.last columnWords == Just "by" then
else if List.last colWords == Just "by" then
-- `created_by` columns should refer to a user like table
[ { src = colRef, ref = [ [ "user" ], [ "account" ] ] |> List.findMap (\tableHint -> getTargetColumn tableNames tables table.schema tableHint "id"), when = Nothing } ]

Expand All @@ -113,38 +113,31 @@ guessRelations tableNames tables relationBySrc table { path, column } =
|> removeKnownRelations relationBySrc table.id path


getTypeColumn : Table -> ColumnPath -> Maybe { path : ColumnPath, column : Column }
getTypeColumn table path =
-- useful for polymorphic relations
getPolymorphicColumn : Table -> ColumnPath -> Maybe { path : ColumnPath, values : Maybe (Nel String) }
getPolymorphicColumn table path =
let
typePath : ColumnPath
typePath =
path
|> Nel.mapLast
(\name ->
if name |> String.endsWith "id" then
String.dropRight 2 name ++ "type"

else if name |> String.endsWith "ids" then
String.dropRight 3 name ++ "type"
( suffixes, name ) =
( [ "type", "class", "kind" ], path |> ColumnPath.name )

else if name |> String.endsWith "Id" then
String.dropRight 2 name ++ "Type"
prefix : String
prefix =
if name |> String.toLower |> String.endsWith "ids" then
name |> String.dropRight 3

else if name |> String.endsWith "Ids" then
String.dropRight 3 name ++ "Type"
else if name |> String.toLower |> String.endsWith "id" then
name |> String.dropRight 2

else if name |> String.endsWith "ID" then
String.dropRight 2 name ++ "TYPE"
else
name
in
(table |> Table.getPeerColumns path)
|> List.find (\c -> (c.name |> String.startsWith prefix) && (suffixes |> List.any (\s -> hasSuffix s c.name)))
|> Maybe.map (\c -> { path = path |> Nel.mapLast (\_ -> c.name), values = c.values })

else if name |> String.endsWith "IDS" then
String.dropRight 3 name ++ "TYPE"

else
name ++ "_type"
)
in
table |> Table.getColumn typePath |> Maybe.map (\c -> { path = typePath, column = c })
hasSuffix : String -> String -> Bool
hasSuffix suffix str =
String.endsWith (String.toLower suffix) str || String.endsWith (String.toUpper suffix) str || String.endsWith (String.capitalize suffix) str


getTargetColumn : Dict NormalizedTableName (List TableId) -> Dict TableId Table -> SchemaName -> List String -> ColumnName -> Maybe SuggestedRelationRef
Expand Down
6 changes: 5 additions & 1 deletion frontend/tests/Libs/StringTest.elm
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Libs.StringTest exposing (..)

import Expect
import Libs.String exposing (hashCode, plural, singular, slugify, splitWords, unique)
import Libs.String exposing (capitalize, hashCode, plural, singular, slugify, splitWords, unique)
import Test exposing (Test, describe, test)


Expand All @@ -16,6 +16,10 @@ suite =
, test "conflict with extension and number" (\_ -> unique [ "eee2.txt" ] "eee2.txt" |> Expect.equal "eee3.txt")
, test "multi conflicts" (\_ -> unique [ "fff.txt", "fff2.txt", "fff3.txt" ] "fff.txt" |> Expect.equal "fff4.txt")
]
, describe "capitalize"
[ test "upper" (\_ -> "AAA" |> capitalize |> Expect.equal "Aaa")
, test "lower" (\_ -> "aaa" |> capitalize |> Expect.equal "Aaa")
]
, describe "splitWords"
[ test "CamelUpper" (\_ -> "AzimuttIsAwesome" |> splitWords |> Expect.equal [ "azimutt", "is", "awesome" ])
, test "CamelLower" (\_ -> "azimuttIsAwesome" |> splitWords |> Expect.equal [ "azimutt", "is", "awesome" ])
Expand Down
13 changes: 3 additions & 10 deletions gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@ The other way is to use the [desktop app](../desktop) for this, keeping everythi

## Set Up

- Install the dependencies:
- copy `.env.example` to `.env` and adapt values
- run `npm install` to install dependencies
- start dev server with `npm start`

```bash
npm install
```

- Start the server in development mode:

```bash
npm start
```

## Env vars

Expand Down
Loading

0 comments on commit 4822dd0

Please sign in to comment.