@constructive-io/graphql-codegen 2.22.1 → 2.23.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/cli/codegen/barrel.d.ts +5 -1
- package/cli/codegen/barrel.js +13 -11
- package/cli/codegen/index.d.ts +3 -3
- package/cli/codegen/index.js +15 -9
- package/cli/codegen/orm/client-generator.js +3 -2
- package/cli/codegen/orm/custom-ops-generator.js +17 -4
- package/cli/codegen/orm/input-types-generator.js +129 -18
- package/cli/codegen/orm/model-generator.js +2 -1
- package/cli/codegen/orm/query-builder.d.ts +1 -1
- package/cli/codegen/orm/query-builder.js +2 -2
- package/cli/codegen/schema-types-generator.js +5 -5
- package/cli/codegen/utils.d.ts +6 -1
- package/cli/codegen/utils.js +23 -8
- package/cli/commands/generate-orm.d.ts +5 -3
- package/cli/commands/generate-orm.js +65 -84
- package/cli/commands/generate.d.ts +2 -0
- package/cli/commands/generate.js +66 -87
- package/cli/commands/shared.d.ts +74 -0
- package/cli/commands/shared.js +88 -0
- package/cli/index.js +75 -45
- package/cli/introspect/index.d.ts +8 -5
- package/cli/introspect/index.js +19 -7
- package/cli/introspect/infer-tables.d.ts +51 -0
- package/cli/introspect/infer-tables.js +550 -0
- package/cli/introspect/source/endpoint.d.ts +34 -0
- package/cli/introspect/source/endpoint.js +35 -0
- package/cli/introspect/source/file.d.ts +20 -0
- package/cli/introspect/source/file.js +103 -0
- package/cli/introspect/source/index.d.ts +48 -0
- package/cli/introspect/source/index.js +72 -0
- package/cli/introspect/source/types.d.ts +58 -0
- package/cli/introspect/source/types.js +27 -0
- package/cli/introspect/transform.d.ts +5 -6
- package/cli/introspect/transform.js +0 -173
- package/cli/watch/cache.d.ts +3 -4
- package/cli/watch/cache.js +6 -10
- package/cli/watch/poller.d.ts +1 -2
- package/cli/watch/poller.js +27 -45
- package/cli/watch/types.d.ts +0 -3
- package/core/ast.js +4 -4
- package/core/query-builder.js +12 -12
- package/esm/cli/codegen/barrel.d.ts +5 -1
- package/esm/cli/codegen/barrel.js +13 -11
- package/esm/cli/codegen/index.d.ts +3 -3
- package/esm/cli/codegen/index.js +18 -12
- package/esm/cli/codegen/orm/client-generator.js +3 -2
- package/esm/cli/codegen/orm/custom-ops-generator.js +18 -5
- package/esm/cli/codegen/orm/input-types-generator.js +130 -19
- package/esm/cli/codegen/orm/model-generator.js +3 -2
- package/esm/cli/codegen/orm/query-builder.d.ts +1 -1
- package/esm/cli/codegen/orm/query-builder.js +2 -2
- package/esm/cli/codegen/schema-types-generator.js +6 -6
- package/esm/cli/codegen/utils.d.ts +6 -1
- package/esm/cli/codegen/utils.js +22 -8
- package/esm/cli/commands/generate-orm.d.ts +5 -3
- package/esm/cli/commands/generate-orm.js +65 -84
- package/esm/cli/commands/generate.d.ts +2 -0
- package/esm/cli/commands/generate.js +66 -87
- package/esm/cli/commands/shared.d.ts +74 -0
- package/esm/cli/commands/shared.js +84 -0
- package/esm/cli/index.js +76 -46
- package/esm/cli/introspect/index.d.ts +8 -5
- package/esm/cli/introspect/index.js +10 -3
- package/esm/cli/introspect/infer-tables.d.ts +51 -0
- package/esm/cli/introspect/infer-tables.js +547 -0
- package/esm/cli/introspect/source/endpoint.d.ts +34 -0
- package/esm/cli/introspect/source/endpoint.js +31 -0
- package/esm/cli/introspect/source/file.d.ts +20 -0
- package/esm/cli/introspect/source/file.js +66 -0
- package/esm/cli/introspect/source/index.d.ts +48 -0
- package/esm/cli/introspect/source/index.js +54 -0
- package/esm/cli/introspect/source/types.d.ts +58 -0
- package/esm/cli/introspect/source/types.js +23 -0
- package/esm/cli/introspect/transform.d.ts +5 -6
- package/esm/cli/introspect/transform.js +0 -172
- package/esm/cli/watch/cache.d.ts +3 -4
- package/esm/cli/watch/cache.js +7 -11
- package/esm/cli/watch/poller.d.ts +1 -2
- package/esm/cli/watch/poller.js +28 -46
- package/esm/cli/watch/types.d.ts +0 -3
- package/esm/core/ast.js +4 -4
- package/esm/core/query-builder.js +12 -12
- package/esm/generators/mutations.js +3 -3
- package/esm/generators/select.js +7 -7
- package/esm/types/config.d.ts +21 -5
- package/esm/types/config.js +2 -1
- package/generators/mutations.js +3 -3
- package/generators/select.js +7 -7
- package/package.json +5 -3
- package/types/config.d.ts +21 -5
- package/types/config.js +2 -1
- package/cli/introspect/fetch-meta.d.ts +0 -31
- package/cli/introspect/fetch-meta.js +0 -108
- package/cli/introspect/meta-query.d.ts +0 -111
- package/cli/introspect/meta-query.js +0 -191
- package/esm/cli/introspect/fetch-meta.d.ts +0 -31
- package/esm/cli/introspect/fetch-meta.js +0 -104
- package/esm/cli/introspect/meta-query.d.ts +0 -111
- package/esm/cli/introspect/meta-query.js +0 -188
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferTablesFromIntrospection = inferTablesFromIntrospection;
|
|
4
|
+
const introspection_1 = require("../../types/introspection");
|
|
5
|
+
const inflekt_1 = require("inflekt");
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Pattern Matching Constants
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* PostGraphile naming patterns for type detection
|
|
11
|
+
*/
|
|
12
|
+
const PATTERNS = {
|
|
13
|
+
// Type suffixes
|
|
14
|
+
connection: /^(.+)Connection$/,
|
|
15
|
+
edge: /^(.+)Edge$/,
|
|
16
|
+
filter: /^(.+)Filter$/,
|
|
17
|
+
condition: /^(.+)Condition$/,
|
|
18
|
+
orderBy: /^(.+)OrderBy$/,
|
|
19
|
+
patch: /^(.+)Patch$/,
|
|
20
|
+
// Input type patterns
|
|
21
|
+
createInput: /^Create(.+)Input$/,
|
|
22
|
+
updateInput: /^Update(.+)Input$/,
|
|
23
|
+
deleteInput: /^Delete(.+)Input$/,
|
|
24
|
+
// Payload type patterns
|
|
25
|
+
createPayload: /^Create(.+)Payload$/,
|
|
26
|
+
updatePayload: /^Update(.+)Payload$/,
|
|
27
|
+
deletePayload: /^Delete(.+)Payload$/,
|
|
28
|
+
// Mutation name patterns (camelCase)
|
|
29
|
+
createMutation: /^create([A-Z][a-zA-Z0-9]*)$/,
|
|
30
|
+
updateMutation: /^update([A-Z][a-zA-Z0-9]*)$/,
|
|
31
|
+
deleteMutation: /^delete([A-Z][a-zA-Z0-9]*)$/,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Built-in GraphQL types to ignore
|
|
35
|
+
*/
|
|
36
|
+
const BUILTIN_TYPES = new Set([
|
|
37
|
+
'Query',
|
|
38
|
+
'Mutation',
|
|
39
|
+
'Subscription',
|
|
40
|
+
'String',
|
|
41
|
+
'Int',
|
|
42
|
+
'Float',
|
|
43
|
+
'Boolean',
|
|
44
|
+
'ID',
|
|
45
|
+
// PostGraphile built-in types
|
|
46
|
+
'Node',
|
|
47
|
+
'PageInfo',
|
|
48
|
+
'Cursor',
|
|
49
|
+
'UUID',
|
|
50
|
+
'Datetime',
|
|
51
|
+
'Date',
|
|
52
|
+
'Time',
|
|
53
|
+
'JSON',
|
|
54
|
+
'BigInt',
|
|
55
|
+
'BigFloat',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Types that start with __ are internal GraphQL types
|
|
59
|
+
*/
|
|
60
|
+
function isInternalType(name) {
|
|
61
|
+
return name.startsWith('__');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Infer CleanTable[] from GraphQL introspection by recognizing PostGraphile patterns
|
|
65
|
+
*
|
|
66
|
+
* @param introspection - Standard GraphQL introspection response
|
|
67
|
+
* @param options - Optional configuration
|
|
68
|
+
* @returns Array of CleanTable objects compatible with existing generators
|
|
69
|
+
*/
|
|
70
|
+
function inferTablesFromIntrospection(introspection, options = {}) {
|
|
71
|
+
const { __schema: schema } = introspection;
|
|
72
|
+
const { types, queryType, mutationType } = schema;
|
|
73
|
+
// Build lookup maps for efficient access
|
|
74
|
+
const typeMap = buildTypeMap(types);
|
|
75
|
+
const queryFields = getTypeFields(typeMap.get(queryType.name));
|
|
76
|
+
const mutationFields = mutationType
|
|
77
|
+
? getTypeFields(typeMap.get(mutationType.name))
|
|
78
|
+
: [];
|
|
79
|
+
// Step 1: Detect entity types by finding Connection types
|
|
80
|
+
const entityNames = detectEntityTypes(types);
|
|
81
|
+
// Step 2: Build CleanTable for each entity
|
|
82
|
+
const tables = [];
|
|
83
|
+
for (const entityName of entityNames) {
|
|
84
|
+
const entityType = typeMap.get(entityName);
|
|
85
|
+
if (!entityType)
|
|
86
|
+
continue;
|
|
87
|
+
// Infer all metadata for this entity
|
|
88
|
+
const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields);
|
|
89
|
+
// Only include tables that have at least one real operation
|
|
90
|
+
if (hasRealOperation) {
|
|
91
|
+
tables.push(table);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return tables;
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Entity Detection
|
|
98
|
+
// ============================================================================
|
|
99
|
+
/**
|
|
100
|
+
* Detect entity types by finding Connection types in the schema
|
|
101
|
+
*
|
|
102
|
+
* PostGraphile generates a {PluralName}Connection type for each table.
|
|
103
|
+
* From this, we can derive the entity type name.
|
|
104
|
+
*/
|
|
105
|
+
function detectEntityTypes(types) {
|
|
106
|
+
const entityNames = new Set();
|
|
107
|
+
const typeNames = new Set(types.map((t) => t.name));
|
|
108
|
+
for (const type of types) {
|
|
109
|
+
// Skip internal types
|
|
110
|
+
if (isInternalType(type.name))
|
|
111
|
+
continue;
|
|
112
|
+
if (BUILTIN_TYPES.has(type.name))
|
|
113
|
+
continue;
|
|
114
|
+
// Check for Connection pattern
|
|
115
|
+
const connectionMatch = type.name.match(PATTERNS.connection);
|
|
116
|
+
if (connectionMatch) {
|
|
117
|
+
const pluralName = connectionMatch[1]; // e.g., "Users" from "UsersConnection"
|
|
118
|
+
const singularName = (0, inflekt_1.singularize)(pluralName); // e.g., "User"
|
|
119
|
+
// Verify the entity type actually exists
|
|
120
|
+
if (typeNames.has(singularName)) {
|
|
121
|
+
entityNames.add(singularName);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return entityNames;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Build a complete CleanTable from an entity type
|
|
129
|
+
*/
|
|
130
|
+
function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields) {
|
|
131
|
+
// Extract scalar fields from entity type
|
|
132
|
+
const fields = extractEntityFields(entityType, typeMap);
|
|
133
|
+
// Infer relations from entity fields
|
|
134
|
+
const relations = inferRelations(entityType, typeMap);
|
|
135
|
+
// Match query and mutation operations
|
|
136
|
+
const queryOps = matchQueryOperations(entityName, queryFields, typeMap);
|
|
137
|
+
const mutationOps = matchMutationOperations(entityName, mutationFields);
|
|
138
|
+
// Check if we found at least one real operation (not a fallback)
|
|
139
|
+
const hasRealOperation = !!(queryOps.all ||
|
|
140
|
+
queryOps.one ||
|
|
141
|
+
mutationOps.create ||
|
|
142
|
+
mutationOps.update ||
|
|
143
|
+
mutationOps.delete);
|
|
144
|
+
// Infer primary key from mutation inputs
|
|
145
|
+
const constraints = inferConstraints(entityName, typeMap);
|
|
146
|
+
// Build inflection map from discovered types
|
|
147
|
+
const inflection = buildInflection(entityName, typeMap);
|
|
148
|
+
// Combine query operations with fallbacks for UI purposes
|
|
149
|
+
// (but hasRealOperation indicates if we should include this table)
|
|
150
|
+
const query = {
|
|
151
|
+
all: queryOps.all ?? (0, inflekt_1.lcFirst)((0, inflekt_1.pluralize)(entityName)),
|
|
152
|
+
one: queryOps.one ?? (0, inflekt_1.lcFirst)(entityName),
|
|
153
|
+
create: mutationOps.create ?? `create${entityName}`,
|
|
154
|
+
update: mutationOps.update,
|
|
155
|
+
delete: mutationOps.delete,
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
table: {
|
|
159
|
+
name: entityName,
|
|
160
|
+
fields,
|
|
161
|
+
relations,
|
|
162
|
+
inflection,
|
|
163
|
+
query,
|
|
164
|
+
constraints,
|
|
165
|
+
},
|
|
166
|
+
hasRealOperation,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Field Extraction
|
|
171
|
+
// ============================================================================
|
|
172
|
+
/**
|
|
173
|
+
* Extract scalar fields from an entity type
|
|
174
|
+
* Excludes relation fields (those returning other entity types or connections)
|
|
175
|
+
*/
|
|
176
|
+
function extractEntityFields(entityType, typeMap) {
|
|
177
|
+
const fields = [];
|
|
178
|
+
if (!entityType.fields)
|
|
179
|
+
return fields;
|
|
180
|
+
for (const field of entityType.fields) {
|
|
181
|
+
const baseTypeName = (0, introspection_1.getBaseTypeName)(field.type);
|
|
182
|
+
if (!baseTypeName)
|
|
183
|
+
continue;
|
|
184
|
+
// Skip relation fields (those returning other objects or connections)
|
|
185
|
+
const fieldType = typeMap.get(baseTypeName);
|
|
186
|
+
if (fieldType?.kind === 'OBJECT') {
|
|
187
|
+
// Check if it's a Connection type (hasMany) or entity type (belongsTo)
|
|
188
|
+
if (baseTypeName.endsWith('Connection') ||
|
|
189
|
+
isEntityType(baseTypeName, typeMap)) {
|
|
190
|
+
continue; // Skip relation fields
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Include scalar, enum, and other non-relation fields
|
|
194
|
+
fields.push({
|
|
195
|
+
name: field.name,
|
|
196
|
+
type: convertToCleanFieldType(field.type),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return fields;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if a type name is an entity type (has a corresponding Connection)
|
|
203
|
+
*/
|
|
204
|
+
function isEntityType(typeName, typeMap) {
|
|
205
|
+
const connectionName = `${(0, inflekt_1.pluralize)(typeName)}Connection`;
|
|
206
|
+
return typeMap.has(connectionName);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Convert IntrospectionTypeRef to CleanFieldType
|
|
210
|
+
*/
|
|
211
|
+
function convertToCleanFieldType(typeRef) {
|
|
212
|
+
const baseType = (0, introspection_1.unwrapType)(typeRef);
|
|
213
|
+
const isArray = (0, introspection_1.isList)(typeRef);
|
|
214
|
+
return {
|
|
215
|
+
gqlType: baseType.name ?? 'Unknown',
|
|
216
|
+
isArray,
|
|
217
|
+
// PostgreSQL-specific fields are not available from introspection
|
|
218
|
+
// They were optional anyway and not used by generators
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Relation Inference
|
|
223
|
+
// ============================================================================
|
|
224
|
+
/**
|
|
225
|
+
* Infer relations from entity type fields
|
|
226
|
+
*/
|
|
227
|
+
function inferRelations(entityType, typeMap) {
|
|
228
|
+
const belongsTo = [];
|
|
229
|
+
const hasMany = [];
|
|
230
|
+
const manyToMany = [];
|
|
231
|
+
if (!entityType.fields) {
|
|
232
|
+
return { belongsTo, hasOne: [], hasMany, manyToMany };
|
|
233
|
+
}
|
|
234
|
+
for (const field of entityType.fields) {
|
|
235
|
+
const baseTypeName = (0, introspection_1.getBaseTypeName)(field.type);
|
|
236
|
+
if (!baseTypeName)
|
|
237
|
+
continue;
|
|
238
|
+
// Check for Connection type → hasMany or manyToMany
|
|
239
|
+
if (baseTypeName.endsWith('Connection')) {
|
|
240
|
+
const relation = inferHasManyOrManyToMany(field, baseTypeName, typeMap);
|
|
241
|
+
if (relation.type === 'manyToMany') {
|
|
242
|
+
manyToMany.push(relation.relation);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
hasMany.push(relation.relation);
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
// Check for entity type → belongsTo
|
|
250
|
+
if (isEntityType(baseTypeName, typeMap)) {
|
|
251
|
+
belongsTo.push({
|
|
252
|
+
fieldName: field.name,
|
|
253
|
+
isUnique: false, // Can't determine from introspection alone
|
|
254
|
+
referencesTable: baseTypeName,
|
|
255
|
+
type: baseTypeName,
|
|
256
|
+
keys: [], // Would need FK info to populate
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { belongsTo, hasOne: [], hasMany, manyToMany };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Determine if a Connection field is hasMany or manyToMany
|
|
264
|
+
*
|
|
265
|
+
* ManyToMany pattern: field name contains "By" and "And"
|
|
266
|
+
* e.g., "productsByOrderItemOrderIdAndProductId"
|
|
267
|
+
*/
|
|
268
|
+
function inferHasManyOrManyToMany(field, connectionTypeName, typeMap) {
|
|
269
|
+
// Extract the related entity name from Connection type
|
|
270
|
+
const match = connectionTypeName.match(PATTERNS.connection);
|
|
271
|
+
const relatedPluralName = match ? match[1] : connectionTypeName;
|
|
272
|
+
const relatedEntityName = (0, inflekt_1.singularize)(relatedPluralName);
|
|
273
|
+
// Check for manyToMany pattern in field name
|
|
274
|
+
const isManyToMany = field.name.includes('By') && field.name.includes('And');
|
|
275
|
+
if (isManyToMany) {
|
|
276
|
+
// For ManyToMany, extract the actual entity name from the field name prefix
|
|
277
|
+
// Field name pattern: {relatedEntities}By{JunctionTable}{Keys}
|
|
278
|
+
// e.g., "usersByMembershipActorIdAndEntityId" → "users" → "User"
|
|
279
|
+
// e.g., "productsByOrderItemOrderIdAndProductId" → "products" → "Product"
|
|
280
|
+
const prefixMatch = field.name.match(/^([a-z]+)By/i);
|
|
281
|
+
const actualEntityName = prefixMatch
|
|
282
|
+
? (0, inflekt_1.singularize)((0, inflekt_1.ucFirst)(prefixMatch[1]))
|
|
283
|
+
: relatedEntityName;
|
|
284
|
+
// Try to extract junction table from field name
|
|
285
|
+
// Pattern: {relatedEntities}By{JunctionTable}{Keys}
|
|
286
|
+
// e.g., "productsByProductCategoryProductIdAndCategoryId" → "ProductCategory"
|
|
287
|
+
// The junction table name ends where the first field key begins (identified by capital letter after lowercase)
|
|
288
|
+
const junctionMatch = field.name.match(/By([A-Z][a-z]+(?:[A-Z][a-z]+)*?)(?:[A-Z][a-z]+Id)/);
|
|
289
|
+
const junctionTable = junctionMatch ? junctionMatch[1] : 'Unknown';
|
|
290
|
+
return {
|
|
291
|
+
type: 'manyToMany',
|
|
292
|
+
relation: {
|
|
293
|
+
fieldName: field.name,
|
|
294
|
+
rightTable: actualEntityName,
|
|
295
|
+
junctionTable,
|
|
296
|
+
type: connectionTypeName,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
type: 'hasMany',
|
|
302
|
+
relation: {
|
|
303
|
+
fieldName: field.name,
|
|
304
|
+
isUnique: false,
|
|
305
|
+
referencedByTable: relatedEntityName,
|
|
306
|
+
type: connectionTypeName,
|
|
307
|
+
keys: [],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Match query operations for an entity
|
|
313
|
+
*
|
|
314
|
+
* Looks for:
|
|
315
|
+
* - List query: returns {PluralName}Connection (e.g., users → UsersConnection)
|
|
316
|
+
* - Single query: returns {EntityName} with id/nodeId arg (e.g., user → User)
|
|
317
|
+
*/
|
|
318
|
+
function matchQueryOperations(entityName, queryFields, typeMap) {
|
|
319
|
+
const pluralName = (0, inflekt_1.pluralize)(entityName);
|
|
320
|
+
const connectionTypeName = `${pluralName}Connection`;
|
|
321
|
+
let all = null;
|
|
322
|
+
let one = null;
|
|
323
|
+
for (const field of queryFields) {
|
|
324
|
+
const returnTypeName = (0, introspection_1.getBaseTypeName)(field.type);
|
|
325
|
+
if (!returnTypeName)
|
|
326
|
+
continue;
|
|
327
|
+
// Match list query by return type (Connection)
|
|
328
|
+
if (returnTypeName === connectionTypeName) {
|
|
329
|
+
// Prefer the simple plural name, but accept any that returns the connection
|
|
330
|
+
if (!all || field.name === (0, inflekt_1.lcFirst)(pluralName)) {
|
|
331
|
+
all = field.name;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Match single query by return type (Entity) and having an id-like arg
|
|
335
|
+
if (returnTypeName === entityName) {
|
|
336
|
+
const hasIdArg = field.args.some((arg) => arg.name === 'id' ||
|
|
337
|
+
arg.name === 'nodeId' ||
|
|
338
|
+
arg.name.toLowerCase().endsWith('id'));
|
|
339
|
+
if (hasIdArg) {
|
|
340
|
+
// Prefer exact match (e.g., "user" for "User")
|
|
341
|
+
if (!one || field.name === (0, inflekt_1.lcFirst)(entityName)) {
|
|
342
|
+
one = field.name;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { all, one };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Match mutation operations for an entity
|
|
351
|
+
*
|
|
352
|
+
* Looks for mutations named:
|
|
353
|
+
* - create{EntityName}
|
|
354
|
+
* - update{EntityName} or update{EntityName}ById
|
|
355
|
+
* - delete{EntityName} or delete{EntityName}ById
|
|
356
|
+
*/
|
|
357
|
+
function matchMutationOperations(entityName, mutationFields) {
|
|
358
|
+
let create = null;
|
|
359
|
+
let update = null;
|
|
360
|
+
let del = null;
|
|
361
|
+
const expectedCreate = `create${entityName}`;
|
|
362
|
+
const expectedUpdate = `update${entityName}`;
|
|
363
|
+
const expectedDelete = `delete${entityName}`;
|
|
364
|
+
for (const field of mutationFields) {
|
|
365
|
+
// Exact match for create
|
|
366
|
+
if (field.name === expectedCreate) {
|
|
367
|
+
create = field.name;
|
|
368
|
+
}
|
|
369
|
+
// Match update (could be updateUser or updateUserById)
|
|
370
|
+
if (field.name === expectedUpdate ||
|
|
371
|
+
field.name === `${expectedUpdate}ById`) {
|
|
372
|
+
// Prefer non-ById version
|
|
373
|
+
if (!update || field.name === expectedUpdate) {
|
|
374
|
+
update = field.name;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Match delete (could be deleteUser or deleteUserById)
|
|
378
|
+
if (field.name === expectedDelete ||
|
|
379
|
+
field.name === `${expectedDelete}ById`) {
|
|
380
|
+
// Prefer non-ById version
|
|
381
|
+
if (!del || field.name === expectedDelete) {
|
|
382
|
+
del = field.name;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { create, update, delete: del };
|
|
387
|
+
}
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// Constraint Inference
|
|
390
|
+
// ============================================================================
|
|
391
|
+
/**
|
|
392
|
+
* Infer constraints from mutation input types
|
|
393
|
+
*
|
|
394
|
+
* Primary key can be inferred from Update/Delete mutation input types,
|
|
395
|
+
* which typically have an 'id' field or similar.
|
|
396
|
+
*/
|
|
397
|
+
function inferConstraints(entityName, typeMap) {
|
|
398
|
+
const primaryKey = [];
|
|
399
|
+
// Try to find Update or Delete input type to extract PK
|
|
400
|
+
const updateInputName = `Update${entityName}Input`;
|
|
401
|
+
const deleteInputName = `Delete${entityName}Input`;
|
|
402
|
+
const updateInput = typeMap.get(updateInputName);
|
|
403
|
+
const deleteInput = typeMap.get(deleteInputName);
|
|
404
|
+
// Check update input for id field
|
|
405
|
+
const inputToCheck = updateInput || deleteInput;
|
|
406
|
+
if (inputToCheck?.inputFields) {
|
|
407
|
+
const idField = inputToCheck.inputFields.find((f) => f.name === 'id' || f.name === 'nodeId');
|
|
408
|
+
if (idField) {
|
|
409
|
+
primaryKey.push({
|
|
410
|
+
name: 'primary',
|
|
411
|
+
fields: [
|
|
412
|
+
{
|
|
413
|
+
name: idField.name,
|
|
414
|
+
type: convertToCleanFieldType(idField.type),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// If no PK found from inputs, try to find 'id' field in entity type
|
|
421
|
+
if (primaryKey.length === 0) {
|
|
422
|
+
const entityType = typeMap.get(entityName);
|
|
423
|
+
if (entityType?.fields) {
|
|
424
|
+
const idField = entityType.fields.find((f) => f.name === 'id' || f.name === 'nodeId');
|
|
425
|
+
if (idField) {
|
|
426
|
+
primaryKey.push({
|
|
427
|
+
name: 'primary',
|
|
428
|
+
fields: [
|
|
429
|
+
{
|
|
430
|
+
name: idField.name,
|
|
431
|
+
type: convertToCleanFieldType(idField.type),
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
primaryKey,
|
|
440
|
+
foreignKey: [], // Would need FK info to populate
|
|
441
|
+
unique: [], // Would need constraint info to populate
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// ============================================================================
|
|
445
|
+
// Inflection Building
|
|
446
|
+
// ============================================================================
|
|
447
|
+
/**
|
|
448
|
+
* Build inflection map from discovered types
|
|
449
|
+
*/
|
|
450
|
+
function buildInflection(entityName, typeMap) {
|
|
451
|
+
const pluralName = (0, inflekt_1.pluralize)(entityName);
|
|
452
|
+
const singularFieldName = (0, inflekt_1.lcFirst)(entityName);
|
|
453
|
+
const pluralFieldName = (0, inflekt_1.lcFirst)(pluralName);
|
|
454
|
+
// Check which types actually exist in the schema
|
|
455
|
+
const hasFilter = typeMap.has(`${entityName}Filter`);
|
|
456
|
+
const hasPatch = typeMap.has(`${entityName}Patch`);
|
|
457
|
+
const hasUpdatePayload = typeMap.has(`Update${entityName}Payload`);
|
|
458
|
+
// Detect the actual OrderBy type from schema
|
|
459
|
+
// PostGraphile typically generates {PluralName}OrderBy (e.g., AddressesOrderBy)
|
|
460
|
+
// but we check for the actual type in case of custom inflection
|
|
461
|
+
const expectedOrderByType = `${pluralName}OrderBy`;
|
|
462
|
+
const orderByType = findOrderByType(entityName, pluralName, typeMap) || expectedOrderByType;
|
|
463
|
+
return {
|
|
464
|
+
allRows: pluralFieldName,
|
|
465
|
+
allRowsSimple: pluralFieldName,
|
|
466
|
+
conditionType: `${entityName}Condition`,
|
|
467
|
+
connection: `${pluralName}Connection`,
|
|
468
|
+
createField: `create${entityName}`,
|
|
469
|
+
createInputType: `Create${entityName}Input`,
|
|
470
|
+
createPayloadType: `Create${entityName}Payload`,
|
|
471
|
+
deleteByPrimaryKey: `delete${entityName}`,
|
|
472
|
+
deletePayloadType: `Delete${entityName}Payload`,
|
|
473
|
+
edge: `${pluralName}Edge`,
|
|
474
|
+
edgeField: (0, inflekt_1.lcFirst)(pluralName),
|
|
475
|
+
enumType: `${entityName}Enum`,
|
|
476
|
+
filterType: hasFilter ? `${entityName}Filter` : null,
|
|
477
|
+
inputType: `${entityName}Input`,
|
|
478
|
+
orderByType,
|
|
479
|
+
patchField: singularFieldName,
|
|
480
|
+
patchType: hasPatch ? `${entityName}Patch` : null,
|
|
481
|
+
tableFieldName: singularFieldName,
|
|
482
|
+
tableType: entityName,
|
|
483
|
+
typeName: entityName,
|
|
484
|
+
updateByPrimaryKey: `update${entityName}`,
|
|
485
|
+
updatePayloadType: hasUpdatePayload ? `Update${entityName}Payload` : null,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Find the actual OrderBy enum type for an entity from the schema
|
|
490
|
+
*
|
|
491
|
+
* PostGraphile generates OrderBy enums with various patterns:
|
|
492
|
+
* - {PluralName}OrderBy (e.g., AddressesOrderBy, UsersOrderBy)
|
|
493
|
+
* - Sometimes with custom inflection (e.g., SchemataOrderBy for Schema)
|
|
494
|
+
*
|
|
495
|
+
* We search for the actual type in the schema to handle all cases.
|
|
496
|
+
*/
|
|
497
|
+
function findOrderByType(entityName, pluralName, typeMap) {
|
|
498
|
+
// Try the standard pattern first: {PluralName}OrderBy
|
|
499
|
+
const standardName = `${pluralName}OrderBy`;
|
|
500
|
+
if (typeMap.has(standardName)) {
|
|
501
|
+
return standardName;
|
|
502
|
+
}
|
|
503
|
+
// Build a list of candidate OrderBy names to check
|
|
504
|
+
// These are variations of the entity name with common plural suffixes
|
|
505
|
+
const candidates = [
|
|
506
|
+
`${entityName}sOrderBy`, // Simple 's' plural: User -> UsersOrderBy
|
|
507
|
+
`${entityName}esOrderBy`, // 'es' plural: Address -> AddressesOrderBy
|
|
508
|
+
`${entityName}OrderBy`, // No change (already plural or singular OK)
|
|
509
|
+
];
|
|
510
|
+
// Check each candidate
|
|
511
|
+
for (const candidate of candidates) {
|
|
512
|
+
if (typeMap.has(candidate) && typeMap.get(candidate)?.kind === 'ENUM') {
|
|
513
|
+
return candidate;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Fallback: search for an enum that EXACTLY matches the entity plural pattern
|
|
517
|
+
// We look for types that are the pluralized entity name + OrderBy
|
|
518
|
+
// This avoids matching SchemaGrantsOrderBy when looking for Schema's OrderBy
|
|
519
|
+
for (const [typeName, type] of typeMap) {
|
|
520
|
+
if (type.kind !== 'ENUM' || !typeName.endsWith('OrderBy'))
|
|
521
|
+
continue;
|
|
522
|
+
// Extract the base name (without OrderBy suffix)
|
|
523
|
+
const baseName = typeName.slice(0, -7); // Remove 'OrderBy'
|
|
524
|
+
// Check if singularizing the base name gives us the entity name
|
|
525
|
+
// e.g., 'Schemata' -> 'Schema', 'Users' -> 'User', 'Addresses' -> 'Address'
|
|
526
|
+
if ((0, inflekt_1.singularize)(baseName) === entityName) {
|
|
527
|
+
return typeName;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// Utility Functions
|
|
534
|
+
// ============================================================================
|
|
535
|
+
/**
|
|
536
|
+
* Build a map of type name → IntrospectionType for efficient lookup
|
|
537
|
+
*/
|
|
538
|
+
function buildTypeMap(types) {
|
|
539
|
+
const map = new Map();
|
|
540
|
+
for (const type of types) {
|
|
541
|
+
map.set(type.name, type);
|
|
542
|
+
}
|
|
543
|
+
return map;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Get fields from a type, returning empty array if null
|
|
547
|
+
*/
|
|
548
|
+
function getTypeFields(type) {
|
|
549
|
+
return type?.fields ?? [];
|
|
550
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint Schema Source
|
|
3
|
+
*
|
|
4
|
+
* Fetches GraphQL schema via introspection from a live endpoint.
|
|
5
|
+
* Wraps the existing fetchSchema() function with the SchemaSource interface.
|
|
6
|
+
*/
|
|
7
|
+
import type { SchemaSource, SchemaSourceResult } from './types';
|
|
8
|
+
export interface EndpointSchemaSourceOptions {
|
|
9
|
+
/**
|
|
10
|
+
* GraphQL endpoint URL
|
|
11
|
+
*/
|
|
12
|
+
endpoint: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional authorization header value (e.g., "Bearer token")
|
|
15
|
+
*/
|
|
16
|
+
authorization?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional additional headers
|
|
19
|
+
*/
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Request timeout in milliseconds (default: 30000)
|
|
23
|
+
*/
|
|
24
|
+
timeout?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Schema source that fetches from a live GraphQL endpoint
|
|
28
|
+
*/
|
|
29
|
+
export declare class EndpointSchemaSource implements SchemaSource {
|
|
30
|
+
private readonly options;
|
|
31
|
+
constructor(options: EndpointSchemaSourceOptions);
|
|
32
|
+
fetch(): Promise<SchemaSourceResult>;
|
|
33
|
+
describe(): string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EndpointSchemaSource = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
const fetch_schema_1 = require("../fetch-schema");
|
|
6
|
+
/**
|
|
7
|
+
* Schema source that fetches from a live GraphQL endpoint
|
|
8
|
+
*/
|
|
9
|
+
class EndpointSchemaSource {
|
|
10
|
+
options;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
async fetch() {
|
|
15
|
+
const result = await (0, fetch_schema_1.fetchSchema)({
|
|
16
|
+
endpoint: this.options.endpoint,
|
|
17
|
+
authorization: this.options.authorization,
|
|
18
|
+
headers: this.options.headers,
|
|
19
|
+
timeout: this.options.timeout,
|
|
20
|
+
});
|
|
21
|
+
if (!result.success) {
|
|
22
|
+
throw new types_1.SchemaSourceError(result.error ?? 'Unknown error fetching schema', this.describe());
|
|
23
|
+
}
|
|
24
|
+
if (!result.data) {
|
|
25
|
+
throw new types_1.SchemaSourceError('No introspection data returned', this.describe());
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
introspection: result.data,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
describe() {
|
|
32
|
+
return `endpoint: ${this.options.endpoint}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.EndpointSchemaSource = EndpointSchemaSource;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SchemaSource, SchemaSourceResult } from './types';
|
|
2
|
+
export interface FileSchemaSourceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Path to the GraphQL schema file (.graphql)
|
|
5
|
+
* Can be absolute or relative to current working directory
|
|
6
|
+
*/
|
|
7
|
+
schemaPath: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Schema source that loads from a local GraphQL schema file
|
|
11
|
+
*
|
|
12
|
+
* Supports .graphql files containing SDL (Schema Definition Language).
|
|
13
|
+
* The SDL is parsed using graphql-js and converted to introspection format.
|
|
14
|
+
*/
|
|
15
|
+
export declare class FileSchemaSource implements SchemaSource {
|
|
16
|
+
private readonly options;
|
|
17
|
+
constructor(options: FileSchemaSourceOptions);
|
|
18
|
+
fetch(): Promise<SchemaSourceResult>;
|
|
19
|
+
describe(): string;
|
|
20
|
+
}
|