mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-07-28 05:53:20 +03:00
Prevent naming conflicts for _by_pk queries/mutations
This commit is contained in:
parent
c8a5e17f25
commit
7a9914325d
5 changed files with 113 additions and 46 deletions
source/AirGQL
|
@ -82,6 +82,7 @@ import AirGQL.Config (
|
|||
)
|
||||
|
||||
import AirGQL.Introspection qualified as Introspection
|
||||
import AirGQL.Introspection.NamingConflict (encodeOutsidePKNames)
|
||||
import AirGQL.Introspection.Resolver qualified as Introspection
|
||||
import AirGQL.Introspection.Types qualified as Introspection
|
||||
import AirGQL.Lib (
|
||||
|
@ -523,8 +524,7 @@ executeUpdateMutation
|
|||
-> HashMap Text Value
|
||||
-> [(Text, Value)]
|
||||
-> IO (Int, [[SQLData]])
|
||||
executeUpdateMutation connection table args filterElements = do
|
||||
pairsToSet :: HashMap Text Value <- getArg "set" args
|
||||
executeUpdateMutation connection table pairsToSet filterElements = do
|
||||
let
|
||||
columnsToSet :: [(ColumnEntry, Value)]
|
||||
columnsToSet =
|
||||
|
@ -770,7 +770,7 @@ queryType connection accessMode dbId tables = do
|
|||
|
||||
getTableByPKTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
||||
getTableByPKTuple table =
|
||||
P.for (Introspection.tableQueryByPKField table) $ \field ->
|
||||
P.for (Introspection.tableQueryByPKField tables table) $ \field ->
|
||||
makeResolver field (getDbEntriesByPK table)
|
||||
|
||||
queryMany <- P.for tables getTableTuple
|
||||
|
@ -998,13 +998,14 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
|||
let Arguments args = context.arguments
|
||||
liftIO $ do
|
||||
filterObj <- getArg "filter" args
|
||||
pairsToSet <- getArg "set" args
|
||||
(numOfChanges, updatedRows) <- case HashMap.toList filterObj of
|
||||
[] -> P.throwIO $ userError "Error: Filter must not be empty"
|
||||
filterElements ->
|
||||
executeUpdateMutation
|
||||
connection
|
||||
table
|
||||
args
|
||||
pairsToSet
|
||||
filterElements
|
||||
|
||||
mutationResponse table numOfChanges updatedRows
|
||||
|
@ -1019,11 +1020,12 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
|||
& getByPKFilterElements
|
||||
|
||||
liftIO $ do
|
||||
pairsToSet <- getArg (encodeOutsidePKNames table "set") args
|
||||
(numOfChanges, updatedRows) <-
|
||||
executeUpdateMutation
|
||||
connection
|
||||
table
|
||||
args
|
||||
pairsToSet
|
||||
filterElements
|
||||
|
||||
mutationByPKResponse table numOfChanges $ P.head updatedRows
|
||||
|
@ -1083,8 +1085,8 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
|||
|
||||
getUpdateByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
||||
getUpdateByPKTableTuple table =
|
||||
P.for (Introspection.tableUpdateFieldByPk accessMode table) $ \field ->
|
||||
makeResolver field (executeDbUpdatesByPK table)
|
||||
P.for (Introspection.tableUpdateFieldByPk accessMode tables table) $
|
||||
\field -> makeResolver field (executeDbUpdatesByPK table)
|
||||
|
||||
getDeleteTableTuple :: TableEntry -> IO (Text, Resolver IO)
|
||||
getDeleteTableTuple table =
|
||||
|
@ -1094,8 +1096,8 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do
|
|||
|
||||
getDeleteByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
|
||||
getDeleteByPKTableTuple table =
|
||||
P.for (Introspection.tableDeleteFieldByPK accessMode table) $ \field ->
|
||||
makeResolver field (executeDbDeletionsByPK table)
|
||||
P.for (Introspection.tableDeleteFieldByPK accessMode tables table) $
|
||||
\field -> makeResolver field (executeDbDeletionsByPK table)
|
||||
|
||||
getTableTuples :: IO [(Text, Resolver IO)]
|
||||
getTableTuples =
|
||||
|
|
|
@ -33,12 +33,16 @@ import Language.GraphQL.Type.Out as Out (
|
|||
Type (NonNullScalarType),
|
||||
)
|
||||
|
||||
import AirGQL.Introspection.NamingConflict (
|
||||
encodeOutsidePKNames,
|
||||
encodeOutsideTableNames,
|
||||
)
|
||||
import AirGQL.Introspection.Resolver (makeType)
|
||||
import AirGQL.Introspection.Types (IntrospectionType)
|
||||
import AirGQL.Introspection.Types qualified as Type
|
||||
import AirGQL.Lib (
|
||||
AccessMode,
|
||||
ColumnEntry (isRowid, primary_key),
|
||||
ColumnEntry,
|
||||
GqlTypeName (full, root),
|
||||
ObjectType (Table),
|
||||
TableEntry (columns, name, object_type),
|
||||
|
@ -46,6 +50,7 @@ import AirGQL.Lib (
|
|||
canWrite,
|
||||
column_name_gql,
|
||||
datatype_gql,
|
||||
getPKColumns,
|
||||
isOmittable,
|
||||
notnull,
|
||||
)
|
||||
|
@ -182,26 +187,20 @@ tableQueryField table =
|
|||
|
||||
tablePKArguments :: TableEntry -> Maybe [Type.InputValue]
|
||||
tablePKArguments table = do
|
||||
let pks = List.filter (\col -> col.primary_key) table.columns
|
||||
|
||||
-- We filter out the rowid column, unless it is the only one
|
||||
withoutRowid <- case pks of
|
||||
[] -> Nothing
|
||||
[first] | first.isRowid -> Just [first]
|
||||
_ -> Just $ List.filter (\col -> P.not col.isRowid) pks
|
||||
pks <- getPKColumns table
|
||||
|
||||
pure $
|
||||
withoutRowid <&> \column -> do
|
||||
pks <&> \column -> do
|
||||
let name = doubleXEncodeGql column.column_name_gql
|
||||
Type.inputValue name $ Type.nonNull $ columnType column
|
||||
|
||||
|
||||
tableQueryByPKField :: TableEntry -> Maybe Type.Field
|
||||
tableQueryByPKField table = do
|
||||
tableQueryByPKField :: [TableEntry] -> TableEntry -> Maybe Type.Field
|
||||
tableQueryByPKField tables table = do
|
||||
pkArguments <- tablePKArguments table
|
||||
pure $
|
||||
Type.field
|
||||
(doubleXEncodeGql table.name <> "_by_pk")
|
||||
(encodeOutsideTableNames tables $ doubleXEncodeGql table.name <> "_by_pk")
|
||||
(tableRowType table)
|
||||
& Type.fieldWithDescription
|
||||
( "Rows from the table \""
|
||||
|
@ -237,7 +236,6 @@ mutationResponseType accessMode table = do
|
|||
|
||||
mutationByPkResponseType :: AccessMode -> TableEntry -> Type.IntrospectionType
|
||||
mutationByPkResponseType accessMode table = do
|
||||
let tableName = doubleXEncodeGql table.name
|
||||
let readonlyFields =
|
||||
if canRead accessMode
|
||||
then
|
||||
|
@ -247,7 +245,7 @@ mutationByPkResponseType accessMode table = do
|
|||
else []
|
||||
|
||||
Type.object
|
||||
(tableName <> "_mutation_by_pk_response")
|
||||
(doubleXEncodeGql table.name <> "_mutation_by_pk_response")
|
||||
( [ Type.field "affected_rows" (Type.nonNull Type.typeInt)
|
||||
]
|
||||
<> readonlyFields
|
||||
|
@ -363,13 +361,17 @@ tableUpdateField accessMode table = do
|
|||
]
|
||||
|
||||
|
||||
tableUpdateFieldByPk :: AccessMode -> TableEntry -> Maybe Type.Field
|
||||
tableUpdateFieldByPk accessMode table = do
|
||||
tableUpdateFieldByPk
|
||||
:: AccessMode
|
||||
-> [TableEntry]
|
||||
-> TableEntry
|
||||
-> Maybe Type.Field
|
||||
tableUpdateFieldByPk accessMode tables table = do
|
||||
pkArguments <- tablePKArguments table
|
||||
|
||||
let arguments =
|
||||
[ Type.inputValue
|
||||
"set"
|
||||
(encodeOutsidePKNames table "set")
|
||||
(Type.nonNull $ tableSetInput table)
|
||||
& Type.inputValueWithDescription "Fields to be updated"
|
||||
]
|
||||
|
@ -377,7 +379,13 @@ tableUpdateFieldByPk accessMode table = do
|
|||
|
||||
pure $
|
||||
Type.field
|
||||
("update_" <> doubleXEncodeGql table.name <> "_by_pk")
|
||||
( "update_"
|
||||
<> encodeOutsideTableNames
|
||||
tables
|
||||
( doubleXEncodeGql table.name
|
||||
<> "_by_pk"
|
||||
)
|
||||
)
|
||||
(Type.nonNull $ mutationByPkResponseType accessMode table)
|
||||
& Type.fieldWithDescription
|
||||
("Update row in table \"" <> table.name <> "\"")
|
||||
|
@ -399,12 +407,20 @@ tableDeleteField accessMode table = do
|
|||
]
|
||||
|
||||
|
||||
tableDeleteFieldByPK :: AccessMode -> TableEntry -> Maybe Type.Field
|
||||
tableDeleteFieldByPK accessMode table = do
|
||||
tableDeleteFieldByPK
|
||||
:: AccessMode
|
||||
-> [TableEntry]
|
||||
-> TableEntry
|
||||
-> Maybe Type.Field
|
||||
tableDeleteFieldByPK accessMode tables table = do
|
||||
args <- tablePKArguments table
|
||||
pure $
|
||||
Type.field
|
||||
("delete_" <> doubleXEncodeGql table.name <> "_by_pk")
|
||||
( "delete_"
|
||||
<> encodeOutsideTableNames
|
||||
tables
|
||||
(doubleXEncodeGql table.name <> "_by_pk")
|
||||
)
|
||||
(Type.nonNull $ mutationByPkResponseType accessMode table)
|
||||
& Type.fieldWithDescription
|
||||
("Delete row in table \"" <> table.name <> "\"")
|
||||
|
@ -445,7 +461,7 @@ getSchema accessMode tables = do
|
|||
then
|
||||
P.fold
|
||||
[ tables <&> tableQueryField
|
||||
, tables & P.mapMaybe tableQueryByPKField
|
||||
, tables & P.mapMaybe (tableQueryByPKField tables)
|
||||
]
|
||||
else []
|
||||
|
||||
|
@ -462,9 +478,9 @@ getSchema accessMode tables = do
|
|||
, tablesWithoutViews <&> tableUpdateField accessMode
|
||||
, tablesWithoutViews <&> tableDeleteField accessMode
|
||||
, tablesWithoutViews
|
||||
& P.mapMaybe (tableUpdateFieldByPk accessMode)
|
||||
& P.mapMaybe (tableUpdateFieldByPk accessMode tables)
|
||||
, tablesWithoutViews
|
||||
& P.mapMaybe (tableDeleteFieldByPK accessMode)
|
||||
& P.mapMaybe (tableDeleteFieldByPK accessMode tables)
|
||||
]
|
||||
else []
|
||||
|
||||
|
|
42
source/AirGQL/Introspection/NamingConflict.hs
Normal file
42
source/AirGQL/Introspection/NamingConflict.hs
Normal file
|
@ -0,0 +1,42 @@
|
|||
{-| Each table, say `foo`, generates a `foo` and a `foo_by_pk`. If a table
|
||||
named `foo_by_pk` also exists, this would create a naming conflict. This
|
||||
issue also occurs in a few other places.
|
||||
|
||||
To solve this, we implement the `encodeOutsideList` function, which encodes a name
|
||||
such that it does not conflict with any other name from a given list. This
|
||||
is done by repeatedly appending _ at the end, until the name does not reside in
|
||||
the given list anymore.
|
||||
-}
|
||||
module AirGQL.Introspection.NamingConflict (
|
||||
encodeOutsideList,
|
||||
encodeOutsideTableNames,
|
||||
encodeOutsidePKNames,
|
||||
) where
|
||||
|
||||
import Protolude (Text, fromMaybe, ($), (<$>), (<>))
|
||||
|
||||
import AirGQL.Lib (ColumnEntry (column_name_gql), TableEntry (name), getPKColumns)
|
||||
import Data.List qualified as List
|
||||
import DoubleXEncoding (doubleXEncodeGql)
|
||||
|
||||
|
||||
encodeOutsideList :: [Text] -> Text -> Text
|
||||
encodeOutsideList list name = do
|
||||
if name `List.elem` list
|
||||
then encodeOutsideList list (name <> "_")
|
||||
else name
|
||||
|
||||
|
||||
-- | Encode a name so it does not conflict with any table name
|
||||
encodeOutsideTableNames :: [TableEntry] -> Text -> Text
|
||||
encodeOutsideTableNames tables =
|
||||
encodeOutsideList $ (\t -> doubleXEncodeGql t.name) <$> tables
|
||||
|
||||
|
||||
{-| Encode a name so it does not conflict with any column that is part of a
|
||||
PK constraint for a given table.
|
||||
-}
|
||||
encodeOutsidePKNames :: TableEntry -> Text -> Text
|
||||
encodeOutsidePKNames table = do
|
||||
let cols = fromMaybe [] $ getPKColumns table
|
||||
encodeOutsideList $ column_name_gql <$> cols
|
|
@ -369,7 +369,7 @@ instance ToGraphQL Directive where
|
|||
, ("description", toGraphQL value.description)
|
||||
, ("isRepeatable", toGraphQL value.isRepeatable)
|
||||
, ("args", toGraphQL value.args)
|
||||
, ("locations", Value.List $ Value.Enum <$> value.locations)
|
||||
, ("locations", toGraphQL $ Value.Enum <$> value.locations)
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ module AirGQL.Lib (
|
|||
getTableNames,
|
||||
getColumnNames,
|
||||
getEnrichedTables,
|
||||
getPKColumns,
|
||||
ObjectType (..),
|
||||
parseSql,
|
||||
replaceCaseInsensitive,
|
||||
|
@ -73,8 +74,9 @@ import Control.Monad (MonadFail (fail))
|
|||
import Control.Monad.Catch (catchAll)
|
||||
import Data.Aeson (FromJSON, ToJSON, Value (Bool, Null, Number, Object, String))
|
||||
import Data.Aeson.KeyMap qualified as KeyMap
|
||||
import Data.List qualified as List
|
||||
import Data.Scientific qualified as Scientific
|
||||
import Data.Text (isInfixOf, isSuffixOf, toUpper)
|
||||
import Data.Text (isInfixOf, toUpper)
|
||||
import Data.Text qualified as T
|
||||
import Database.SQLite.Simple (
|
||||
Connection,
|
||||
|
@ -733,18 +735,8 @@ lintTable allEntries parsed =
|
|||
<> " does not have a rowid column. "
|
||||
<> "Such tables are not currently supported by Airsequel."
|
||||
_ -> []
|
||||
|
||||
illegalName = case parsed.statement of
|
||||
CreateTable names _ _
|
||||
| Just name <- getFirstName (Just names)
|
||||
, "_by_pk" `isSuffixOf` name ->
|
||||
pure $
|
||||
"Table names shouldn't contain \"_by_pk\", yet \""
|
||||
<> name
|
||||
<> "\" does"
|
||||
_ -> []
|
||||
in
|
||||
rowidReferenceWarnings <> withoutRowidWarning <> illegalName
|
||||
rowidReferenceWarnings <> withoutRowidWarning
|
||||
|
||||
|
||||
{-| Lint the sql code for creating a table
|
||||
|
@ -778,6 +770,21 @@ getRowidColumnName colNames
|
|||
| otherwise = "rowid" -- TODO: Return error to user
|
||||
|
||||
|
||||
{-| Select the column(s) that form this table's primary key. If no non-rowid
|
||||
columns are marked as part of a PK constraint, the rowid column will be
|
||||
returned instead.
|
||||
-}
|
||||
getPKColumns :: TableEntry -> Maybe [ColumnEntry]
|
||||
getPKColumns table = do
|
||||
let pks = List.filter (\col -> col.primary_key) table.columns
|
||||
|
||||
-- We filter out the rowid column, unless it is the only one
|
||||
case pks of
|
||||
[] -> Nothing
|
||||
[first] | first.isRowid -> Just [first]
|
||||
_ -> Just $ List.filter (\col -> P.not col.isRowid) pks
|
||||
|
||||
|
||||
columnDefName :: ColumnDef -> Text
|
||||
columnDefName (ColumnDef name _ _) = nameAsText name
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue