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:
parent
8894f14002
commit
8febe6be43
2 changed files with 86 additions and 35 deletions
source/AirGQL
|
@ -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 =
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue