@constructive-io/graphql-codegen 4.5.3 → 4.6.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.
- package/core/codegen/hooks-docs-generator.js +16 -16
- package/core/codegen/mutations.js +6 -6
- package/core/codegen/orm/custom-ops-generator.d.ts +2 -2
- package/core/codegen/orm/custom-ops-generator.js +15 -6
- package/core/codegen/orm/docs-generator.js +6 -6
- package/core/codegen/orm/index.js +4 -3
- package/core/codegen/orm/input-types-generator.d.ts +1 -1
- package/core/codegen/orm/input-types-generator.js +41 -17
- package/core/codegen/queries.js +12 -10
- package/core/codegen/schema-types-generator.d.ts +2 -0
- package/core/codegen/schema-types-generator.js +41 -12
- package/core/codegen/shared/index.js +2 -0
- package/core/codegen/utils.d.ts +14 -0
- package/core/codegen/utils.js +87 -0
- package/core/introspect/infer-tables.d.ts +5 -0
- package/core/introspect/infer-tables.js +54 -4
- package/core/pipeline/index.js +2 -1
- package/esm/core/codegen/hooks-docs-generator.js +16 -16
- package/esm/core/codegen/mutations.js +6 -6
- package/esm/core/codegen/orm/custom-ops-generator.d.ts +2 -2
- package/esm/core/codegen/orm/custom-ops-generator.js +17 -8
- package/esm/core/codegen/orm/docs-generator.js +6 -6
- package/esm/core/codegen/orm/index.js +4 -3
- package/esm/core/codegen/orm/input-types-generator.d.ts +1 -1
- package/esm/core/codegen/orm/input-types-generator.js +43 -19
- package/esm/core/codegen/queries.js +12 -10
- package/esm/core/codegen/schema-types-generator.d.ts +2 -0
- package/esm/core/codegen/schema-types-generator.js +43 -14
- package/esm/core/codegen/shared/index.js +2 -0
- package/esm/core/codegen/utils.d.ts +14 -0
- package/esm/core/codegen/utils.js +86 -0
- package/esm/core/introspect/infer-tables.d.ts +5 -0
- package/esm/core/introspect/infer-tables.js +55 -5
- package/esm/core/pipeline/index.js +2 -1
- package/esm/generators/field-selector.js +32 -7
- package/esm/generators/mutations.d.ts +1 -2
- package/esm/generators/mutations.js +12 -9
- package/esm/generators/naming-helpers.d.ts +48 -0
- package/esm/generators/naming-helpers.js +154 -0
- package/esm/generators/select.d.ts +1 -12
- package/esm/generators/select.js +96 -71
- package/esm/types/config.d.ts +6 -0
- package/esm/types/config.js +1 -0
- package/esm/types/query.d.ts +9 -0
- package/esm/types/schema.d.ts +8 -0
- package/generators/field-selector.js +32 -7
- package/generators/mutations.d.ts +1 -2
- package/generators/mutations.js +12 -9
- package/generators/naming-helpers.d.ts +48 -0
- package/generators/naming-helpers.js +169 -0
- package/generators/select.d.ts +1 -12
- package/generators/select.js +98 -72
- package/package.json +6 -6
- package/types/config.d.ts +6 -0
- package/types/config.js +1 -0
- package/types/query.d.ts +9 -0
- package/types/schema.d.ts +8 -0
|
@@ -323,6 +323,92 @@ export function getQueryKeyPrefix(table) {
|
|
|
323
323
|
return lcFirst(table.name);
|
|
324
324
|
}
|
|
325
325
|
// ============================================================================
|
|
326
|
+
// Smart Comment Utilities
|
|
327
|
+
// ============================================================================
|
|
328
|
+
/**
|
|
329
|
+
* PostGraphile smart comment tags that should be stripped from descriptions.
|
|
330
|
+
* Smart comments start with `@` and control PostGraphile behavior
|
|
331
|
+
* (e.g., `@omit`, `@name`, `@foreignKey`, etc.)
|
|
332
|
+
*
|
|
333
|
+
* A PostgreSQL COMMENT may contain both human-readable text and smart comments:
|
|
334
|
+
* COMMENT ON TABLE users IS 'User accounts for the application\n@omit delete';
|
|
335
|
+
*
|
|
336
|
+
* PostGraphile's introspection already separates these: the GraphQL `description`
|
|
337
|
+
* field contains only the human-readable part. So in most cases, the description
|
|
338
|
+
* we receive from introspection is already clean.
|
|
339
|
+
*
|
|
340
|
+
* However, as a safety measure, this utility strips any remaining `@`-prefixed
|
|
341
|
+
* lines that may have leaked through.
|
|
342
|
+
*/
|
|
343
|
+
/**
|
|
344
|
+
* PostGraphile auto-generated boilerplate descriptions that add no value.
|
|
345
|
+
* These are generic descriptions PostGraphile puts on every mutation input,
|
|
346
|
+
* clientMutationId field, etc. We filter them out to keep generated code clean.
|
|
347
|
+
*/
|
|
348
|
+
const POSTGRAPHILE_BOILERPLATE = [
|
|
349
|
+
'The exclusive input argument for this mutation.',
|
|
350
|
+
'An arbitrary string value with no semantic meaning.',
|
|
351
|
+
'The exact same `clientMutationId` that was provided in the mutation input,',
|
|
352
|
+
'The output of our',
|
|
353
|
+
'All input for the',
|
|
354
|
+
'A cursor for use in pagination.',
|
|
355
|
+
'An edge for our',
|
|
356
|
+
'Information to aid in pagination.',
|
|
357
|
+
'Reads and enables pagination through a set of',
|
|
358
|
+
'A list of edges which contains the',
|
|
359
|
+
'The count of *all* `',
|
|
360
|
+
'A list of `',
|
|
361
|
+
'Our root query field',
|
|
362
|
+
'Reads a single',
|
|
363
|
+
'The root query type',
|
|
364
|
+
'The root mutation type',
|
|
365
|
+
];
|
|
366
|
+
/**
|
|
367
|
+
* Check if a description is generic PostGraphile boilerplate that should be suppressed.
|
|
368
|
+
*/
|
|
369
|
+
function isBoilerplateDescription(description) {
|
|
370
|
+
const trimmed = description.trim();
|
|
371
|
+
return POSTGRAPHILE_BOILERPLATE.some((bp) => trimmed.startsWith(bp));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Strip PostGraphile smart comments and boilerplate from a description string.
|
|
375
|
+
*
|
|
376
|
+
* Smart comments are lines starting with `@` (e.g., `@omit`, `@name newName`).
|
|
377
|
+
* Boilerplate descriptions are generic PostGraphile-generated text that repeats
|
|
378
|
+
* on every mutation input, clientMutationId field, etc.
|
|
379
|
+
*
|
|
380
|
+
* This returns only the meaningful human-readable portion of the comment,
|
|
381
|
+
* or undefined if the result is empty or boilerplate.
|
|
382
|
+
*
|
|
383
|
+
* @param description - Raw description from GraphQL introspection
|
|
384
|
+
* @returns Cleaned description, or undefined if empty/boilerplate
|
|
385
|
+
*/
|
|
386
|
+
export function stripSmartComments(description, enabled = true) {
|
|
387
|
+
if (!enabled)
|
|
388
|
+
return undefined;
|
|
389
|
+
if (!description)
|
|
390
|
+
return undefined;
|
|
391
|
+
// Check if entire description is boilerplate
|
|
392
|
+
if (isBoilerplateDescription(description))
|
|
393
|
+
return undefined;
|
|
394
|
+
const lines = description.split('\n');
|
|
395
|
+
const cleanLines = [];
|
|
396
|
+
for (const line of lines) {
|
|
397
|
+
const trimmed = line.trim();
|
|
398
|
+
// Skip lines that start with @ (smart comment directives)
|
|
399
|
+
if (trimmed.startsWith('@'))
|
|
400
|
+
continue;
|
|
401
|
+
cleanLines.push(line);
|
|
402
|
+
}
|
|
403
|
+
const result = cleanLines.join('\n').trim();
|
|
404
|
+
if (result.length === 0)
|
|
405
|
+
return undefined;
|
|
406
|
+
// Re-check after stripping smart comments
|
|
407
|
+
if (isBoilerplateDescription(result))
|
|
408
|
+
return undefined;
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
// ============================================================================
|
|
326
412
|
// Code generation helpers
|
|
327
413
|
// ============================================================================
|
|
328
414
|
/**
|
|
@@ -25,6 +25,11 @@ export interface InferTablesOptions {
|
|
|
25
25
|
* Custom pattern overrides (for non-standard PostGraphile configurations)
|
|
26
26
|
*/
|
|
27
27
|
patterns?: Partial<typeof PATTERNS>;
|
|
28
|
+
/**
|
|
29
|
+
* Include PostgreSQL COMMENT descriptions on tables and fields.
|
|
30
|
+
* @default true
|
|
31
|
+
*/
|
|
32
|
+
comments?: boolean;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Infer CleanTable[] from GraphQL introspection by recognizing PostGraphile patterns
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - Mutation operations: create{Name}, update{Name}, delete{Name}
|
|
14
14
|
*/
|
|
15
15
|
import { lcFirst, pluralize, singularize, ucFirst } from 'inflekt';
|
|
16
|
-
import {
|
|
16
|
+
import { stripSmartComments } from '../codegen/utils';
|
|
17
|
+
import { getBaseTypeName, isList, isNonNull, unwrapType } from '../../types/introspection';
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Pattern Matching Constants
|
|
19
20
|
// ============================================================================
|
|
@@ -81,6 +82,7 @@ function isInternalType(name) {
|
|
|
81
82
|
export function inferTablesFromIntrospection(introspection, options = {}) {
|
|
82
83
|
const { __schema: schema } = introspection;
|
|
83
84
|
const { types, queryType, mutationType } = schema;
|
|
85
|
+
const commentsEnabled = options.comments !== false;
|
|
84
86
|
// Build lookup maps for efficient access
|
|
85
87
|
const typeMap = buildTypeMap(types);
|
|
86
88
|
const { entityNames, entityToConnection, connectionToEntity } = buildEntityConnectionMaps(types, typeMap);
|
|
@@ -95,7 +97,7 @@ export function inferTablesFromIntrospection(introspection, options = {}) {
|
|
|
95
97
|
if (!entityType)
|
|
96
98
|
continue;
|
|
97
99
|
// Infer all metadata for this entity
|
|
98
|
-
const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity);
|
|
100
|
+
const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity, commentsEnabled);
|
|
99
101
|
// Only include tables that have at least one real operation
|
|
100
102
|
if (hasRealOperation) {
|
|
101
103
|
tables.push(table);
|
|
@@ -169,9 +171,9 @@ function resolveEntityNameFromConnectionType(connectionType, typeMap) {
|
|
|
169
171
|
/**
|
|
170
172
|
* Build a complete CleanTable from an entity type
|
|
171
173
|
*/
|
|
172
|
-
function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity) {
|
|
174
|
+
function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity, commentsEnabled) {
|
|
173
175
|
// Extract scalar fields from entity type
|
|
174
|
-
const fields = extractEntityFields(entityType, typeMap, entityToConnection);
|
|
176
|
+
const fields = extractEntityFields(entityType, typeMap, entityToConnection, commentsEnabled);
|
|
175
177
|
// Infer relations from entity fields
|
|
176
178
|
const relations = inferRelations(entityType, entityToConnection, connectionToEntity);
|
|
177
179
|
// Match query and mutation operations
|
|
@@ -199,9 +201,12 @@ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationF
|
|
|
199
201
|
delete: mutationOps.delete,
|
|
200
202
|
patchFieldName,
|
|
201
203
|
};
|
|
204
|
+
// Extract description from entity type (PostgreSQL COMMENT), strip smart comments
|
|
205
|
+
const description = commentsEnabled ? stripSmartComments(entityType.description) : undefined;
|
|
202
206
|
return {
|
|
203
207
|
table: {
|
|
204
208
|
name: entityName,
|
|
209
|
+
...(description ? { description } : {}),
|
|
205
210
|
fields,
|
|
206
211
|
relations,
|
|
207
212
|
inflection,
|
|
@@ -218,10 +223,14 @@ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationF
|
|
|
218
223
|
* Extract scalar fields from an entity type
|
|
219
224
|
* Excludes relation fields (those returning other entity types or connections)
|
|
220
225
|
*/
|
|
221
|
-
function extractEntityFields(entityType, typeMap, entityToConnection) {
|
|
226
|
+
function extractEntityFields(entityType, typeMap, entityToConnection, commentsEnabled) {
|
|
222
227
|
const fields = [];
|
|
223
228
|
if (!entityType.fields)
|
|
224
229
|
return fields;
|
|
230
|
+
// Build a lookup of CreateXxxInput fields to infer hasDefault.
|
|
231
|
+
// If a field is NOT NULL on the entity but NOT required in CreateXxxInput,
|
|
232
|
+
// then it likely has a server-side default (serial, uuid_generate_v4, now(), etc.).
|
|
233
|
+
const createInputRequiredFields = buildCreateInputRequiredFieldSet(entityType.name, typeMap);
|
|
225
234
|
for (const field of entityType.fields) {
|
|
226
235
|
const baseTypeName = getBaseTypeName(field.type);
|
|
227
236
|
if (!baseTypeName)
|
|
@@ -235,14 +244,55 @@ function extractEntityFields(entityType, typeMap, entityToConnection) {
|
|
|
235
244
|
continue; // Skip relation fields
|
|
236
245
|
}
|
|
237
246
|
}
|
|
247
|
+
// Infer isNotNull from the NON_NULL wrapper on the entity type field
|
|
248
|
+
const fieldIsNotNull = isNonNull(field.type);
|
|
249
|
+
// Infer hasDefault: if a field is NOT NULL on the entity but NOT required
|
|
250
|
+
// in CreateXxxInput, it likely has a default value.
|
|
251
|
+
// Also: if it's absent from CreateInput entirely, it's likely computed/generated.
|
|
252
|
+
let fieldHasDefault = null;
|
|
253
|
+
if (createInputRequiredFields !== null) {
|
|
254
|
+
if (fieldIsNotNull && !createInputRequiredFields.has(field.name)) {
|
|
255
|
+
fieldHasDefault = true;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
fieldHasDefault = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
238
261
|
// Include scalar, enum, and other non-relation fields
|
|
262
|
+
const fieldDescription = commentsEnabled ? stripSmartComments(field.description) : undefined;
|
|
239
263
|
fields.push({
|
|
240
264
|
name: field.name,
|
|
265
|
+
...(fieldDescription ? { description: fieldDescription } : {}),
|
|
241
266
|
type: convertToCleanFieldType(field.type),
|
|
267
|
+
isNotNull: fieldIsNotNull,
|
|
268
|
+
hasDefault: fieldHasDefault,
|
|
242
269
|
});
|
|
243
270
|
}
|
|
244
271
|
return fields;
|
|
245
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Build a set of field names that are required (NON_NULL) in the CreateXxxInput type.
|
|
275
|
+
* Returns null if the CreateXxxInput type doesn't exist (no create mutation).
|
|
276
|
+
*/
|
|
277
|
+
function buildCreateInputRequiredFieldSet(entityName, typeMap) {
|
|
278
|
+
const createInputName = `Create${entityName}Input`;
|
|
279
|
+
const createInput = typeMap.get(createInputName);
|
|
280
|
+
if (!createInput?.inputFields)
|
|
281
|
+
return null;
|
|
282
|
+
// The CreateXxxInput typically has a single field like { user: UserInput! }
|
|
283
|
+
// We need to look inside the actual entity input type (e.g., UserInput)
|
|
284
|
+
const entityInputName = `${entityName}Input`;
|
|
285
|
+
const entityInput = typeMap.get(entityInputName);
|
|
286
|
+
if (!entityInput?.inputFields)
|
|
287
|
+
return null;
|
|
288
|
+
const requiredFields = new Set();
|
|
289
|
+
for (const inputField of entityInput.inputFields) {
|
|
290
|
+
if (isNonNull(inputField.type)) {
|
|
291
|
+
requiredFields.add(inputField.name);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return requiredFields;
|
|
295
|
+
}
|
|
246
296
|
/**
|
|
247
297
|
* Check if a type name is an entity type (has a corresponding Connection)
|
|
248
298
|
*/
|
|
@@ -23,7 +23,8 @@ export async function runCodegenPipeline(options) {
|
|
|
23
23
|
const { introspection } = await source.fetch();
|
|
24
24
|
// 2. Infer tables from introspection (replaces _meta)
|
|
25
25
|
log('Inferring table metadata from schema...');
|
|
26
|
-
|
|
26
|
+
const commentsEnabled = config.codegen?.comments !== false;
|
|
27
|
+
let tables = inferTablesFromIntrospection(introspection, { comments: commentsEnabled });
|
|
27
28
|
const totalTables = tables.length;
|
|
28
29
|
log(` Found ${totalTables} tables`);
|
|
29
30
|
// 3. Filter tables by config (combine exclude and systemExclude)
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
const relationalFieldSetCache = new WeakMap();
|
|
2
|
+
function getRelationalFieldSet(table) {
|
|
3
|
+
const cached = relationalFieldSetCache.get(table);
|
|
4
|
+
if (cached)
|
|
5
|
+
return cached;
|
|
6
|
+
const set = new Set();
|
|
7
|
+
for (const rel of table.relations.belongsTo) {
|
|
8
|
+
if (rel.fieldName)
|
|
9
|
+
set.add(rel.fieldName);
|
|
10
|
+
}
|
|
11
|
+
for (const rel of table.relations.hasOne) {
|
|
12
|
+
if (rel.fieldName)
|
|
13
|
+
set.add(rel.fieldName);
|
|
14
|
+
}
|
|
15
|
+
for (const rel of table.relations.hasMany) {
|
|
16
|
+
if (rel.fieldName)
|
|
17
|
+
set.add(rel.fieldName);
|
|
18
|
+
}
|
|
19
|
+
for (const rel of table.relations.manyToMany) {
|
|
20
|
+
if (rel.fieldName)
|
|
21
|
+
set.add(rel.fieldName);
|
|
22
|
+
}
|
|
23
|
+
relationalFieldSetCache.set(table, set);
|
|
24
|
+
return set;
|
|
25
|
+
}
|
|
1
26
|
/**
|
|
2
27
|
* Convert simplified field selection to QueryBuilder SelectionOptions
|
|
3
28
|
*/
|
|
@@ -154,11 +179,7 @@ function getNonRelationalFields(table) {
|
|
|
154
179
|
* Check if a field is relational using table metadata
|
|
155
180
|
*/
|
|
156
181
|
export function isRelationalField(fieldName, table) {
|
|
157
|
-
|
|
158
|
-
return (belongsTo.some((rel) => rel.fieldName === fieldName) ||
|
|
159
|
-
hasOne.some((rel) => rel.fieldName === fieldName) ||
|
|
160
|
-
hasMany.some((rel) => rel.fieldName === fieldName) ||
|
|
161
|
-
manyToMany.some((rel) => rel.fieldName === fieldName));
|
|
182
|
+
return getRelationalFieldSet(table).has(fieldName);
|
|
162
183
|
}
|
|
163
184
|
/**
|
|
164
185
|
* Get scalar fields for a related table to include in relation queries
|
|
@@ -227,16 +248,20 @@ function getRelatedTableScalarFields(relationField, table, allTables) {
|
|
|
227
248
|
'createdAt',
|
|
228
249
|
'updatedAt',
|
|
229
250
|
];
|
|
251
|
+
const scalarFieldSet = new Set(scalarFields);
|
|
252
|
+
// Use Set for O(1) duplicate checking
|
|
253
|
+
const includedSet = new Set();
|
|
230
254
|
const included = [];
|
|
231
255
|
const push = (fieldName) => {
|
|
232
256
|
if (!fieldName)
|
|
233
257
|
return;
|
|
234
|
-
if (!
|
|
258
|
+
if (!scalarFieldSet.has(fieldName))
|
|
235
259
|
return;
|
|
236
|
-
if (
|
|
260
|
+
if (includedSet.has(fieldName))
|
|
237
261
|
return;
|
|
238
262
|
if (included.length >= MAX_RELATED_FIELDS)
|
|
239
263
|
return;
|
|
264
|
+
includedSet.add(fieldName);
|
|
240
265
|
included.push(fieldName);
|
|
241
266
|
};
|
|
242
267
|
// Always try to include stable identifiers first.
|
|
@@ -17,8 +17,7 @@ export declare function buildPostGraphileCreate(table: CleanTable, _allTables: C
|
|
|
17
17
|
export declare function buildPostGraphileUpdate(table: CleanTable, _allTables: CleanTable[], _options?: MutationOptions): TypedDocumentString<Record<string, unknown>, {
|
|
18
18
|
input: {
|
|
19
19
|
id: string | number;
|
|
20
|
-
|
|
21
|
-
};
|
|
20
|
+
} & Record<string, unknown>;
|
|
22
21
|
}>;
|
|
23
22
|
/**
|
|
24
23
|
* Build PostGraphile-style DELETE mutation
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as t from 'gql-ast';
|
|
6
6
|
import { OperationTypeNode, print } from 'graphql';
|
|
7
|
-
import { camelize } from 'inflekt';
|
|
8
7
|
import { TypedDocumentString } from '../client/typed-document';
|
|
9
8
|
import { getCustomAstForCleanField, requiresSubfieldSelection, } from '../core/custom-ast';
|
|
10
9
|
import { isRelationalField } from './field-selector';
|
|
10
|
+
import { toCamelCaseSingular, toCreateInputTypeName, toCreateMutationName, toDeleteInputTypeName, toDeleteMutationName, toUpdateInputTypeName, toUpdateMutationName, } from './naming-helpers';
|
|
11
11
|
/**
|
|
12
12
|
* Generate field selections for PostGraphile mutations using custom AST logic
|
|
13
13
|
* This handles both scalar fields and complex types that require subfield selections
|
|
@@ -31,14 +31,15 @@ function generateFieldSelections(table) {
|
|
|
31
31
|
* PostGraphile expects: mutation { createTableName(input: { tableName: TableNameInput! }) { tableName { ... } } }
|
|
32
32
|
*/
|
|
33
33
|
export function buildPostGraphileCreate(table, _allTables, _options = {}) {
|
|
34
|
-
const mutationName =
|
|
35
|
-
const singularName =
|
|
34
|
+
const mutationName = toCreateMutationName(table.name, table);
|
|
35
|
+
const singularName = toCamelCaseSingular(table.name, table);
|
|
36
|
+
const inputTypeName = toCreateInputTypeName(table.name, table);
|
|
36
37
|
// Create the variable definition for $input
|
|
37
38
|
const variableDefinitions = [
|
|
38
39
|
t.variableDefinition({
|
|
39
40
|
variable: t.variable({ name: 'input' }),
|
|
40
41
|
type: t.nonNullType({
|
|
41
|
-
type: t.namedType({ type:
|
|
42
|
+
type: t.namedType({ type: inputTypeName }),
|
|
42
43
|
}),
|
|
43
44
|
}),
|
|
44
45
|
];
|
|
@@ -90,14 +91,15 @@ export function buildPostGraphileCreate(table, _allTables, _options = {}) {
|
|
|
90
91
|
* PostGraphile expects: mutation { updateTableName(input: { id: UUID!, patch: TableNamePatch! }) { tableName { ... } } }
|
|
91
92
|
*/
|
|
92
93
|
export function buildPostGraphileUpdate(table, _allTables, _options = {}) {
|
|
93
|
-
const mutationName =
|
|
94
|
-
const singularName =
|
|
94
|
+
const mutationName = toUpdateMutationName(table.name, table);
|
|
95
|
+
const singularName = toCamelCaseSingular(table.name, table);
|
|
96
|
+
const inputTypeName = toUpdateInputTypeName(table.name);
|
|
95
97
|
// Create the variable definition for $input
|
|
96
98
|
const variableDefinitions = [
|
|
97
99
|
t.variableDefinition({
|
|
98
100
|
variable: t.variable({ name: 'input' }),
|
|
99
101
|
type: t.nonNullType({
|
|
100
|
-
type: t.namedType({ type:
|
|
102
|
+
type: t.namedType({ type: inputTypeName }),
|
|
101
103
|
}),
|
|
102
104
|
}),
|
|
103
105
|
];
|
|
@@ -149,13 +151,14 @@ export function buildPostGraphileUpdate(table, _allTables, _options = {}) {
|
|
|
149
151
|
* PostGraphile expects: mutation { deleteTableName(input: { id: UUID! }) { clientMutationId } }
|
|
150
152
|
*/
|
|
151
153
|
export function buildPostGraphileDelete(table, _allTables, _options = {}) {
|
|
152
|
-
const mutationName =
|
|
154
|
+
const mutationName = toDeleteMutationName(table.name, table);
|
|
155
|
+
const inputTypeName = toDeleteInputTypeName(table.name);
|
|
153
156
|
// Create the variable definition for $input
|
|
154
157
|
const variableDefinitions = [
|
|
155
158
|
t.variableDefinition({
|
|
156
159
|
variable: t.variable({ name: 'input' }),
|
|
157
160
|
type: t.nonNullType({
|
|
158
|
-
type: t.namedType({ type:
|
|
161
|
+
type: t.namedType({ type: inputTypeName }),
|
|
159
162
|
}),
|
|
160
163
|
}),
|
|
161
164
|
];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CleanTable } from '../types/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Safely normalise a server-provided inflection value.
|
|
4
|
+
* Returns `null` for null, undefined, or whitespace-only strings.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeInflectionValue(value: string | null | undefined): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Convert PascalCase table name to camelCase plural for GraphQL queries.
|
|
9
|
+
* Prefers server-provided `table.query.all` / `table.inflection.allRows`
|
|
10
|
+
* when available, with guards against naive pluralisation drift and
|
|
11
|
+
* missing camelCase boundaries.
|
|
12
|
+
*
|
|
13
|
+
* Example: "ActionGoal" -> "actionGoals", "User" -> "users", "Person" -> "people"
|
|
14
|
+
*/
|
|
15
|
+
export declare function toCamelCasePlural(tableName: string, table?: CleanTable | null): string;
|
|
16
|
+
/**
|
|
17
|
+
* Convert PascalCase table name to camelCase singular field name.
|
|
18
|
+
* Prefers server-provided names when available.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toCamelCaseSingular(tableName: string, table?: CleanTable | null): string;
|
|
21
|
+
export declare function toCreateMutationName(tableName: string, table?: CleanTable | null): string;
|
|
22
|
+
export declare function toUpdateMutationName(tableName: string, table?: CleanTable | null): string;
|
|
23
|
+
export declare function toDeleteMutationName(tableName: string, table?: CleanTable | null): string;
|
|
24
|
+
export declare function toCreateInputTypeName(tableName: string, table?: CleanTable | null): string;
|
|
25
|
+
export declare function toUpdateInputTypeName(tableName: string): string;
|
|
26
|
+
export declare function toDeleteInputTypeName(tableName: string): string;
|
|
27
|
+
export declare function toFilterTypeName(tableName: string, table?: CleanTable | null): string;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve PostGraphile patch field name.
|
|
30
|
+
* In v5 this is typically entity-specific: e.g. "userPatch", "contactPatch".
|
|
31
|
+
* Prefers the value discovered from the schema (`table.query.patchFieldName`
|
|
32
|
+
* or `table.inflection.patchField`), falls back to `${singularName}Patch`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function toPatchFieldName(tableName: string, table?: CleanTable | null): string;
|
|
35
|
+
/**
|
|
36
|
+
* Convert camelCase field name to SCREAMING_SNAKE_CASE for PostGraphile
|
|
37
|
+
* orderBy enums.
|
|
38
|
+
*
|
|
39
|
+
* "displayName" -> "DISPLAY_NAME_ASC"
|
|
40
|
+
* "createdAt" -> "CREATED_AT_DESC"
|
|
41
|
+
* "id" -> "ID_ASC"
|
|
42
|
+
*/
|
|
43
|
+
export declare function toOrderByEnumValue(fieldName: string, direction: 'asc' | 'desc'): string;
|
|
44
|
+
/**
|
|
45
|
+
* Generate the PostGraphile OrderBy enum type name for a table.
|
|
46
|
+
* Prefers server-provided `table.inflection.orderByType` when available.
|
|
47
|
+
*/
|
|
48
|
+
export declare function toOrderByTypeName(tableName: string, table?: CleanTable | null): string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-aware naming helpers for GraphQL query/mutation generation.
|
|
3
|
+
*
|
|
4
|
+
* These functions prefer names already discovered from the GraphQL schema
|
|
5
|
+
* (stored on `table.query` and `table.inflection` by `infer-tables.ts`)
|
|
6
|
+
* and fall back to local inflection when introspection data is unavailable.
|
|
7
|
+
*
|
|
8
|
+
* Back-ported from Dashboard's `packages/data/src/query-generator.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import { camelize, pluralize } from 'inflekt';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Internal helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Safely normalise a server-provided inflection value.
|
|
16
|
+
* Returns `null` for null, undefined, or whitespace-only strings.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeInflectionValue(value) {
|
|
19
|
+
if (typeof value !== 'string')
|
|
20
|
+
return null;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Plural / Singular
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Convert PascalCase table name to camelCase plural for GraphQL queries.
|
|
29
|
+
* Prefers server-provided `table.query.all` / `table.inflection.allRows`
|
|
30
|
+
* when available, with guards against naive pluralisation drift and
|
|
31
|
+
* missing camelCase boundaries.
|
|
32
|
+
*
|
|
33
|
+
* Example: "ActionGoal" -> "actionGoals", "User" -> "users", "Person" -> "people"
|
|
34
|
+
*/
|
|
35
|
+
export function toCamelCasePlural(tableName, table) {
|
|
36
|
+
const singular = camelize(tableName, true);
|
|
37
|
+
const inflectedPlural = pluralize(singular);
|
|
38
|
+
const serverPluralCandidates = [
|
|
39
|
+
table?.query?.all,
|
|
40
|
+
table?.inflection?.allRows,
|
|
41
|
+
];
|
|
42
|
+
for (const candidateRaw of serverPluralCandidates) {
|
|
43
|
+
const candidate = normalizeInflectionValue(candidateRaw);
|
|
44
|
+
if (!candidate)
|
|
45
|
+
continue;
|
|
46
|
+
// Guard against known fallback drift:
|
|
47
|
+
// 1. Naive pluralisation: "activitys" instead of "activities"
|
|
48
|
+
const isNaivePlural = candidate === `${singular}s` && candidate !== inflectedPlural;
|
|
49
|
+
// 2. Missing camelCase boundaries: "deliveryzones" instead of "deliveryZones"
|
|
50
|
+
const isMiscased = candidate !== inflectedPlural &&
|
|
51
|
+
candidate.toLowerCase() === inflectedPlural.toLowerCase();
|
|
52
|
+
if (isNaivePlural || isMiscased)
|
|
53
|
+
continue;
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
return inflectedPlural;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Convert PascalCase table name to camelCase singular field name.
|
|
60
|
+
* Prefers server-provided names when available.
|
|
61
|
+
*/
|
|
62
|
+
export function toCamelCaseSingular(tableName, table) {
|
|
63
|
+
const localSingular = camelize(tableName, true);
|
|
64
|
+
for (const candidateRaw of [
|
|
65
|
+
table?.query?.one,
|
|
66
|
+
table?.inflection?.tableFieldName,
|
|
67
|
+
]) {
|
|
68
|
+
const candidate = normalizeInflectionValue(candidateRaw);
|
|
69
|
+
if (!candidate)
|
|
70
|
+
continue;
|
|
71
|
+
// Reject miscased versions: "deliveryzone" vs "deliveryZone"
|
|
72
|
+
if (candidate !== localSingular &&
|
|
73
|
+
candidate.toLowerCase() === localSingular.toLowerCase())
|
|
74
|
+
continue;
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
return localSingular;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Mutation names
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
export function toCreateMutationName(tableName, table) {
|
|
83
|
+
return (normalizeInflectionValue(table?.query?.create) ?? `create${tableName}`);
|
|
84
|
+
}
|
|
85
|
+
export function toUpdateMutationName(tableName, table) {
|
|
86
|
+
return (normalizeInflectionValue(table?.query?.update) ?? `update${tableName}`);
|
|
87
|
+
}
|
|
88
|
+
export function toDeleteMutationName(tableName, table) {
|
|
89
|
+
return (normalizeInflectionValue(table?.query?.delete) ?? `delete${tableName}`);
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Input / type names
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
export function toCreateInputTypeName(tableName, table) {
|
|
95
|
+
return (normalizeInflectionValue(table?.inflection?.createInputType) ??
|
|
96
|
+
`Create${tableName}Input`);
|
|
97
|
+
}
|
|
98
|
+
export function toUpdateInputTypeName(tableName) {
|
|
99
|
+
return `Update${tableName}Input`;
|
|
100
|
+
}
|
|
101
|
+
export function toDeleteInputTypeName(tableName) {
|
|
102
|
+
return `Delete${tableName}Input`;
|
|
103
|
+
}
|
|
104
|
+
export function toFilterTypeName(tableName, table) {
|
|
105
|
+
return (normalizeInflectionValue(table?.inflection?.filterType) ??
|
|
106
|
+
`${tableName}Filter`);
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Patch field name
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Resolve PostGraphile patch field name.
|
|
113
|
+
* In v5 this is typically entity-specific: e.g. "userPatch", "contactPatch".
|
|
114
|
+
* Prefers the value discovered from the schema (`table.query.patchFieldName`
|
|
115
|
+
* or `table.inflection.patchField`), falls back to `${singularName}Patch`.
|
|
116
|
+
*/
|
|
117
|
+
export function toPatchFieldName(tableName, table) {
|
|
118
|
+
// First check the patch field name discovered from UpdateXxxInput
|
|
119
|
+
const introspectedPatch = normalizeInflectionValue(table?.query?.patchFieldName);
|
|
120
|
+
if (introspectedPatch)
|
|
121
|
+
return introspectedPatch;
|
|
122
|
+
// Then check the inflection table
|
|
123
|
+
const explicitPatchField = normalizeInflectionValue(table?.inflection?.patchField);
|
|
124
|
+
if (explicitPatchField)
|
|
125
|
+
return explicitPatchField;
|
|
126
|
+
return `${toCamelCaseSingular(tableName, table)}Patch`;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// OrderBy helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/**
|
|
132
|
+
* Convert camelCase field name to SCREAMING_SNAKE_CASE for PostGraphile
|
|
133
|
+
* orderBy enums.
|
|
134
|
+
*
|
|
135
|
+
* "displayName" -> "DISPLAY_NAME_ASC"
|
|
136
|
+
* "createdAt" -> "CREATED_AT_DESC"
|
|
137
|
+
* "id" -> "ID_ASC"
|
|
138
|
+
*/
|
|
139
|
+
export function toOrderByEnumValue(fieldName, direction) {
|
|
140
|
+
const screaming = fieldName
|
|
141
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
142
|
+
.toUpperCase();
|
|
143
|
+
return `${screaming}_${direction.toUpperCase()}`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Generate the PostGraphile OrderBy enum type name for a table.
|
|
147
|
+
* Prefers server-provided `table.inflection.orderByType` when available.
|
|
148
|
+
*/
|
|
149
|
+
export function toOrderByTypeName(tableName, table) {
|
|
150
|
+
if (table?.inflection?.orderByType)
|
|
151
|
+
return table.inflection.orderByType;
|
|
152
|
+
const plural = toCamelCasePlural(tableName, table);
|
|
153
|
+
return `${plural.charAt(0).toUpperCase() + plural.slice(1)}OrderBy`;
|
|
154
|
+
}
|
|
@@ -3,18 +3,7 @@ import { QueryBuilder } from '../core/query-builder';
|
|
|
3
3
|
import type { IntrospectionSchema, MetaObject } from '../core/types';
|
|
4
4
|
import type { QueryOptions } from '../types/query';
|
|
5
5
|
import type { CleanTable } from '../types/schema';
|
|
6
|
-
|
|
7
|
-
* Convert PascalCase table name to camelCase plural for GraphQL queries
|
|
8
|
-
* Uses the inflection library for proper pluralization
|
|
9
|
-
* Example: "ActionGoal" -> "actionGoals", "User" -> "users", "Person" -> "people"
|
|
10
|
-
*/
|
|
11
|
-
export declare function toCamelCasePlural(tableName: string): string;
|
|
12
|
-
/**
|
|
13
|
-
* Generate the PostGraphile OrderBy enum type name for a table
|
|
14
|
-
* PostGraphile uses pluralized PascalCase: "Product" -> "ProductsOrderBy"
|
|
15
|
-
* Example: "Product" -> "ProductsOrderBy", "Person" -> "PeopleOrderBy"
|
|
16
|
-
*/
|
|
17
|
-
export declare function toOrderByTypeName(tableName: string): string;
|
|
6
|
+
export { toCamelCasePlural, toOrderByTypeName } from './naming-helpers';
|
|
18
7
|
/**
|
|
19
8
|
* Convert CleanTable to MetaObject format for QueryBuilder
|
|
20
9
|
*/
|