graphile-settings 5.1.1 → 5.2.1

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.
@@ -66,10 +66,85 @@ function arraysMatch(array1, array2, comparator = (v1, v2) => v1 === v2) {
66
66
  }
67
67
  return true;
68
68
  }
69
+ /**
70
+ * Handle @listSuffix smart tag for per-table control of Connection/List suffix.
71
+ * When @listSuffix is "omit", connections get a "Connection" suffix and lists are bare.
72
+ * When @listSuffix is "include" (or absent), default behavior applies.
73
+ */
74
+ let globalPgOmitListSuffix = null;
75
+ function overrideListSuffix(listSuffix, cb) {
76
+ if (listSuffix == null) {
77
+ return cb();
78
+ }
79
+ if (listSuffix !== 'include' && listSuffix !== 'omit') {
80
+ throw new Error(`Unrecognized @listSuffix value "${String(listSuffix)}". Must be "omit" or "include".`);
81
+ }
82
+ const old = globalPgOmitListSuffix;
83
+ try {
84
+ globalPgOmitListSuffix = listSuffix === 'omit';
85
+ return cb();
86
+ }
87
+ finally {
88
+ globalPgOmitListSuffix = old;
89
+ }
90
+ }
69
91
  export const InflektPlugin = {
70
92
  name: 'InflektPlugin',
71
93
  version: '1.0.0',
72
94
  inflection: {
95
+ add: {
96
+ /**
97
+ * Pluralize ensuring the result differs from the singular.
98
+ * Registered as an inflection method so other plugins can call this.distinctPluralize().
99
+ */
100
+ distinctPluralize(_preset, str) {
101
+ return distinctPluralize(str);
102
+ },
103
+ /**
104
+ * Extract base name from FK attribute names (e.g. "author_id" -> "author").
105
+ * Registered so other plugins can call this._getBaseName().
106
+ */
107
+ _getBaseName(_preset, attributeName) {
108
+ return getBaseName(attributeName);
109
+ },
110
+ /**
111
+ * Check if a base name matches another name when singularized.
112
+ * Registered so other plugins can call this._baseNameMatches().
113
+ */
114
+ _baseNameMatches(_preset, baseName, otherName) {
115
+ return baseNameMatches(baseName, otherName);
116
+ },
117
+ /**
118
+ * Get the opposite name for a relation base name (e.g. "parent" -> "child").
119
+ * Registered so other plugins can call this._getOppositeBaseName().
120
+ */
121
+ _getOppositeBaseName(_preset, baseName) {
122
+ return getOppositeBaseName(baseName);
123
+ },
124
+ /**
125
+ * Extract base name from composite FK keys.
126
+ * Handles single-key (delegates to getBaseName) and multi-key
127
+ * (joins base names with hyphens) FK relations.
128
+ */
129
+ _getBaseNameFromKeys(preset, detailedKeys) {
130
+ if (detailedKeys.length === 1) {
131
+ const key = detailedKeys[0];
132
+ const attributeName = this._attributeName({
133
+ ...key,
134
+ skipRowId: true,
135
+ });
136
+ return getBaseName(attributeName);
137
+ }
138
+ if (preset.schema?.pgSimplifyMultikeyRelations) {
139
+ const attributeNames = detailedKeys.map((key) => this._attributeName({ ...key, skipRowId: true }));
140
+ const baseNames = attributeNames.map((attr) => getBaseName(attr));
141
+ if (baseNames.every((n) => n)) {
142
+ return baseNames.join('-');
143
+ }
144
+ }
145
+ return null;
146
+ },
147
+ },
73
148
  replace: {
74
149
  /**
75
150
  * Remove schema prefixes from all schemas.
@@ -168,22 +243,97 @@ export const InflektPlugin = {
168
243
  singularize(_previous, _preset, str) {
169
244
  return singularizeLast(str);
170
245
  },
246
+ /**
247
+ * Connection field suffix — respects @listSuffix smart tag.
248
+ * When pgOmitListSuffix is true, connections get "Connection" suffix.
249
+ */
250
+ connectionField(_prev, options, baseName) {
251
+ return (globalPgOmitListSuffix ?? options.schema?.pgOmitListSuffix)
252
+ ? baseName + 'Connection'
253
+ : baseName;
254
+ },
255
+ /**
256
+ * List field suffix — respects @listSuffix smart tag.
257
+ * When pgOmitListSuffix is true, lists are bare (no suffix).
258
+ */
259
+ listField(_prev, options, baseName) {
260
+ return (globalPgOmitListSuffix ?? options.schema?.pgOmitListSuffix)
261
+ ? baseName
262
+ : baseName + 'List';
263
+ },
171
264
  /**
172
265
  * Simplify root query connection fields (allUsers -> users)
266
+ * Supports @listSuffix smart tag per-table.
173
267
  */
174
268
  allRowsConnection(_previous, _options, resource) {
175
- const resourceName = this._singularizedResourceName(resource);
176
- return toCamelCase(distinctPluralize(resourceName));
269
+ const listSuffix = resource.extensions?.tags?.listSuffix;
270
+ return overrideListSuffix(listSuffix, () => {
271
+ const resourceName = this._singularizedResourceName(resource);
272
+ return this.connectionField(toCamelCase(distinctPluralize(resourceName)));
273
+ });
177
274
  },
178
275
  /**
179
- * Simplify root query list fields
276
+ * Simplify root query list fields.
277
+ * Supports @listSuffix smart tag per-table.
180
278
  */
181
279
  allRowsList(_previous, _options, resource) {
182
- const resourceName = this._singularizedResourceName(resource);
183
- return toCamelCase(distinctPluralize(resourceName)) + 'List';
280
+ const listSuffix = resource.extensions?.tags?.listSuffix;
281
+ return overrideListSuffix(listSuffix, () => {
282
+ const resourceName = this._singularizedResourceName(resource);
283
+ return this.listField(toCamelCase(distinctPluralize(resourceName)));
284
+ });
285
+ },
286
+ /**
287
+ * @listSuffix passthrough for many-relation connection fields
288
+ */
289
+ manyRelationConnection(previous, _options, details) {
290
+ const { registry, codec, relationName } = details;
291
+ const relation = registry.pgRelations[codec.name]?.[relationName];
292
+ const listSuffix = (relation?.extensions?.tags?.listSuffix ??
293
+ relation?.remoteResource?.extensions?.tags?.listSuffix);
294
+ return overrideListSuffix(listSuffix, () => previous(details));
295
+ },
296
+ /**
297
+ * @listSuffix passthrough for many-relation list fields
298
+ */
299
+ manyRelationList(previous, _options, details) {
300
+ const { registry, codec, relationName } = details;
301
+ const relation = registry.pgRelations[codec.name]?.[relationName];
302
+ const listSuffix = (relation?.extensions?.tags?.listSuffix ??
303
+ relation?.remoteResource?.extensions?.tags?.listSuffix);
304
+ return overrideListSuffix(listSuffix, () => previous(details));
305
+ },
306
+ /**
307
+ * @listSuffix passthrough for custom query connection fields
308
+ */
309
+ customQueryConnectionField(previous, _options, details) {
310
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
311
+ return overrideListSuffix(listSuffix, () => previous(details));
312
+ },
313
+ /**
314
+ * @listSuffix passthrough for custom query list fields
315
+ */
316
+ customQueryListField(previous, _options, details) {
317
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
318
+ return overrideListSuffix(listSuffix, () => previous(details));
319
+ },
320
+ /**
321
+ * @listSuffix passthrough for computed attribute connection fields
322
+ */
323
+ computedAttributeConnectionField(previous, _options, details) {
324
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
325
+ return overrideListSuffix(listSuffix, () => previous(details));
326
+ },
327
+ /**
328
+ * @listSuffix passthrough for computed attribute list fields
329
+ */
330
+ computedAttributeListField(previous, _options, details) {
331
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
332
+ return overrideListSuffix(listSuffix, () => previous(details));
184
333
  },
185
334
  /**
186
- * Simplify single relation field names (userByAuthorId -> author)
335
+ * Simplify single relation field names (userByAuthorId -> author).
336
+ * Uses _getBaseNameFromKeys for composite FK support.
187
337
  */
188
338
  singleRelation(previous, _options, details) {
189
339
  const { registry, codec, relationName } = details;
@@ -194,7 +344,10 @@ export const InflektPlugin = {
194
344
  if (typeof relation.extensions?.tags?.fieldName === 'string') {
195
345
  return relation.extensions.tags.fieldName;
196
346
  }
197
- // Try to extract base name from the local attribute
347
+ const detailedKeys = relation.localAttributes.map((attributeName) => ({
348
+ codec,
349
+ attributeName,
350
+ }));
198
351
  if (relation.localAttributes.length === 1) {
199
352
  const attributeName = relation.localAttributes[0];
200
353
  const baseName = getBaseName(attributeName);
@@ -211,7 +364,8 @@ export const InflektPlugin = {
211
364
  return previous(details);
212
365
  },
213
366
  /**
214
- * Simplify backwards single relation field names
367
+ * Simplify backwards single relation field names.
368
+ * Uses _getBaseNameFromKeys for composite FK support.
215
369
  */
216
370
  singleRelationBackwards(previous, _options, details) {
217
371
  const { registry, codec, relationName } = details;
@@ -225,7 +379,6 @@ export const InflektPlugin = {
225
379
  if (typeof relation.extensions?.tags?.foreignFieldName === 'string') {
226
380
  return relation.extensions.tags.foreignFieldName;
227
381
  }
228
- // Try to extract base name from the remote attribute
229
382
  if (relation.remoteAttributes.length === 1) {
230
383
  const attributeName = relation.remoteAttributes[0];
231
384
  const baseName = getBaseName(attributeName);
@@ -242,7 +395,8 @@ export const InflektPlugin = {
242
395
  return previous(details);
243
396
  },
244
397
  /**
245
- * Simplify many relation field names (postsByAuthorId -> posts)
398
+ * Simplify many relation field names (postsByAuthorId -> posts).
399
+ * Uses _getBaseNameFromKeys for composite FK support.
246
400
  */
247
401
  _manyRelation(previous, _options, details) {
248
402
  const { registry, codec, relationName } = details;
@@ -254,7 +408,6 @@ export const InflektPlugin = {
254
408
  if (typeof baseOverride === 'string') {
255
409
  return baseOverride;
256
410
  }
257
- // Try to extract base name from the remote attribute
258
411
  if (relation.remoteAttributes.length === 1) {
259
412
  const attributeName = relation.remoteAttributes[0];
260
413
  const baseName = getBaseName(attributeName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-settings",
3
- "version": "5.1.1",
3
+ "version": "5.2.1",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "graphile settings",
6
6
  "main": "index.js",
@@ -49,15 +49,15 @@
49
49
  "graphile-bucket-provisioner-plugin": "0.11.0",
50
50
  "graphile-build": "5.0.0",
51
51
  "graphile-build-pg": "5.0.0",
52
- "graphile-bulk-mutations": "^0.3.0",
52
+ "graphile-bulk-mutations": "^0.3.1",
53
53
  "graphile-config": "1.0.0",
54
- "graphile-connection-filter": "^1.10.0",
55
- "graphile-ltree": "^1.7.0",
56
- "graphile-pg-aggregates": "^1.3.0",
57
- "graphile-postgis": "^2.16.0",
54
+ "graphile-connection-filter": "^1.10.1",
55
+ "graphile-ltree": "^1.7.1",
56
+ "graphile-pg-aggregates": "^1.3.1",
57
+ "graphile-postgis": "^2.16.1",
58
58
  "graphile-presigned-url-plugin": "^0.19.0",
59
59
  "graphile-realtime-subscriptions": "^0.7.0",
60
- "graphile-search": "^1.12.0",
60
+ "graphile-search": "^1.12.1",
61
61
  "graphile-sql-expression-validator": "^2.13.0",
62
62
  "graphile-upload-plugin": "^2.11.0",
63
63
  "graphile-utils": "5.0.0",
@@ -76,7 +76,7 @@
76
76
  "@types/express": "^5.0.6",
77
77
  "@types/pg": "^8.18.0",
78
78
  "@types/request-ip": "^0.0.41",
79
- "graphile-test": "^4.15.0",
79
+ "graphile-test": "^4.15.1",
80
80
  "makage": "^0.3.0",
81
81
  "nodemon": "^3.1.14",
82
82
  "ts-node": "^10.9.2"
@@ -88,5 +88,5 @@
88
88
  "constructive",
89
89
  "graphql"
90
90
  ],
91
- "gitHead": "f368992ff2e66fb639e4a5efee163e311f838ce1"
91
+ "gitHead": "62282a9e2b4a72a68c6c3c6a8729e3a0a42a54b2"
92
92
  }
@@ -69,10 +69,85 @@ function arraysMatch(array1, array2, comparator = (v1, v2) => v1 === v2) {
69
69
  }
70
70
  return true;
71
71
  }
72
+ /**
73
+ * Handle @listSuffix smart tag for per-table control of Connection/List suffix.
74
+ * When @listSuffix is "omit", connections get a "Connection" suffix and lists are bare.
75
+ * When @listSuffix is "include" (or absent), default behavior applies.
76
+ */
77
+ let globalPgOmitListSuffix = null;
78
+ function overrideListSuffix(listSuffix, cb) {
79
+ if (listSuffix == null) {
80
+ return cb();
81
+ }
82
+ if (listSuffix !== 'include' && listSuffix !== 'omit') {
83
+ throw new Error(`Unrecognized @listSuffix value "${String(listSuffix)}". Must be "omit" or "include".`);
84
+ }
85
+ const old = globalPgOmitListSuffix;
86
+ try {
87
+ globalPgOmitListSuffix = listSuffix === 'omit';
88
+ return cb();
89
+ }
90
+ finally {
91
+ globalPgOmitListSuffix = old;
92
+ }
93
+ }
72
94
  exports.InflektPlugin = {
73
95
  name: 'InflektPlugin',
74
96
  version: '1.0.0',
75
97
  inflection: {
98
+ add: {
99
+ /**
100
+ * Pluralize ensuring the result differs from the singular.
101
+ * Registered as an inflection method so other plugins can call this.distinctPluralize().
102
+ */
103
+ distinctPluralize(_preset, str) {
104
+ return (0, inflekt_1.distinctPluralize)(str);
105
+ },
106
+ /**
107
+ * Extract base name from FK attribute names (e.g. "author_id" -> "author").
108
+ * Registered so other plugins can call this._getBaseName().
109
+ */
110
+ _getBaseName(_preset, attributeName) {
111
+ return getBaseName(attributeName);
112
+ },
113
+ /**
114
+ * Check if a base name matches another name when singularized.
115
+ * Registered so other plugins can call this._baseNameMatches().
116
+ */
117
+ _baseNameMatches(_preset, baseName, otherName) {
118
+ return baseNameMatches(baseName, otherName);
119
+ },
120
+ /**
121
+ * Get the opposite name for a relation base name (e.g. "parent" -> "child").
122
+ * Registered so other plugins can call this._getOppositeBaseName().
123
+ */
124
+ _getOppositeBaseName(_preset, baseName) {
125
+ return getOppositeBaseName(baseName);
126
+ },
127
+ /**
128
+ * Extract base name from composite FK keys.
129
+ * Handles single-key (delegates to getBaseName) and multi-key
130
+ * (joins base names with hyphens) FK relations.
131
+ */
132
+ _getBaseNameFromKeys(preset, detailedKeys) {
133
+ if (detailedKeys.length === 1) {
134
+ const key = detailedKeys[0];
135
+ const attributeName = this._attributeName({
136
+ ...key,
137
+ skipRowId: true,
138
+ });
139
+ return getBaseName(attributeName);
140
+ }
141
+ if (preset.schema?.pgSimplifyMultikeyRelations) {
142
+ const attributeNames = detailedKeys.map((key) => this._attributeName({ ...key, skipRowId: true }));
143
+ const baseNames = attributeNames.map((attr) => getBaseName(attr));
144
+ if (baseNames.every((n) => n)) {
145
+ return baseNames.join('-');
146
+ }
147
+ }
148
+ return null;
149
+ },
150
+ },
76
151
  replace: {
77
152
  /**
78
153
  * Remove schema prefixes from all schemas.
@@ -171,22 +246,97 @@ exports.InflektPlugin = {
171
246
  singularize(_previous, _preset, str) {
172
247
  return (0, inflekt_1.singularizeLast)(str);
173
248
  },
249
+ /**
250
+ * Connection field suffix — respects @listSuffix smart tag.
251
+ * When pgOmitListSuffix is true, connections get "Connection" suffix.
252
+ */
253
+ connectionField(_prev, options, baseName) {
254
+ return (globalPgOmitListSuffix ?? options.schema?.pgOmitListSuffix)
255
+ ? baseName + 'Connection'
256
+ : baseName;
257
+ },
258
+ /**
259
+ * List field suffix — respects @listSuffix smart tag.
260
+ * When pgOmitListSuffix is true, lists are bare (no suffix).
261
+ */
262
+ listField(_prev, options, baseName) {
263
+ return (globalPgOmitListSuffix ?? options.schema?.pgOmitListSuffix)
264
+ ? baseName
265
+ : baseName + 'List';
266
+ },
174
267
  /**
175
268
  * Simplify root query connection fields (allUsers -> users)
269
+ * Supports @listSuffix smart tag per-table.
176
270
  */
177
271
  allRowsConnection(_previous, _options, resource) {
178
- const resourceName = this._singularizedResourceName(resource);
179
- return (0, inflekt_1.toCamelCase)((0, inflekt_1.distinctPluralize)(resourceName));
272
+ const listSuffix = resource.extensions?.tags?.listSuffix;
273
+ return overrideListSuffix(listSuffix, () => {
274
+ const resourceName = this._singularizedResourceName(resource);
275
+ return this.connectionField((0, inflekt_1.toCamelCase)((0, inflekt_1.distinctPluralize)(resourceName)));
276
+ });
180
277
  },
181
278
  /**
182
- * Simplify root query list fields
279
+ * Simplify root query list fields.
280
+ * Supports @listSuffix smart tag per-table.
183
281
  */
184
282
  allRowsList(_previous, _options, resource) {
185
- const resourceName = this._singularizedResourceName(resource);
186
- return (0, inflekt_1.toCamelCase)((0, inflekt_1.distinctPluralize)(resourceName)) + 'List';
283
+ const listSuffix = resource.extensions?.tags?.listSuffix;
284
+ return overrideListSuffix(listSuffix, () => {
285
+ const resourceName = this._singularizedResourceName(resource);
286
+ return this.listField((0, inflekt_1.toCamelCase)((0, inflekt_1.distinctPluralize)(resourceName)));
287
+ });
288
+ },
289
+ /**
290
+ * @listSuffix passthrough for many-relation connection fields
291
+ */
292
+ manyRelationConnection(previous, _options, details) {
293
+ const { registry, codec, relationName } = details;
294
+ const relation = registry.pgRelations[codec.name]?.[relationName];
295
+ const listSuffix = (relation?.extensions?.tags?.listSuffix ??
296
+ relation?.remoteResource?.extensions?.tags?.listSuffix);
297
+ return overrideListSuffix(listSuffix, () => previous(details));
298
+ },
299
+ /**
300
+ * @listSuffix passthrough for many-relation list fields
301
+ */
302
+ manyRelationList(previous, _options, details) {
303
+ const { registry, codec, relationName } = details;
304
+ const relation = registry.pgRelations[codec.name]?.[relationName];
305
+ const listSuffix = (relation?.extensions?.tags?.listSuffix ??
306
+ relation?.remoteResource?.extensions?.tags?.listSuffix);
307
+ return overrideListSuffix(listSuffix, () => previous(details));
308
+ },
309
+ /**
310
+ * @listSuffix passthrough for custom query connection fields
311
+ */
312
+ customQueryConnectionField(previous, _options, details) {
313
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
314
+ return overrideListSuffix(listSuffix, () => previous(details));
315
+ },
316
+ /**
317
+ * @listSuffix passthrough for custom query list fields
318
+ */
319
+ customQueryListField(previous, _options, details) {
320
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
321
+ return overrideListSuffix(listSuffix, () => previous(details));
322
+ },
323
+ /**
324
+ * @listSuffix passthrough for computed attribute connection fields
325
+ */
326
+ computedAttributeConnectionField(previous, _options, details) {
327
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
328
+ return overrideListSuffix(listSuffix, () => previous(details));
329
+ },
330
+ /**
331
+ * @listSuffix passthrough for computed attribute list fields
332
+ */
333
+ computedAttributeListField(previous, _options, details) {
334
+ const listSuffix = details.resource?.extensions?.tags?.listSuffix;
335
+ return overrideListSuffix(listSuffix, () => previous(details));
187
336
  },
188
337
  /**
189
- * Simplify single relation field names (userByAuthorId -> author)
338
+ * Simplify single relation field names (userByAuthorId -> author).
339
+ * Uses _getBaseNameFromKeys for composite FK support.
190
340
  */
191
341
  singleRelation(previous, _options, details) {
192
342
  const { registry, codec, relationName } = details;
@@ -197,7 +347,10 @@ exports.InflektPlugin = {
197
347
  if (typeof relation.extensions?.tags?.fieldName === 'string') {
198
348
  return relation.extensions.tags.fieldName;
199
349
  }
200
- // Try to extract base name from the local attribute
350
+ const detailedKeys = relation.localAttributes.map((attributeName) => ({
351
+ codec,
352
+ attributeName,
353
+ }));
201
354
  if (relation.localAttributes.length === 1) {
202
355
  const attributeName = relation.localAttributes[0];
203
356
  const baseName = getBaseName(attributeName);
@@ -214,7 +367,8 @@ exports.InflektPlugin = {
214
367
  return previous(details);
215
368
  },
216
369
  /**
217
- * Simplify backwards single relation field names
370
+ * Simplify backwards single relation field names.
371
+ * Uses _getBaseNameFromKeys for composite FK support.
218
372
  */
219
373
  singleRelationBackwards(previous, _options, details) {
220
374
  const { registry, codec, relationName } = details;
@@ -228,7 +382,6 @@ exports.InflektPlugin = {
228
382
  if (typeof relation.extensions?.tags?.foreignFieldName === 'string') {
229
383
  return relation.extensions.tags.foreignFieldName;
230
384
  }
231
- // Try to extract base name from the remote attribute
232
385
  if (relation.remoteAttributes.length === 1) {
233
386
  const attributeName = relation.remoteAttributes[0];
234
387
  const baseName = getBaseName(attributeName);
@@ -245,7 +398,8 @@ exports.InflektPlugin = {
245
398
  return previous(details);
246
399
  },
247
400
  /**
248
- * Simplify many relation field names (postsByAuthorId -> posts)
401
+ * Simplify many relation field names (postsByAuthorId -> posts).
402
+ * Uses _getBaseNameFromKeys for composite FK support.
249
403
  */
250
404
  _manyRelation(previous, _options, details) {
251
405
  const { registry, codec, relationName } = details;
@@ -257,7 +411,6 @@ exports.InflektPlugin = {
257
411
  if (typeof baseOverride === 'string') {
258
412
  return baseOverride;
259
413
  }
260
- // Try to extract base name from the remote attribute
261
414
  if (relation.remoteAttributes.length === 1) {
262
415
  const attributeName = relation.remoteAttributes[0];
263
416
  const baseName = getBaseName(attributeName);