1
Fork 0
mirror of https://github.com/Airsequel/AirGQL.git synced 2025-07-30 09:47:01 +03:00

Implement introspection for by_pk queries

- Make column names containing "_by_pk" disallowed on the client side (I
technically already did that on the server side with the previous
commit, but this makes it nicer)
- Implement the actual introspection logic
- If a primary key has a "reserved" argument name (i.e. a name we use
for built in airsequel arguments) like "filter" or "limit", then we
rename that to pk from <name> to <name>_. This might technically still
create collisions in the rare case where both <name> and <name>_ are
columns that are part of the same primary key, but I don't think that
matters in practice. The hasura playground doesn't seem to do any
renaming (I tried it in the playground), so we could also not do that at
all I guess...
This commit is contained in:
prescientmoon 2024-10-09 19:15:39 +02:00
commit 8febe6be43
2 changed files with 86 additions and 35 deletions
source/AirGQL

View file

@ -30,7 +30,7 @@ import AirGQL.Introspection.Types (IntrospectionType)
import AirGQL.Introspection.Types qualified as Type
import AirGQL.Lib (
AccessMode,
ColumnEntry,
ColumnEntry (isRowid, primary_key),
GqlTypeName (full),
TableEntry (columns, name),
canRead,
@ -41,6 +41,7 @@ import AirGQL.Lib (
notnull,
)
import Control.Monad (MonadFail (fail))
import Data.List qualified as List
import Data.Text qualified as T
import DoubleXEncoding (doubleXEncodeGql)
import Language.GraphQL.Class (ToGraphQL (toGraphQL))
@ -130,39 +131,80 @@ tableRowType table = do
Type.object (doubleXEncodeGql table.name <> "_row") fields
tableQueryCommonArgs :: TableEntry -> [Type.InputValue]
tableQueryCommonArgs table =
let
fieldsWithOrderingTerm =
table.columns <&> \columnEntry -> do
let colName = columnEntry.column_name_gql
Type.inputValue colName orderingTermType
orderType =
Type.inputObject
(doubleXEncodeGql table.name <> "_order_by")
fieldsWithOrderingTerm
& Type.withDescription
( "Ordering options when selecting data from \""
<> table.name
<> "\"."
)
in
[ Type.inputValue "filter" (filterType table)
& Type.inputValueWithDescription "Filter to select specific rows"
, Type.inputValue "order_by" (Type.list orderType)
& Type.inputValueWithDescription "Columns used to sort the data"
, Type.inputValue "limit" Type.typeInt
& Type.inputValueWithDescription "Limit the number of returned rows"
, Type.inputValue "offset" Type.typeInt
& Type.inputValueWithDescription "The index to start returning rows from"
]
tableQueryField :: TableEntry -> Type.Field
tableQueryField table = do
let tableName = table.name
tableQueryField table =
Type.field
(doubleXEncodeGql table.name)
(Type.nonNull $ Type.list $ Type.nonNull $ tableRowType table)
& Type.fieldWithDescription ("Rows from the table \"" <> table.name <> "\"")
& Type.withArguments (tableQueryCommonArgs table)
let fieldsWithOrderingTerm =
table.columns <&> \columnEntry -> do
let colName = columnEntry.column_name_gql
Type.inputValue colName orderingTermType
let orderType =
Type.inputObject
(doubleXEncodeGql tableName <> "_order_by")
fieldsWithOrderingTerm
& Type.withDescription
( "Ordering options when selecting data from \""
<> table.name
<> "\"."
)
restrictedArgNames :: [Text]
restrictedArgNames = ["limit", "offset", "order_by", "filter"]
mkArgName :: Text -> Text
mkArgName name = do
let encoded = doubleXEncodeGql name
if P.elem encoded restrictedArgNames
then encoded <> "_"
else encoded
tableQueryByPk :: TableEntry -> Type.Field
tableQueryByPk table = do
let pks = List.filter (\col -> col.primary_key) table.columns
-- We filter out the rowid column, unless it is the only one
let withoutRowid = case pks of
[first] | first.isRowid -> [first]
_ -> List.filter (\col -> P.not col.isRowid) pks
let pkArguments =
withoutRowid <&> \column -> do
let name = mkArgName column.column_name_gql
Type.inputValue name $ Type.nonNull $ columnType column
Type.field
(doubleXEncodeGql tableName)
(Type.nonNull $ Type.list $ Type.nonNull $ tableRowType table)
& Type.fieldWithDescription ("Rows from the table \"" <> tableName <> "\"")
& Type.withArguments
[ Type.inputValue "filter" (filterType table)
& Type.inputValueWithDescription "Filter to select specific rows"
, Type.inputValue "order_by" (Type.list orderType)
& Type.inputValueWithDescription "Columns used to sort the data"
, Type.inputValue "limit" Type.typeInt
& Type.inputValueWithDescription "Limit the number of returned rows"
, Type.inputValue "offset" Type.typeInt
& Type.inputValueWithDescription "The index to start returning rows from"
]
(doubleXEncodeGql table.name <> "_by_pk")
(tableRowType table)
& Type.fieldWithDescription
( "Rows from the table \""
<> table.name
<> "\", accessible by their primary key"
)
& Type.withArguments (tableQueryCommonArgs table)
& Type.withArguments pkArguments
mutationResponseType :: AccessMode -> TableEntry -> Type.IntrospectionType
@ -318,7 +360,11 @@ getSchema accessMode tables = do
let
queryType =
if canRead accessMode
then tables <&> tableQueryField
then
P.fold
[ tables <&> tableQueryField
, tables <&> tableQueryByPk
]
else []
mutationType =

View file

@ -30,10 +30,6 @@ module AirGQL.Introspection.Types (
deprecatedEnumValue,
) where
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Language.GraphQL.Class (ToGraphQL (toGraphQL))
import Language.GraphQL.Type qualified as Value
import Protolude (
Bool (False, True),
Generic,
@ -51,8 +47,14 @@ import Protolude (
when,
($),
(&),
(<>),
)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Language.GraphQL.Class (ToGraphQL (toGraphQL))
import Language.GraphQL.Type qualified as Value
data Schema = Schema
{ description :: Maybe Text
@ -334,7 +336,7 @@ field fieldName fieldType =
withArguments :: [InputValue] -> Field -> Field
withArguments argList (Field{..}) = Field{args = argList, ..}
withArguments argList (Field{..}) = Field{args = args <> argList, ..}
data InputValue = InputValue
@ -448,6 +450,9 @@ typeBool :: IntrospectionType
typeBool = scalar "Boolean"
{-| Updates the `types` property of a schema to reference
every type contained in other parts of the schema.
-}
collectSchemaTypes :: Schema -> Schema
collectSchemaTypes schema = do
let basic = [typeInt, typeString, typeBool, typeSchema]