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.
Files changed (71) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +107 -0
  3. package/augmentations.d.ts +104 -0
  4. package/augmentations.js +11 -0
  5. package/esm/augmentations.d.ts +104 -0
  6. package/esm/augmentations.js +9 -0
  7. package/esm/index.d.ts +55 -0
  8. package/esm/index.js +56 -0
  9. package/esm/plugins/ConnectionFilterArgPlugin.d.ts +13 -0
  10. package/esm/plugins/ConnectionFilterArgPlugin.js +96 -0
  11. package/esm/plugins/ConnectionFilterAttributesPlugin.d.ts +14 -0
  12. package/esm/plugins/ConnectionFilterAttributesPlugin.js +79 -0
  13. package/esm/plugins/ConnectionFilterBackwardRelationsPlugin.d.ts +33 -0
  14. package/esm/plugins/ConnectionFilterBackwardRelationsPlugin.js +398 -0
  15. package/esm/plugins/ConnectionFilterComputedAttributesPlugin.d.ts +19 -0
  16. package/esm/plugins/ConnectionFilterComputedAttributesPlugin.js +133 -0
  17. package/esm/plugins/ConnectionFilterCustomOperatorsPlugin.d.ts +35 -0
  18. package/esm/plugins/ConnectionFilterCustomOperatorsPlugin.js +129 -0
  19. package/esm/plugins/ConnectionFilterForwardRelationsPlugin.d.ts +28 -0
  20. package/esm/plugins/ConnectionFilterForwardRelationsPlugin.js +168 -0
  21. package/esm/plugins/ConnectionFilterInflectionPlugin.d.ts +11 -0
  22. package/esm/plugins/ConnectionFilterInflectionPlugin.js +27 -0
  23. package/esm/plugins/ConnectionFilterLogicalOperatorsPlugin.d.ts +15 -0
  24. package/esm/plugins/ConnectionFilterLogicalOperatorsPlugin.js +86 -0
  25. package/esm/plugins/ConnectionFilterOperatorsPlugin.d.ts +21 -0
  26. package/esm/plugins/ConnectionFilterOperatorsPlugin.js +677 -0
  27. package/esm/plugins/ConnectionFilterTypesPlugin.d.ts +12 -0
  28. package/esm/plugins/ConnectionFilterTypesPlugin.js +225 -0
  29. package/esm/plugins/index.d.ts +11 -0
  30. package/esm/plugins/index.js +11 -0
  31. package/esm/plugins/operatorApply.d.ts +11 -0
  32. package/esm/plugins/operatorApply.js +70 -0
  33. package/esm/preset.d.ts +35 -0
  34. package/esm/preset.js +72 -0
  35. package/esm/types.d.ts +146 -0
  36. package/esm/types.js +4 -0
  37. package/esm/utils.d.ts +44 -0
  38. package/esm/utils.js +112 -0
  39. package/index.d.ts +55 -0
  40. package/index.js +77 -0
  41. package/package.json +58 -0
  42. package/plugins/ConnectionFilterArgPlugin.d.ts +13 -0
  43. package/plugins/ConnectionFilterArgPlugin.js +99 -0
  44. package/plugins/ConnectionFilterAttributesPlugin.d.ts +14 -0
  45. package/plugins/ConnectionFilterAttributesPlugin.js +82 -0
  46. package/plugins/ConnectionFilterBackwardRelationsPlugin.d.ts +33 -0
  47. package/plugins/ConnectionFilterBackwardRelationsPlugin.js +401 -0
  48. package/plugins/ConnectionFilterComputedAttributesPlugin.d.ts +19 -0
  49. package/plugins/ConnectionFilterComputedAttributesPlugin.js +136 -0
  50. package/plugins/ConnectionFilterCustomOperatorsPlugin.d.ts +35 -0
  51. package/plugins/ConnectionFilterCustomOperatorsPlugin.js +132 -0
  52. package/plugins/ConnectionFilterForwardRelationsPlugin.d.ts +28 -0
  53. package/plugins/ConnectionFilterForwardRelationsPlugin.js +171 -0
  54. package/plugins/ConnectionFilterInflectionPlugin.d.ts +11 -0
  55. package/plugins/ConnectionFilterInflectionPlugin.js +30 -0
  56. package/plugins/ConnectionFilterLogicalOperatorsPlugin.d.ts +15 -0
  57. package/plugins/ConnectionFilterLogicalOperatorsPlugin.js +89 -0
  58. package/plugins/ConnectionFilterOperatorsPlugin.d.ts +21 -0
  59. package/plugins/ConnectionFilterOperatorsPlugin.js +680 -0
  60. package/plugins/ConnectionFilterTypesPlugin.d.ts +12 -0
  61. package/plugins/ConnectionFilterTypesPlugin.js +228 -0
  62. package/plugins/index.d.ts +11 -0
  63. package/plugins/index.js +25 -0
  64. package/plugins/operatorApply.d.ts +11 -0
  65. package/plugins/operatorApply.js +73 -0
  66. package/preset.d.ts +35 -0
  67. package/preset.js +75 -0
  68. package/types.d.ts +146 -0
  69. package/types.js +7 -0
  70. package/utils.d.ts +44 -0
  71. 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;