Skip to content

Commit f9f4924

Browse files
author
Auke Booij
committed
[skip ci] Avoid infinite recursion in schema generation
Please see the note that this commit adds for details.
1 parent 12451f0 commit f9f4924

File tree

3 files changed

+71
-17
lines changed

3 files changed

+71
-17
lines changed

server/src-lib/Hasura/GraphQL/Parser/Internal/Parser.hs

+2
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ fieldOptional name description parser = FieldsParser
285285
, ifParser = M.lookup name >>> withPath (Key (unName name) :) . traverse (pInputParser parser)
286286
}
287287

288+
-- Should this rather take a non-empty `FieldParser` list?
289+
-- See also Note [Selectability of tables].
288290
selectionSet
289291
:: MonadParse m
290292
=> Name

server/src-lib/Hasura/GraphQL/Schema/Mutation.hs

+3-3
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ updateTableByPk table fieldName description updatePerms selectPerms stringifyNum
311311
columns <- lift $ tableSelectColumns table selectPerms
312312
pkArgs <- MaybeT $ primaryKeysArguments table selectPerms
313313
opArgs <- MaybeT $ updateOperators table updatePerms
314-
selection <- MaybeT $ tableSelectionSet table selectPerms stringifyNum
314+
selection <- lift $ tableSelectionSet table selectPerms stringifyNum
315315
let argsParser = liftA2 (,) opArgs pkArgs
316316
pure $ P.selection fieldName description argsParser selection
317317
`mapField` (mkUpdateObject table columns updatePerms . third RQL.MOutSinglerowObject)
@@ -463,7 +463,7 @@ deleteFromTableByPk
463463
deleteFromTableByPk table fieldName description deletePerms selectPerms stringifyNum = runMaybeT $ do
464464
columns <- lift $ tableSelectColumns table selectPerms
465465
pkArgs <- MaybeT $ primaryKeysArguments table selectPerms
466-
selection <- MaybeT $ tableSelectionSet table selectPerms stringifyNum
466+
selection <- lift $ tableSelectionSet table selectPerms stringifyNum
467467
pure $ P.selection fieldName description pkArgs selection
468468
`mapField` (mkDeleteObject table columns deletePerms . third RQL.MOutSinglerowObject)
469469

@@ -496,7 +496,7 @@ mutationSelectionSet table selectPerms stringifyNum = do
496496
tableName <- qualifiedObjectToName table
497497
returning <- runMaybeT do
498498
permissions <- MaybeT $ pure selectPerms
499-
tableSet <- MaybeT $ tableSelectionSet table permissions stringifyNum
499+
tableSet <- lift $ tableSelectionSet table permissions stringifyNum
500500
let returningName = $$(G.litName "returning")
501501
returningDesc = "data from the rows affected by the mutation"
502502
pure $ P.selection_ returningName (Just returningDesc) tableSet

server/src-lib/Hasura/GraphQL/Schema/Select.hs

