@@ -74,7 +74,7 @@ selectTable
74
74
-> m (Maybe (FieldsParser 'Output n (Maybe (G. Name , SelectExp ))))
75
75
selectTable table fieldName description selectPermissions stringifyNum = runMaybeT do
76
76
tableArgsParser <- lift $ tableArgs table selectPermissions
77
- selectionSetParser <- MaybeT $ tableSelectionSet table selectPermissions stringifyNum
77
+ selectionSetParser <- lift $ tableSelectionSet table selectPermissions stringifyNum
78
78
pure $ P. selection fieldName description tableArgsParser selectionSetParser `mapField`
79
79
\ (aliasName, args, fields) ->
80
80
( aliasName
@@ -113,7 +113,7 @@ selectTableByPk table fieldName description selectPermissions stringifyNum = run
113
113
field <- P. column (pgiType columnInfo) (G. Nullability $ pgiIsNullable columnInfo)
114
114
pure $ BoolFld . AVCol columnInfo . pure . AEQ True . mkParameter <$>
115
115
P. field (pgiName columnInfo) (pgiDescription columnInfo) field
116
- selectionSetParser <- MaybeT $ tableSelectionSet table selectPermissions stringifyNum
116
+ selectionSetParser <- lift $ tableSelectionSet table selectPermissions stringifyNum
117
117
pure $ P. selection fieldName description argsParser selectionSetParser
118
118
`mapField` \ (aliasName, boolExpr, fields) ->
119
119
let defaultPerms = tablePermissionsInfo selectPermissions
@@ -154,7 +154,7 @@ selectTableAggregate table fieldName description selectPermissions stringifyNum
154
154
tableArgsParser <- lift $ tableArgs table selectPermissions
155
155
aggregateParser <- lift $ tableAggregationFields table selectPermissions
156
156
selectionName <- lift $ qualifiedObjectToName table <&> (<> $$ (G. litName " _aggregate" ))
157
- nodesParser <- MaybeT $ tableSelectionSet table selectPermissions stringifyNum
157
+ nodesParser <- lift $ tableSelectionSet table selectPermissions stringifyNum
158
158
let typenameRepr = Just ($$ (G. litName " __typename" ), RQL. TAFExp $ G. unName selectionName)
159
159
aggregationParser =
160
160
fmap catMaybes $ P. selectionSet selectionName Nothing typenameRepr $ sequenceA
@@ -175,6 +175,54 @@ selectTableAggregate table fieldName description selectPermissions stringifyNum
175
175
}
176
176
)
177
177
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
+ -}
178
226
179
227
-- | Fields of a table
180
228
--
@@ -184,21 +232,25 @@ tableSelectionSet
184
232
=> QualifiedTable
185
233
-> SelPermInfo
186
234
-> 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
189
237
tableInfo <- _tiCoreInfo <$> askTableInfo table
190
238
tableName <- qualifiedObjectToName table
191
239
let tableFields = Map. elems $ _tciFieldInfoMap tableInfo
192
240
fieldParsers <- fmap concat $ for tableFields \ fieldInfo ->
193
241
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
202
254
203
255
204
256
@@ -498,7 +550,7 @@ computedField ComputedFieldInfo{..} selectPermissions stringifyNum = runMaybeT d
498
550
CFRSetofTable tableName -> do
499
551
remotePerms <- MaybeT $ tableSelectPermissions tableName
500
552
selectArgsParser <- lift $ tableArgs tableName remotePerms
501
- selectionSetParser <- MaybeT $ tableSelectionSet tableName remotePerms stringifyNum
553
+ selectionSetParser <- lift $ tableSelectionSet tableName remotePerms stringifyNum
502
554
let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser
503
555
pure $ P. selection fieldName Nothing fieldArgsParser selectionSetParser
504
556
`mapField` \ (aliasName, (functionArgs, args), fields) ->
0 commit comments