1
Fork 0
mirror of https://github.com/Airsequel/AirGQL.git synced 2025-07-27 21:51:11 +03:00

Prevent naming conflicts for _by_pk queries/mutations

This commit is contained in:
prescientmoon 2024-11-20 00:30:49 +01:00
commit 7a9914325d
5 changed files with 113 additions and 46 deletions

View file

@ -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 =

View file

@ -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 []

View 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

View file

@ -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)
]

View file

@ -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