graphile-connection-filter 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +107 -0
- package/augmentations.d.ts +104 -0
- package/augmentations.js +11 -0
- package/esm/augmentations.d.ts +104 -0
- package/esm/augmentations.js +9 -0
- package/esm/index.d.ts +55 -0
- package/esm/index.js +56 -0
- package/esm/plugins/ConnectionFilterArgPlugin.d.ts +13 -0
- package/esm/plugins/ConnectionFilterArgPlugin.js +96 -0
- package/esm/plugins/ConnectionFilterAttributesPlugin.d.ts +14 -0
- package/esm/plugins/ConnectionFilterAttributesPlugin.js +79 -0
- package/esm/plugins/ConnectionFilterBackwardRelationsPlugin.d.ts +33 -0
- package/esm/plugins/ConnectionFilterBackwardRelationsPlugin.js +398 -0
- package/esm/plugins/ConnectionFilterComputedAttributesPlugin.d.ts +19 -0
- package/esm/plugins/ConnectionFilterComputedAttributesPlugin.js +133 -0
- package/esm/plugins/ConnectionFilterCustomOperatorsPlugin.d.ts +35 -0
- package/esm/plugins/ConnectionFilterCustomOperatorsPlugin.js +129 -0
- package/esm/plugins/ConnectionFilterForwardRelationsPlugin.d.ts +28 -0
- package/esm/plugins/ConnectionFilterForwardRelationsPlugin.js +168 -0
- package/esm/plugins/ConnectionFilterInflectionPlugin.d.ts +11 -0
- package/esm/plugins/ConnectionFilterInflectionPlugin.js +27 -0
- package/esm/plugins/ConnectionFilterLogicalOperatorsPlugin.d.ts +15 -0
- package/esm/plugins/ConnectionFilterLogicalOperatorsPlugin.js +86 -0
- package/esm/plugins/ConnectionFilterOperatorsPlugin.d.ts +21 -0
- package/esm/plugins/ConnectionFilterOperatorsPlugin.js +677 -0
- package/esm/plugins/ConnectionFilterTypesPlugin.d.ts +12 -0
- package/esm/plugins/ConnectionFilterTypesPlugin.js +225 -0
- package/esm/plugins/index.d.ts +11 -0
- package/esm/plugins/index.js +11 -0
- package/esm/plugins/operatorApply.d.ts +11 -0
- package/esm/plugins/operatorApply.js +70 -0
- package/esm/preset.d.ts +35 -0
- package/esm/preset.js +72 -0
- package/esm/types.d.ts +146 -0
- package/esm/types.js +4 -0
- package/esm/utils.d.ts +44 -0
- package/esm/utils.js +112 -0
- package/index.d.ts +55 -0
- package/index.js +77 -0
- package/package.json +58 -0
- package/plugins/ConnectionFilterArgPlugin.d.ts +13 -0
- package/plugins/ConnectionFilterArgPlugin.js +99 -0
- package/plugins/ConnectionFilterAttributesPlugin.d.ts +14 -0
- package/plugins/ConnectionFilterAttributesPlugin.js +82 -0
- package/plugins/ConnectionFilterBackwardRelationsPlugin.d.ts +33 -0
- package/plugins/ConnectionFilterBackwardRelationsPlugin.js +401 -0
- package/plugins/ConnectionFilterComputedAttributesPlugin.d.ts +19 -0
- package/plugins/ConnectionFilterComputedAttributesPlugin.js +136 -0
- package/plugins/ConnectionFilterCustomOperatorsPlugin.d.ts +35 -0
- package/plugins/ConnectionFilterCustomOperatorsPlugin.js +132 -0
- package/plugins/ConnectionFilterForwardRelationsPlugin.d.ts +28 -0
- package/plugins/ConnectionFilterForwardRelationsPlugin.js +171 -0
- package/plugins/ConnectionFilterInflectionPlugin.d.ts +11 -0
- package/plugins/ConnectionFilterInflectionPlugin.js +30 -0
- package/plugins/ConnectionFilterLogicalOperatorsPlugin.d.ts +15 -0
- package/plugins/ConnectionFilterLogicalOperatorsPlugin.js +89 -0
- package/plugins/ConnectionFilterOperatorsPlugin.d.ts +21 -0
- package/plugins/ConnectionFilterOperatorsPlugin.js +680 -0
- package/plugins/ConnectionFilterTypesPlugin.d.ts +12 -0
- package/plugins/ConnectionFilterTypesPlugin.js +228 -0
- package/plugins/index.d.ts +11 -0
- package/plugins/index.js +25 -0
- package/plugins/operatorApply.d.ts +11 -0
- package/plugins/operatorApply.js +73 -0
- package/preset.d.ts +35 -0
- package/preset.js +75 -0
- package/types.d.ts +146 -0
- package/types.js +7 -0
- package/utils.d.ts +44 -0
- package/utils.js +119 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionFilterBackwardRelationsPlugin = void 0;
|
|
4
|
+
require("../augmentations");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const version = '1.0.0';
|
|
7
|
+
/**
|
|
8
|
+
* ConnectionFilterBackwardRelationsPlugin
|
|
9
|
+
*
|
|
10
|
+
* Adds backward relation filter fields to table filter types.
|
|
11
|
+
* A "backward" relation is one where another table has a FK referencing the current table.
|
|
12
|
+
*
|
|
13
|
+
* For unique backward relations (one-to-one), a single filter field is added:
|
|
14
|
+
* ```graphql
|
|
15
|
+
* allClients(filter: {
|
|
16
|
+
* profileByClientId: { bio: { includes: "engineer" } }
|
|
17
|
+
* }) { ... }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* For non-unique backward relations (one-to-many), a "many" filter type is added
|
|
21
|
+
* with `some`, `every`, and `none` sub-fields:
|
|
22
|
+
* ```graphql
|
|
23
|
+
* allClients(filter: {
|
|
24
|
+
* ordersByClientId: { some: { total: { greaterThan: 1000 } } }
|
|
25
|
+
* }) { ... }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* The SQL generated uses EXISTS subqueries:
|
|
29
|
+
* ```sql
|
|
30
|
+
* WHERE EXISTS (
|
|
31
|
+
* SELECT 1 FROM orders
|
|
32
|
+
* WHERE orders.client_id = clients.id
|
|
33
|
+
* AND <nested filter conditions>
|
|
34
|
+
* )
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
exports.ConnectionFilterBackwardRelationsPlugin = {
|
|
38
|
+
name: 'ConnectionFilterBackwardRelationsPlugin',
|
|
39
|
+
version,
|
|
40
|
+
description: 'Adds backward relation filter fields to connection filter types',
|
|
41
|
+
inflection: {
|
|
42
|
+
add: {
|
|
43
|
+
filterManyType(_preset, table, foreignTable) {
|
|
44
|
+
return this.upperCamelCase(`${this.tableType(table)}-to-many-${this.tableType(foreignTable.codec)}-filter`);
|
|
45
|
+
},
|
|
46
|
+
filterBackwardSingleRelationExistsFieldName(_preset, relationFieldName) {
|
|
47
|
+
return `${relationFieldName}Exists`;
|
|
48
|
+
},
|
|
49
|
+
filterBackwardManyRelationExistsFieldName(_preset, relationFieldName) {
|
|
50
|
+
return `${relationFieldName}Exist`;
|
|
51
|
+
},
|
|
52
|
+
filterSingleRelationByKeysBackwardsFieldName(_preset, fieldName) {
|
|
53
|
+
return fieldName;
|
|
54
|
+
},
|
|
55
|
+
filterManyRelationByKeysFieldName(_preset, fieldName) {
|
|
56
|
+
return fieldName;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
schema: {
|
|
61
|
+
behaviorRegistry: {
|
|
62
|
+
add: {
|
|
63
|
+
filterBy: {
|
|
64
|
+
description: 'Whether a relation should be available as a filter field',
|
|
65
|
+
entities: ['pgCodecRelation'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
entityBehavior: {
|
|
70
|
+
pgCodecRelation: 'filterBy',
|
|
71
|
+
},
|
|
72
|
+
hooks: {
|
|
73
|
+
init(_, build) {
|
|
74
|
+
// Runtime check: only proceed if relation filters are enabled
|
|
75
|
+
if (!build.options.connectionFilterRelations) {
|
|
76
|
+
return _;
|
|
77
|
+
}
|
|
78
|
+
const { inflection } = build;
|
|
79
|
+
// Register "many" filter types (e.g. ClientToManyOrderFilter)
|
|
80
|
+
// These contain `some`, `every`, `none` fields.
|
|
81
|
+
const requireIndex = build.options.connectionFilterRelationsRequireIndex !== false;
|
|
82
|
+
for (const source of Object.values(build.input.pgRegistry.pgResources)) {
|
|
83
|
+
if (source.parameters ||
|
|
84
|
+
!source.codec.attributes ||
|
|
85
|
+
source.isUnique) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const [_relationName, relation] of Object.entries(source.getRelations())) {
|
|
89
|
+
if (!relation.isReferencee)
|
|
90
|
+
continue;
|
|
91
|
+
if (relation.isUnique)
|
|
92
|
+
continue;
|
|
93
|
+
const foreignTable = relation.remoteResource;
|
|
94
|
+
if (typeof foreignTable.from === 'function')
|
|
95
|
+
continue;
|
|
96
|
+
if (!build.behavior.pgCodecRelationMatches(relation, 'filterBy')) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Skip unindexed relations when requireIndex is enabled.
|
|
100
|
+
// PgIndexBehaviorsPlugin sets relation.extensions.isIndexed = false
|
|
101
|
+
// on relations without supporting indexes on the FK columns.
|
|
102
|
+
if (requireIndex && relation.extensions?.isIndexed === false) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const filterManyTypeName = inflection.filterManyType(source.codec, foreignTable);
|
|
106
|
+
const foreignTableTypeName = inflection.tableType(foreignTable.codec);
|
|
107
|
+
if (!build.getTypeMetaByName(filterManyTypeName)) {
|
|
108
|
+
build.recoverable(null, () => {
|
|
109
|
+
build.registerInputObjectType(filterManyTypeName, {
|
|
110
|
+
foreignTable,
|
|
111
|
+
isPgConnectionFilterMany: true,
|
|
112
|
+
}, () => ({
|
|
113
|
+
name: filterManyTypeName,
|
|
114
|
+
description: `A filter to be used against many \`${foreignTableTypeName}\` object types. All fields are combined with a logical \u2018and.\u2019`,
|
|
115
|
+
}), `ConnectionFilterBackwardRelationsPlugin: Adding '${filterManyTypeName}' type for ${foreignTable.name}`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return _;
|
|
121
|
+
},
|
|
122
|
+
GraphQLInputObjectType_fields(inFields, build, context) {
|
|
123
|
+
let fields = inFields;
|
|
124
|
+
// Runtime check: only proceed if relation filters are enabled
|
|
125
|
+
if (!build.options.connectionFilterRelations) {
|
|
126
|
+
return fields;
|
|
127
|
+
}
|
|
128
|
+
const { extend, inflection, sql, graphql: { GraphQLBoolean }, EXPORTABLE, } = build;
|
|
129
|
+
const { fieldWithHooks, scope: { pgCodec, isPgConnectionFilter, foreignTable: scopeForeignTable, isPgConnectionFilterMany, }, Self, } = context;
|
|
130
|
+
const assertAllowed = (0, utils_1.makeAssertAllowed)(build);
|
|
131
|
+
// ─── Part 1: Add backward relation fields to table filter types ───
|
|
132
|
+
const source = pgCodec &&
|
|
133
|
+
Object.values(build.input.pgRegistry.pgResources).find((s) => s.codec === pgCodec && !s.parameters);
|
|
134
|
+
if (isPgConnectionFilter && pgCodec && pgCodec.attributes && source) {
|
|
135
|
+
const requireIndex = build.options.connectionFilterRelationsRequireIndex !== false;
|
|
136
|
+
const backwardRelations = Object.entries(source.getRelations()).filter(([_relationName, relation]) => relation.isReferencee);
|
|
137
|
+
for (const [relationName, relation] of backwardRelations) {
|
|
138
|
+
const foreignTable = relation.remoteResource;
|
|
139
|
+
if (!build.behavior.pgCodecRelationMatches(relation, 'filterBy')) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (typeof foreignTable.from === 'function') {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Skip unindexed relations when requireIndex is enabled.
|
|
146
|
+
// PgIndexBehaviorsPlugin sets relation.extensions.isIndexed = false
|
|
147
|
+
// on backward relations without supporting indexes on the FK columns.
|
|
148
|
+
if (requireIndex && relation.extensions?.isIndexed === false) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const isForeignKeyUnique = relation.isUnique;
|
|
152
|
+
const foreignTableExpression = foreignTable.from;
|
|
153
|
+
const localAttributes = relation.localAttributes;
|
|
154
|
+
const remoteAttributes = relation.remoteAttributes;
|
|
155
|
+
if (isForeignKeyUnique) {
|
|
156
|
+
// One-to-one: add a single relation filter field
|
|
157
|
+
const fieldName = inflection.singleRelationBackwards({
|
|
158
|
+
registry: source.registry,
|
|
159
|
+
codec: source.codec,
|
|
160
|
+
relationName,
|
|
161
|
+
});
|
|
162
|
+
const filterFieldName = inflection.filterSingleRelationByKeysBackwardsFieldName(fieldName);
|
|
163
|
+
const foreignTableTypeName = inflection.tableType(foreignTable.codec);
|
|
164
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
165
|
+
const ForeignTableFilterType = build.getTypeByName(foreignTableFilterTypeName);
|
|
166
|
+
if (!ForeignTableFilterType)
|
|
167
|
+
continue;
|
|
168
|
+
fields = extend(fields, {
|
|
169
|
+
[filterFieldName]: fieldWithHooks({
|
|
170
|
+
fieldName: filterFieldName,
|
|
171
|
+
isPgConnectionFilterField: true,
|
|
172
|
+
}, () => ({
|
|
173
|
+
description: `Filter by the object\u2019s \`${fieldName}\` relation.`,
|
|
174
|
+
type: ForeignTableFilterType,
|
|
175
|
+
apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql) => function ($where, value) {
|
|
176
|
+
assertAllowed(value, 'object');
|
|
177
|
+
if (value == null)
|
|
178
|
+
return;
|
|
179
|
+
const $subQuery = $where.existsPlan({
|
|
180
|
+
tableExpression: foreignTableExpression,
|
|
181
|
+
alias: foreignTable.name,
|
|
182
|
+
});
|
|
183
|
+
localAttributes.forEach((localAttribute, i) => {
|
|
184
|
+
const remoteAttribute = remoteAttributes[i];
|
|
185
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`);
|
|
186
|
+
});
|
|
187
|
+
return $subQuery;
|
|
188
|
+
}, [
|
|
189
|
+
assertAllowed,
|
|
190
|
+
foreignTable,
|
|
191
|
+
foreignTableExpression,
|
|
192
|
+
localAttributes,
|
|
193
|
+
remoteAttributes,
|
|
194
|
+
sql,
|
|
195
|
+
]),
|
|
196
|
+
})),
|
|
197
|
+
}, `Adding connection filter backward single relation field from ${source.name} to ${foreignTable.name}`);
|
|
198
|
+
// Add exists field
|
|
199
|
+
const existsFieldName = inflection.filterBackwardSingleRelationExistsFieldName(fieldName);
|
|
200
|
+
fields = extend(fields, {
|
|
201
|
+
[existsFieldName]: fieldWithHooks({
|
|
202
|
+
fieldName: existsFieldName,
|
|
203
|
+
isPgConnectionFilterField: true,
|
|
204
|
+
}, () => ({
|
|
205
|
+
description: `A related \`${fieldName}\` exists.`,
|
|
206
|
+
type: GraphQLBoolean,
|
|
207
|
+
apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql) => function ($where, value) {
|
|
208
|
+
assertAllowed(value, 'scalar');
|
|
209
|
+
if (value == null)
|
|
210
|
+
return;
|
|
211
|
+
const $subQuery = $where.existsPlan({
|
|
212
|
+
tableExpression: foreignTableExpression,
|
|
213
|
+
alias: foreignTable.name,
|
|
214
|
+
equals: value,
|
|
215
|
+
});
|
|
216
|
+
localAttributes.forEach((localAttribute, i) => {
|
|
217
|
+
const remoteAttribute = remoteAttributes[i];
|
|
218
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`);
|
|
219
|
+
});
|
|
220
|
+
}, [
|
|
221
|
+
assertAllowed,
|
|
222
|
+
foreignTable,
|
|
223
|
+
foreignTableExpression,
|
|
224
|
+
localAttributes,
|
|
225
|
+
remoteAttributes,
|
|
226
|
+
sql,
|
|
227
|
+
]),
|
|
228
|
+
})),
|
|
229
|
+
}, `Adding connection filter backward single relation exists field for ${fieldName}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// One-to-many: add a "many" filter field (e.g. ordersByClientId: ClientToManyOrderFilter)
|
|
233
|
+
const fieldName = inflection._manyRelation({
|
|
234
|
+
registry: source.registry,
|
|
235
|
+
codec: source.codec,
|
|
236
|
+
relationName,
|
|
237
|
+
});
|
|
238
|
+
const filterFieldName = inflection.filterManyRelationByKeysFieldName(fieldName);
|
|
239
|
+
const filterManyTypeName = inflection.filterManyType(source.codec, foreignTable);
|
|
240
|
+
const FilterManyType = build.getTypeByName(filterManyTypeName);
|
|
241
|
+
if (!FilterManyType)
|
|
242
|
+
continue;
|
|
243
|
+
// The many relation field tags $where with relation info so that
|
|
244
|
+
// some/every/none can create the appropriate EXISTS subquery.
|
|
245
|
+
fields = extend(fields, {
|
|
246
|
+
[filterFieldName]: fieldWithHooks({
|
|
247
|
+
fieldName: filterFieldName,
|
|
248
|
+
isPgConnectionFilterField: true,
|
|
249
|
+
isPgConnectionFilterManyField: true,
|
|
250
|
+
}, () => ({
|
|
251
|
+
description: `Filter by the object\u2019s \`${fieldName}\` relation.`,
|
|
252
|
+
type: FilterManyType,
|
|
253
|
+
apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes) => function ($where, value) {
|
|
254
|
+
assertAllowed(value, 'object');
|
|
255
|
+
if (value == null)
|
|
256
|
+
return;
|
|
257
|
+
// Tag $where with relation info for some/every/none
|
|
258
|
+
$where._manyRelation = {
|
|
259
|
+
foreignTable,
|
|
260
|
+
foreignTableExpression,
|
|
261
|
+
localAttributes,
|
|
262
|
+
remoteAttributes,
|
|
263
|
+
};
|
|
264
|
+
return $where;
|
|
265
|
+
}, [
|
|
266
|
+
assertAllowed,
|
|
267
|
+
foreignTable,
|
|
268
|
+
foreignTableExpression,
|
|
269
|
+
localAttributes,
|
|
270
|
+
remoteAttributes,
|
|
271
|
+
]),
|
|
272
|
+
})),
|
|
273
|
+
}, `Adding connection filter backward many relation field from ${source.name} to ${foreignTable.name}`);
|
|
274
|
+
// Add exists field for many relations
|
|
275
|
+
const existsFieldName = inflection.filterBackwardManyRelationExistsFieldName(fieldName);
|
|
276
|
+
fields = extend(fields, {
|
|
277
|
+
[existsFieldName]: fieldWithHooks({
|
|
278
|
+
fieldName: existsFieldName,
|
|
279
|
+
isPgConnectionFilterField: true,
|
|
280
|
+
}, () => ({
|
|
281
|
+
description: `\`${fieldName}\` exist.`,
|
|
282
|
+
type: GraphQLBoolean,
|
|
283
|
+
apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql) => function ($where, value) {
|
|
284
|
+
assertAllowed(value, 'scalar');
|
|
285
|
+
if (value == null)
|
|
286
|
+
return;
|
|
287
|
+
const $subQuery = $where.existsPlan({
|
|
288
|
+
tableExpression: foreignTableExpression,
|
|
289
|
+
alias: foreignTable.name,
|
|
290
|
+
equals: value,
|
|
291
|
+
});
|
|
292
|
+
localAttributes.forEach((localAttribute, i) => {
|
|
293
|
+
const remoteAttribute = remoteAttributes[i];
|
|
294
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`);
|
|
295
|
+
});
|
|
296
|
+
}, [
|
|
297
|
+
assertAllowed,
|
|
298
|
+
foreignTable,
|
|
299
|
+
foreignTableExpression,
|
|
300
|
+
localAttributes,
|
|
301
|
+
remoteAttributes,
|
|
302
|
+
sql,
|
|
303
|
+
]),
|
|
304
|
+
})),
|
|
305
|
+
}, `Adding connection filter backward many relation exists field for ${fieldName}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// ─── Part 2: Add some/every/none fields to "many" filter types ───
|
|
310
|
+
if (isPgConnectionFilterMany && scopeForeignTable) {
|
|
311
|
+
const foreignTableTypeName = inflection.tableType(scopeForeignTable.codec);
|
|
312
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
313
|
+
const ForeignTableFilterType = build.getTypeByName(foreignTableFilterTypeName);
|
|
314
|
+
if (!ForeignTableFilterType)
|
|
315
|
+
return fields;
|
|
316
|
+
fields = extend(fields, {
|
|
317
|
+
// `some`: EXISTS (... WHERE join AND filter)
|
|
318
|
+
some: fieldWithHooks({
|
|
319
|
+
fieldName: 'some',
|
|
320
|
+
isPgConnectionFilterField: true,
|
|
321
|
+
}, () => ({
|
|
322
|
+
description: 'Filters to entities where at least one related entity matches.',
|
|
323
|
+
type: ForeignTableFilterType,
|
|
324
|
+
apply: EXPORTABLE((assertAllowed, sql) => function ($where, value) {
|
|
325
|
+
assertAllowed(value, 'object');
|
|
326
|
+
if (value == null)
|
|
327
|
+
return;
|
|
328
|
+
const rel = $where._manyRelation;
|
|
329
|
+
if (!rel)
|
|
330
|
+
return;
|
|
331
|
+
const $subQuery = $where.existsPlan({
|
|
332
|
+
tableExpression: rel.foreignTableExpression,
|
|
333
|
+
alias: rel.foreignTable.name,
|
|
334
|
+
});
|
|
335
|
+
rel.localAttributes.forEach((la, i) => {
|
|
336
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(la)} = ${$subQuery.alias}.${sql.identifier(rel.remoteAttributes[i])}`);
|
|
337
|
+
});
|
|
338
|
+
return $subQuery;
|
|
339
|
+
}, [assertAllowed, sql]),
|
|
340
|
+
})),
|
|
341
|
+
// `every`: NOT EXISTS (... WHERE join AND NOT(filter))
|
|
342
|
+
every: fieldWithHooks({
|
|
343
|
+
fieldName: 'every',
|
|
344
|
+
isPgConnectionFilterField: true,
|
|
345
|
+
}, () => ({
|
|
346
|
+
description: 'Filters to entities where every related entity matches.',
|
|
347
|
+
type: ForeignTableFilterType,
|
|
348
|
+
apply: EXPORTABLE((assertAllowed, sql) => function ($where, value) {
|
|
349
|
+
assertAllowed(value, 'object');
|
|
350
|
+
if (value == null)
|
|
351
|
+
return;
|
|
352
|
+
const rel = $where._manyRelation;
|
|
353
|
+
if (!rel)
|
|
354
|
+
return;
|
|
355
|
+
// NOT EXISTS (... WHERE join AND NOT(filter))
|
|
356
|
+
const $subQuery = $where.existsPlan({
|
|
357
|
+
tableExpression: rel.foreignTableExpression,
|
|
358
|
+
alias: rel.foreignTable.name,
|
|
359
|
+
equals: false,
|
|
360
|
+
});
|
|
361
|
+
rel.localAttributes.forEach((la, i) => {
|
|
362
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(la)} = ${$subQuery.alias}.${sql.identifier(rel.remoteAttributes[i])}`);
|
|
363
|
+
});
|
|
364
|
+
// Negate the inner filter conditions
|
|
365
|
+
const $not = $subQuery.notPlan();
|
|
366
|
+
return $not;
|
|
367
|
+
}, [assertAllowed, sql]),
|
|
368
|
+
})),
|
|
369
|
+
// `none`: NOT EXISTS (... WHERE join AND filter)
|
|
370
|
+
none: fieldWithHooks({
|
|
371
|
+
fieldName: 'none',
|
|
372
|
+
isPgConnectionFilterField: true,
|
|
373
|
+
}, () => ({
|
|
374
|
+
description: 'Filters to entities where no related entity matches.',
|
|
375
|
+
type: ForeignTableFilterType,
|
|
376
|
+
apply: EXPORTABLE((assertAllowed, sql) => function ($where, value) {
|
|
377
|
+
assertAllowed(value, 'object');
|
|
378
|
+
if (value == null)
|
|
379
|
+
return;
|
|
380
|
+
const rel = $where._manyRelation;
|
|
381
|
+
if (!rel)
|
|
382
|
+
return;
|
|
383
|
+
// NOT EXISTS (... WHERE join AND filter)
|
|
384
|
+
const $subQuery = $where.existsPlan({
|
|
385
|
+
tableExpression: rel.foreignTableExpression,
|
|
386
|
+
alias: rel.foreignTable.name,
|
|
387
|
+
equals: false,
|
|
388
|
+
});
|
|
389
|
+
rel.localAttributes.forEach((la, i) => {
|
|
390
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(la)} = ${$subQuery.alias}.${sql.identifier(rel.remoteAttributes[i])}`);
|
|
391
|
+
});
|
|
392
|
+
return $subQuery;
|
|
393
|
+
}, [assertAllowed, sql]),
|
|
394
|
+
})),
|
|
395
|
+
}, 'Adding some/every/none fields to many filter type');
|
|
396
|
+
}
|
|
397
|
+
return fields;
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
/**
|
|
4
|
+
* ConnectionFilterComputedAttributesPlugin
|
|
5
|
+
*
|
|
6
|
+
* Adds filter fields for computed columns (PostgreSQL functions that take
|
|
7
|
+
* a table row as their first argument and return a scalar).
|
|
8
|
+
*
|
|
9
|
+
* For example, given:
|
|
10
|
+
* CREATE FUNCTION person_full_name(person) RETURNS text AS $$ ... $$;
|
|
11
|
+
*
|
|
12
|
+
* This plugin adds a `fullName` filter field to `PersonFilter`, typed as `StringFilter`,
|
|
13
|
+
* allowing queries like:
|
|
14
|
+
* { people(filter: { fullName: { startsWith: "John" } }) { ... } }
|
|
15
|
+
*
|
|
16
|
+
* Controlled by the `connectionFilterComputedColumns` schema option (default: true).
|
|
17
|
+
* Requires the `filterBy` behavior on the pgResource to be enabled.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ConnectionFilterComputedAttributesPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionFilterComputedAttributesPlugin = void 0;
|
|
4
|
+
require("../augmentations");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const version = '1.0.0';
|
|
7
|
+
/**
|
|
8
|
+
* ConnectionFilterComputedAttributesPlugin
|
|
9
|
+
*
|
|
10
|
+
* Adds filter fields for computed columns (PostgreSQL functions that take
|
|
11
|
+
* a table row as their first argument and return a scalar).
|
|
12
|
+
*
|
|
13
|
+
* For example, given:
|
|
14
|
+
* CREATE FUNCTION person_full_name(person) RETURNS text AS $$ ... $$;
|
|
15
|
+
*
|
|
16
|
+
* This plugin adds a `fullName` filter field to `PersonFilter`, typed as `StringFilter`,
|
|
17
|
+
* allowing queries like:
|
|
18
|
+
* { people(filter: { fullName: { startsWith: "John" } }) { ... } }
|
|
19
|
+
*
|
|
20
|
+
* Controlled by the `connectionFilterComputedColumns` schema option (default: true).
|
|
21
|
+
* Requires the `filterBy` behavior on the pgResource to be enabled.
|
|
22
|
+
*/
|
|
23
|
+
exports.ConnectionFilterComputedAttributesPlugin = {
|
|
24
|
+
name: 'ConnectionFilterComputedAttributesPlugin',
|
|
25
|
+
version,
|
|
26
|
+
description: 'Adds filter fields for computed column functions (scalar-returning functions that take a table row)',
|
|
27
|
+
schema: {
|
|
28
|
+
behaviorRegistry: {
|
|
29
|
+
add: {
|
|
30
|
+
filterBy: {
|
|
31
|
+
description: 'Whether a computed attribute resource should be available as a filter field',
|
|
32
|
+
entities: ['pgResource'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
entityBehavior: {
|
|
37
|
+
pgResource: {
|
|
38
|
+
inferred(behavior, entity, build) {
|
|
39
|
+
if (build.options.connectionFilterComputedColumns &&
|
|
40
|
+
(0, utils_1.isComputedScalarAttributeResource)(entity)) {
|
|
41
|
+
return [behavior, 'filterBy'];
|
|
42
|
+
}
|
|
43
|
+
return behavior;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
hooks: {
|
|
48
|
+
GraphQLInputObjectType_fields(inFields, build, context) {
|
|
49
|
+
let fields = inFields;
|
|
50
|
+
const { inflection, connectionFilterOperatorsDigest, dataplanPg: { TYPES, PgCondition }, EXPORTABLE, } = build;
|
|
51
|
+
const { fieldWithHooks, scope: { pgCodec: codec, isPgConnectionFilter }, } = context;
|
|
52
|
+
// Only apply to table-level filter types (e.g. UserFilter)
|
|
53
|
+
if (!isPgConnectionFilter ||
|
|
54
|
+
!codec ||
|
|
55
|
+
!codec.attributes ||
|
|
56
|
+
codec.isAnonymous) {
|
|
57
|
+
return fields;
|
|
58
|
+
}
|
|
59
|
+
// Find the source resource for this codec
|
|
60
|
+
const source = Object.values(build.input.pgRegistry.pgResources).find((s) => s.codec === codec && !s.parameters && !s.isUnique);
|
|
61
|
+
if (!source) {
|
|
62
|
+
return fields;
|
|
63
|
+
}
|
|
64
|
+
const computedAttributeResources = (0, utils_1.getComputedAttributeResources)(build, source);
|
|
65
|
+
for (const computedAttributeResource of computedAttributeResources) {
|
|
66
|
+
// Must return a scalar or an array (not a composite)
|
|
67
|
+
if (!computedAttributeResource.isUnique) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (computedAttributeResource.codec.attributes) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (computedAttributeResource.codec === TYPES.void) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Get the operator type for this codec
|
|
77
|
+
const digest = connectionFilterOperatorsDigest(computedAttributeResource.codec);
|
|
78
|
+
if (!digest) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const OperatorsType = build.getTypeByName(digest.operatorsTypeName);
|
|
82
|
+
if (!OperatorsType) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Check behavior
|
|
86
|
+
if (!build.behavior.pgResourceMatches(computedAttributeResource, 'filterBy')) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Must have no required arguments beyond the first (the table row)
|
|
90
|
+
const { argDetails } = build.pgGetArgDetailsFromParameters(computedAttributeResource, computedAttributeResource.parameters.slice(1));
|
|
91
|
+
if (argDetails.some((a) => a.required)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Derive the field name from the computed attribute function
|
|
95
|
+
const fieldName = inflection.computedAttributeField({
|
|
96
|
+
resource: computedAttributeResource,
|
|
97
|
+
});
|
|
98
|
+
const functionResultCodec = computedAttributeResource.codec;
|
|
99
|
+
fields = build.extend(fields, {
|
|
100
|
+
[fieldName]: fieldWithHooks({
|
|
101
|
+
fieldName,
|
|
102
|
+
isPgConnectionFilterField: true,
|
|
103
|
+
}, {
|
|
104
|
+
description: `Filter by the object\u2019s \`${fieldName}\` field.`,
|
|
105
|
+
type: OperatorsType,
|
|
106
|
+
apply: EXPORTABLE((PgCondition, computedAttributeResource, fieldName, functionResultCodec) => function ($where, value) {
|
|
107
|
+
if (typeof computedAttributeResource.from !==
|
|
108
|
+
'function') {
|
|
109
|
+
throw new Error(`Unexpected...`);
|
|
110
|
+
}
|
|
111
|
+
if (value == null)
|
|
112
|
+
return;
|
|
113
|
+
const expression = computedAttributeResource.from({
|
|
114
|
+
placeholder: $where.alias,
|
|
115
|
+
});
|
|
116
|
+
const $col = new PgCondition($where);
|
|
117
|
+
$col.extensions.pgFilterAttribute = {
|
|
118
|
+
fieldName,
|
|
119
|
+
codec: functionResultCodec,
|
|
120
|
+
expression,
|
|
121
|
+
};
|
|
122
|
+
return $col;
|
|
123
|
+
}, [
|
|
124
|
+
PgCondition,
|
|
125
|
+
computedAttributeResource,
|
|
126
|
+
fieldName,
|
|
127
|
+
functionResultCodec,
|
|
128
|
+
]),
|
|
129
|
+
}),
|
|
130
|
+
}, '');
|
|
131
|
+
}
|
|
132
|
+
return fields;
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
/**
|
|
4
|
+
* ConnectionFilterCustomOperatorsPlugin
|
|
5
|
+
*
|
|
6
|
+
* Processes declarative operator factories from the preset configuration.
|
|
7
|
+
* Satellite plugins (PostGIS filter, search, pg_trgm, etc.) declare their
|
|
8
|
+
* operators via `connectionFilterOperatorFactories` in their preset's schema
|
|
9
|
+
* options. This plugin processes all factories during its own init hook,
|
|
10
|
+
* populating the filter registry used at schema build time.
|
|
11
|
+
*
|
|
12
|
+
* This declarative approach replaces the previous imperative
|
|
13
|
+
* `build.addConnectionFilterOperator()` API, eliminating timing/ordering
|
|
14
|
+
* dependencies between plugins.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // In a satellite plugin's preset:
|
|
19
|
+
* const MyPreset = {
|
|
20
|
+
* schema: {
|
|
21
|
+
* connectionFilterOperatorFactories: [
|
|
22
|
+
* (build) => [{
|
|
23
|
+
* typeNames: 'String',
|
|
24
|
+
* operatorName: 'myOp',
|
|
25
|
+
* spec: {
|
|
26
|
+
* description: 'My operator',
|
|
27
|
+
* resolve: (i, v) => build.sql`${i} OP ${v}`,
|
|
28
|
+
* },
|
|
29
|
+
* }],
|
|
30
|
+
* ],
|
|
31
|
+
* },
|
|
32
|
+
* };
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const ConnectionFilterCustomOperatorsPlugin: GraphileConfig.Plugin;
|