1
Fork 0
mirror of https://github.com/Airsequel/AirGQL.git synced 2025-09-22 20:14:31 +02: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.Introspection.Types qualified as Type
import AirGQL.Lib ( import AirGQL.Lib (
AccessMode, AccessMode,
ColumnEntry, ColumnEntry (isRowid, primary_key),
GqlTypeName (full), GqlTypeName (full),
TableEntry (columns, name), TableEntry (columns, name),
canRead, canRead,
@ -41,6 +41,7 @@ import AirGQL.Lib (
notnull, notnull,
) )
import Control.Monad (MonadFail (fail)) import Control.Monad (MonadFail (fail))
import Data.List qualified as List
import Data.Text qualified as T import Data.Text qualified as T
import DoubleXEncoding (doubleXEncodeGql) import DoubleXEncoding (doubleXEncodeGql)
import Language.GraphQL.Class (ToGraphQL (toGraphQL)) import Language.GraphQL.Class (ToGraphQL (toGraphQL))
@ -130,39 +131,80 @@ tableRowType table = do
Type.object (doubleXEncodeGql table.name <> "_row") fields 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 :: TableEntry -> Type.Field
tableQueryField table = do tableQueryField table =
let tableName = table.name 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 = restrictedArgNames :: [Text]
Type.inputObject restrictedArgNames = ["limit", "offset", "order_by", "filter"]
(doubleXEncodeGql tableName <> "_order_by")
fieldsWithOrderingTerm
& Type.withDescription mkArgName :: Text -> Text
( "Ordering options when selecting data from \"" mkArgName name = do
<> table.name 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 Type.field
(doubleXEncodeGql tableName) (doubleXEncodeGql table.name <> "_by_pk")
(Type.nonNull $ Type.list $ Type.nonNull $ tableRowType table) (tableRowType table)
& Type.fieldWithDescription ("Rows from the table \"" <> tableName <> "\"") & Type.fieldWithDescription
& Type.withArguments ( "Rows from the table \""
[ Type.inputValue "filter" (filterType table) <> table.name
& Type.inputValueWithDescription "Filter to select specific rows" <> "\", accessible by their primary key"
, Type.inputValue "order_by" (Type.list orderType) )
& Type.inputValueWithDescription "Columns used to sort the data" & Type.withArguments (tableQueryCommonArgs table)
, Type.inputValue "limit" Type.typeInt & Type.withArguments pkArguments
& Type.inputValueWithDescription "Limit the number of returned rows"
, Type.inputValue "offset" Type.typeInt
& Type.inputValueWithDescription "The index to start returning rows from"
]
mutationResponseType :: AccessMode -> TableEntry -> Type.IntrospectionType mutationResponseType :: AccessMode -> TableEntry -> Type.IntrospectionType
@ -318,7 +360,11 @@ getSchema accessMode tables = do
let let
queryType = queryType =
if canRead accessMode if canRead accessMode
then tables <&> tableQueryField then
P.fold
[ tables <&> tableQueryField
, tables <&> tableQueryByPk
]
else [] else []
mutationType = mutationType =

View file

@ -30,10 +30,6 @@ module AirGQL.Introspection.Types (
deprecatedEnumValue, deprecatedEnumValue,
) where ) 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 ( import Protolude (
Bool (False, True), Bool (False, True),
Generic, Generic,
@ -51,8 +47,14 @@ import Protolude (
when, 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 data Schema = Schema
{ description :: Maybe Text { description :: Maybe Text
@ -334,7 +336,7 @@ field fieldName fieldType =
withArguments :: [InputValue] -> Field -> Field withArguments :: [InputValue] -> Field -> Field
withArguments argList (Field{..}) = Field{args = argList, ..} withArguments argList (Field{..}) = Field{args = args <> argList, ..}
data InputValue = InputValue data InputValue = InputValue
@ -448,6 +450,9 @@ typeBool :: IntrospectionType
typeBool = scalar "Boolean" 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 -> Schema
collectSchemaTypes schema = do collectSchemaTypes schema = do
let basic = [typeInt, typeString, typeBool, typeSchema] let basic = [typeInt, typeString, typeBool, typeSchema]