+66-14
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ selectTable
7474
-> m (Maybe (FieldsParser 'Output n (Maybe (G.Name, SelectExp))))
7575
selectTable table fieldName description selectPermissions stringifyNum = runMaybeT do
7676
tableArgsParser <- lift $ tableArgs table selectPermissions
77-
selectionSetParser <- MaybeT $ tableSelectionSet table selectPermissions stringifyNum
77+
selectionSetParser <- lift $ tableSelectionSet table selectPermissions stringifyNum
7878
pure $ P.selection fieldName description tableArgsParser selectionSetParser `mapField`
7979
\(aliasName, args, fields) ->
8080
( aliasName
@@ -113,7 +113,7 @@ selectTableByPk table fieldName description selectPermissions stringifyNum = run
113113
field <- P.column (pgiType columnInfo) (G.Nullability $ pgiIsNullable columnInfo)
114114
pure $ BoolFld . AVCol columnInfo . pure . AEQ True . mkParameter <$>
115115
P.field (pgiName columnInfo) (pgiDescription columnInfo) field
116-
selectionSetParser <- MaybeT $ tableSelectionSet table selectPermissions stringifyNum
116+
selectionSetParser <- lift $ tableSelectionSet table selectPermissions stringifyNum
117117
pure $ P.selection fieldName description argsParser selectionSetParser
118118
`mapField` \(aliasName, boolExpr, fields) ->
119119
let defaultPerms = tablePermissionsInfo selectPermissions
@@ -154,7 +154,7 @@ selectTableAggregate table fieldName description selectPermissions stringifyNum
154154
tableArgsParser <- lift $ tableArgs table selectPermissions
155155
aggregateParser <- lift $ tableAggregationFields table selectPermissions
156156
selectionName <- lift $ qualifiedObjectToName table <&> (<> $$(G.litName "_aggregate"))
157-
nodesParser <- MaybeT $ tableSelectionSet table selectPermissions stringifyNum
157+
nodesParser <- lift $ tableSelectionSet table selectPermissions stringifyNum
158158
let typenameRepr = Just ($$(G.litName "__typename"), RQL.TAFExp $ G.unName selectionName)
159159
aggregationParser =
160160
fmap catMaybes $ P.selectionSet selectionName Nothing typenameRepr $ sequenceA
@@ -175,6 +175,54 @@ selectTableAggregate table fieldName description selectPermissions stringifyNum
175175
}
176176
)
177177

178+
{- Note [Selectability of tables]
179+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
180+
181+
The GraphQL specification requires that if the type of a selected field is an
182+
interface, union, or object, then its subselection set must not be empty
183+
(Section 5.3.3). Since we model database tables by GraphQL objects, this means
184+
that a table can be selected as a GraphQL field only if it has fields that we
185+
can select, such as a column. It is perfectly fine not to allow any selections
186+
of any columns of the table in the database. In that case, the table would not
187+
be selectable as a field in GraphQL.
188+
189+
However, this is not the end of the story. In addition to scalar fields, we
190+
support relationships between tables, so that we may have another table B as a
191+
selected field of this table A. Then the selectability of A depends on the
192+
selectability of B: if we permit selection a column of B, then, as a
193+
consequence, we permit selection of the relationship from A to B, and hence we
194+
permit selection of A, as there would now be valid GraphQL syntax that selects
195+
A. In turn, the selectability of B can depend on the selectability of a further
196+
table C, through a relationship from B to C.
197+
198+
Now consider the case of a table A, whose columns themselves are not selectable,
199+
but which has a relationship with itself. Is A selectable? In fact, if A has
200+
no further relationships with other tables, or any computed fields, A is not
201+
selectable. But as soon as any leaf field in the transitive closure of tables
202+
related to A becomes selectable, A itself becomes selectable.
203+
204+
In summary, figuring out the selectability of a table is a mess. In order to
205+
avoid doing graph theory, for now, we simply pretend that GraphQL did not have
206+
the restriction of only allowing selections of fields of type objects when its
207+
subselection is non-empty. In practice, this white lie is somewhat unlikely to
208+
cause errors on the client side, for the following reasons:
209+
210+
- Introspection of the GraphQL schema is normally provided to aid development of
211+
valid GraphQL schemas, and so any errors in the exposed schema can be caught
212+
at development time: when a developer is building a GraphQL query using schema
213+
introspection, they will eventually find out that the selection they aim to do
214+
is not valid GraphQL.
215+
216+
- We only support tables that have at least one column (since we require primary
217+
keys), so that the admin role can select every table anyway.
218+
219+
However, at the time of writing this note I am not convinced that all we are
220+
doing is exposing non-existent schema: we should also make sure not to parse
221+
invalid GraphQL. So we probably eventually need to figure out the graph theory
222+
underlying this and fix this, or altenatively convince ourselves that we have
223+
set up our parsers in such a way that selections of fields of type objects do
224+
require non-empty subselections.
225+
-}
178226

179227
-- | Fields of a table
180228
--
@@ -184,21 +232,25 @@ tableSelectionSet
184232
=> QualifiedTable
185233
-> SelPermInfo
186234
-> Bool
187-
-> m (Maybe (Parser 'Output n AnnotatedFields))
188-
tableSelectionSet table selectPermissions stringifyNum = do
235+
-> m (Parser 'Output n AnnotatedFields)
236+
tableSelectionSet table selectPermissions stringifyNum = memoizeOn 'tableSelectionSet table do
189237
tableInfo <- _tiCoreInfo <$> askTableInfo table
190238
tableName <- qualifiedObjectToName table
191239
let tableFields = Map.elems $ _tciFieldInfoMap tableInfo
192240
fieldParsers <- fmap concat $ for tableFields \fieldInfo ->
193241
fieldSelection fieldInfo selectPermissions stringifyNum
194-
whenMaybe (not $ null fieldParsers) do
195-
let typenameRepr = (FieldName "__typename", RQL.FExp $ G.unName tableName)
196-
description = G.Description . getPGDescription <$> _tciDescription tableInfo
197-
memoizeOn 'tableSelectionSet table
198-
$ pure
199-
$ P.selectionSet tableName description typenameRepr
200-
$ fmap catMaybes
201-
$ sequenceA fieldParsers
242+
243+
-- TODO Here we *don't* check if the subselection set is non-empty, even
244+
-- though the GraphQL specification requires that it is. See Note
245+
-- [Selectability of tables]
246+
247+
-- whenMaybe (not $ null fieldParsers) do
248+
let typenameRepr = (FieldName "__typename", RQL.FExp $ G.unName tableName)
249+
description = G.Description . getPGDescription <$> _tciDescription tableInfo
250+
pure
251+
$ P.selectionSet tableName description typenameRepr
252+
$ fmap catMaybes
253+
$ sequenceA fieldParsers
202254

203255

204256

@@ -498,7 +550,7 @@ computedField ComputedFieldInfo{..} selectPermissions stringifyNum = runMaybeT d
498550
CFRSetofTable tableName -> do
499551
remotePerms <- MaybeT $ tableSelectPermissions tableName
500552
selectArgsParser <- lift $ tableArgs tableName remotePerms
501-
selectionSetParser <- MaybeT $ tableSelectionSet tableName remotePerms stringifyNum
553+
selectionSetParser <- lift $ tableSelectionSet tableName remotePerms stringifyNum
502554
let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser
503555
pure $ P.selection fieldName Nothing fieldArgsParser selectionSetParser
504556
`mapField` \(aliasName, (functionArgs, args), fields) ->

0 commit comments

Comments
 (0)