mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
3c3ed55914
This PR makes a bunch of schema generation code in Hasura.GraphQL.Schema backend-agnostic, by moving the backend-specific parts into a new BackendSchema type class. This way, the schema generation code can be reused for other backends, simply by implementing new instances of the BackendSchema type class. This work is now in a state where the schema generators are sufficiently generic to accept the implementation of a new backend. That means that we can start exposing MS SQL schema. Execution is not implemented yet, of course. The branch currently does not support computed fields or Relay. This is, in a sense, intentional: computed field support is normally baked into the schema generation (through the fieldSelection schema generator), and so this branch shows a programming technique that allows us to expose certain GraphQL schema depending on backend support. We can write support for computed fields and Relay at a later stage. Co-authored-by: Antoine Leblanc <antoine@hasura.io> GitOrigin-RevId: df369fc3d189cbda1b931d31678e9450a6601314
109 lines
4.3 KiB
Haskell
109 lines
4.3 KiB
Haskell
module Hasura.GraphQL.Schema.BoolExp
|
||
( boolExp
|
||
) where
|
||
|
||
import Hasura.Prelude
|
||
|
||
import qualified Language.GraphQL.Draft.Syntax as G
|
||
|
||
import Data.Text.Extended
|
||
|
||
import qualified Hasura.GraphQL.Parser as P
|
||
|
||
import Hasura.GraphQL.Parser (InputFieldsParser, Kind (..), Parser,
|
||
UnpreparedValue)
|
||
import Hasura.GraphQL.Parser.Class
|
||
import Hasura.GraphQL.Schema.Backend
|
||
import Hasura.GraphQL.Schema.Table
|
||
import Hasura.RQL.Types
|
||
|
||
-- |
|
||
-- > input type_bool_exp {
|
||
-- > _or: [type_bool_exp!]
|
||
-- > _and: [type_bool_exp!]
|
||
-- > _not: type_bool_exp
|
||
-- > column: type_comparison_exp
|
||
-- > ...
|
||
-- > }
|
||
boolExp
|
||
:: forall m n r b. (BackendSchema b, MonadSchema n m, MonadTableInfo b r m, MonadRole r m)
|
||
=> TableName b
|
||
-> Maybe (SelPermInfo b)
|
||
-> m (Parser 'Input n (AnnBoolExp b (UnpreparedValue b)))
|
||
boolExp table selectPermissions = memoizeOn 'boolExp table $ do
|
||
tableGQLName <- getTableGQLName @b table
|
||
let name = tableGQLName <> $$(G.litName "_bool_exp")
|
||
let description = G.Description $
|
||
"Boolean expression to filter rows from the table " <> table <<>
|
||
". All fields are combined with a logical 'AND'."
|
||
|
||
tableFieldParsers <- catMaybes <$> maybe
|
||
(pure [])
|
||
(traverse mkField <=< tableSelectFields table)
|
||
selectPermissions
|
||
recur <- boolExp table selectPermissions
|
||
-- Bafflingly, ApplicativeDo doesn’t work if we inline this definition (I
|
||
-- think the TH splices throw it off), so we have to define it separately.
|
||
let specialFieldParsers =
|
||
[ P.fieldOptional $$(G.litName "_or") Nothing (BoolOr <$> P.list recur)
|
||
, P.fieldOptional $$(G.litName "_and") Nothing (BoolAnd <$> P.list recur)
|
||
, P.fieldOptional $$(G.litName "_not") Nothing (BoolNot <$> recur)
|
||
]
|
||
|
||
pure $ BoolAnd <$> P.object name (Just description) do
|
||
tableFields <- map BoolFld . catMaybes <$> sequenceA tableFieldParsers
|
||
specialFields <- catMaybes <$> sequenceA specialFieldParsers
|
||
pure (tableFields ++ specialFields)
|
||
where
|
||
mkField
|
||
:: FieldInfo b
|
||
-> m (Maybe (InputFieldsParser n (Maybe (AnnBoolExpFld b (UnpreparedValue b)))))
|
||
mkField fieldInfo = runMaybeT do
|
||
fieldName <- MaybeT $ pure $ fieldInfoGraphQLName fieldInfo
|
||
P.fieldOptional fieldName Nothing <$> case fieldInfo of
|
||
-- field_name: field_type_comparison_exp
|
||
FIColumn columnInfo ->
|
||
lift $ fmap (AVCol columnInfo) <$> comparisonExps @b (pgiType columnInfo)
|
||
|
||
-- field_name: field_type_bool_exp
|
||
FIRelationship relationshipInfo -> do
|
||
let remoteTable = riRTable relationshipInfo
|
||
remotePermissions <- lift $ tableSelectPermissions remoteTable
|
||
lift $ fmap (AVRel relationshipInfo) <$> boolExp remoteTable remotePermissions
|
||
|
||
-- Using computed fields in boolean expressions is not currently supported.
|
||
FIComputedField _ -> empty
|
||
|
||
-- Using remote relationship fields in boolean expressions is not supported.
|
||
FIRemoteRelationship _ -> empty
|
||
|
||
{- Note [Columns in comparison expression are never nullable]
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
In comparisonExps, we hardcode `Nullability False` when calling `column`, which
|
||
might seem a bit sketchy. Shouldn’t the nullability depend on the nullability of
|
||
the underlying Postgres column?
|
||
|
||
No. If we did that, then we would allow boolean expressions like this:
|
||
|
||
delete_users(where: {status: {eq: null}})
|
||
|
||
The user expects this to generate SQL like
|
||
|
||
DELETE FROM users WHERE users.status IS NULL
|
||
|
||
but it doesn’t. We treat null to mean “no condition was specified” (since
|
||
that’s how GraphQL indicates an optional field was omitted), and we actually
|
||
generate SQL like this:
|
||
|
||
DELETE FROM users
|
||
|
||
Now we’ve gone and deleted every user in the database. Hopefully the user had
|
||
backups!
|
||
|
||
We avoid this problem by making the column value non-nullable (which is correct,
|
||
since we never treat a null value as a SQL NULL), then creating the field using
|
||
fieldOptional. This creates a parser that rejects nulls, but won’t be called at
|
||
all if the field is not specified, which is permitted by the GraphQL
|
||
specification. See Note [Optional fields and nullability] in
|
||
Hasura.GraphQL.Parser.Internal.Parser for more details. -}
|