@constructive-io/graphql-codegen 3.3.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/core/ast.js +6 -5
- package/core/codegen/custom-mutations.js +22 -22
- package/core/codegen/custom-queries.js +24 -17
- package/core/codegen/hooks-ast.d.ts +1 -1
- package/core/codegen/hooks-ast.js +22 -5
- package/core/codegen/index.d.ts +1 -1
- package/core/codegen/mutations.js +16 -12
- package/core/codegen/orm/custom-ops-generator.js +37 -3
- package/core/codegen/orm/input-types-generator.js +161 -89
- package/core/codegen/orm/model-generator.js +72 -73
- package/core/codegen/orm/select-types.d.ts +27 -17
- package/core/codegen/queries.js +37 -25
- package/core/codegen/schema-types-generator.js +21 -0
- package/core/codegen/templates/hooks-selection.ts +12 -0
- package/core/codegen/templates/query-builder.ts +103 -59
- package/core/codegen/templates/select-types.ts +59 -33
- package/core/codegen/types.js +26 -0
- package/core/codegen/utils.d.ts +1 -1
- package/core/codegen/utils.js +1 -1
- package/core/custom-ast.js +9 -8
- package/core/database/index.js +2 -3
- package/core/index.d.ts +2 -0
- package/core/index.js +2 -0
- package/core/introspect/infer-tables.js +144 -58
- package/core/introspect/transform-schema.d.ts +1 -1
- package/core/introspect/transform-schema.js +3 -1
- package/esm/core/ast.js +6 -5
- package/esm/core/codegen/custom-mutations.js +23 -23
- package/esm/core/codegen/custom-queries.js +25 -18
- package/esm/core/codegen/hooks-ast.d.ts +1 -1
- package/esm/core/codegen/hooks-ast.js +22 -5
- package/esm/core/codegen/index.d.ts +1 -1
- package/esm/core/codegen/mutations.js +16 -12
- package/esm/core/codegen/orm/custom-ops-generator.js +38 -4
- package/esm/core/codegen/orm/input-types-generator.js +163 -91
- package/esm/core/codegen/orm/model-generator.js +73 -74
- package/esm/core/codegen/orm/select-types.d.ts +27 -17
- package/esm/core/codegen/queries.js +37 -25
- package/esm/core/codegen/schema-types-generator.js +21 -0
- package/esm/core/codegen/types.js +26 -0
- package/esm/core/codegen/utils.d.ts +1 -1
- package/esm/core/codegen/utils.js +1 -1
- package/esm/core/custom-ast.js +9 -8
- package/esm/core/database/index.js +2 -3
- package/esm/core/index.d.ts +2 -0
- package/esm/core/index.js +2 -0
- package/esm/core/introspect/infer-tables.js +144 -58
- package/esm/core/introspect/transform-schema.d.ts +1 -1
- package/esm/core/introspect/transform-schema.js +3 -1
- package/esm/generators/field-selector.js +1 -0
- package/esm/generators/index.d.ts +3 -0
- package/esm/generators/index.js +3 -0
- package/esm/generators/mutations.js +4 -4
- package/esm/generators/select.js +4 -4
- package/esm/index.d.ts +1 -1
- package/esm/index.js +1 -1
- package/esm/types/schema.d.ts +5 -3
- package/generators/field-selector.js +1 -0
- package/generators/index.d.ts +3 -0
- package/generators/index.js +3 -0
- package/generators/mutations.js +3 -3
- package/generators/select.js +3 -3
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +11 -11
- package/types/schema.d.ts +5 -3
package/core/codegen/types.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.generateTypesFile = generateTypesFile;
|
|
|
38
38
|
* Types generator - generates types.ts with entity interfaces using Babel AST
|
|
39
39
|
*/
|
|
40
40
|
const t = __importStar(require("@babel/types"));
|
|
41
|
+
const scalars_1 = require("./scalars");
|
|
41
42
|
const babel_ast_1 = require("./babel-ast");
|
|
42
43
|
const utils_1 = require("./utils");
|
|
43
44
|
/** All filter type configurations - scalar and list filters */
|
|
@@ -187,12 +188,34 @@ function createInterfaceDeclaration(name, properties) {
|
|
|
187
188
|
const interfaceDecl = t.tsInterfaceDeclaration(t.identifier(name), null, null, t.tsInterfaceBody(props));
|
|
188
189
|
return t.exportNamedDeclaration(interfaceDecl);
|
|
189
190
|
}
|
|
191
|
+
function createTypeAlias(name, typeNode) {
|
|
192
|
+
const typeAlias = t.tsTypeAliasDeclaration(t.identifier(name), null, typeNode);
|
|
193
|
+
return t.exportNamedDeclaration(typeAlias);
|
|
194
|
+
}
|
|
195
|
+
function collectCustomScalarTypes(tables, excludedTypeNames) {
|
|
196
|
+
const customScalarTypes = new Set();
|
|
197
|
+
const tableTypeNames = new Set(tables.map((table) => table.name));
|
|
198
|
+
for (const table of tables) {
|
|
199
|
+
for (const field of (0, utils_1.getScalarFields)(table)) {
|
|
200
|
+
const cleanType = field.type.gqlType.replace(/!/g, '');
|
|
201
|
+
if (scalars_1.SCALAR_NAMES.has(cleanType))
|
|
202
|
+
continue;
|
|
203
|
+
if (excludedTypeNames.has(cleanType))
|
|
204
|
+
continue;
|
|
205
|
+
if (tableTypeNames.has(cleanType))
|
|
206
|
+
continue;
|
|
207
|
+
customScalarTypes.add(cleanType);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return Array.from(customScalarTypes).sort();
|
|
211
|
+
}
|
|
190
212
|
/**
|
|
191
213
|
* Generate types.ts content with all entity interfaces and base filter types
|
|
192
214
|
*/
|
|
193
215
|
function generateTypesFile(tables, options = {}) {
|
|
194
216
|
const { enumsFromSchemaTypes = [] } = options;
|
|
195
217
|
const enumSet = new Set(enumsFromSchemaTypes);
|
|
218
|
+
const customScalarTypes = collectCustomScalarTypes(tables, enumSet);
|
|
196
219
|
const statements = [];
|
|
197
220
|
// Collect which enums are actually used by entity fields
|
|
198
221
|
const usedEnums = new Set();
|
|
@@ -215,6 +238,9 @@ function generateTypesFile(tables, options = {}) {
|
|
|
215
238
|
importDecl.importKind = 'type';
|
|
216
239
|
statements.push(importDecl);
|
|
217
240
|
}
|
|
241
|
+
for (const scalarType of customScalarTypes) {
|
|
242
|
+
statements.push(createTypeAlias(scalarType, t.tsUnknownKeyword()));
|
|
243
|
+
}
|
|
218
244
|
// Generate entity interfaces
|
|
219
245
|
for (const table of tables) {
|
|
220
246
|
const scalarFields = (0, utils_1.getScalarFields)(table);
|
package/core/codegen/utils.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export declare function getUpdateMutationFileName(table: CleanTable): string;
|
|
|
72
72
|
export declare function getDeleteMutationFileName(table: CleanTable): string;
|
|
73
73
|
/**
|
|
74
74
|
* Get the GraphQL query name for fetching all rows
|
|
75
|
-
* Uses inflection from
|
|
75
|
+
* Uses inflection from introspection, falls back to convention
|
|
76
76
|
*/
|
|
77
77
|
export declare function getAllRowsQueryName(table: CleanTable): string;
|
|
78
78
|
/**
|
package/core/codegen/utils.js
CHANGED
|
@@ -169,7 +169,7 @@ function getDeleteMutationFileName(table) {
|
|
|
169
169
|
// ============================================================================
|
|
170
170
|
/**
|
|
171
171
|
* Get the GraphQL query name for fetching all rows
|
|
172
|
-
* Uses inflection from
|
|
172
|
+
* Uses inflection from introspection, falls back to convention
|
|
173
173
|
*/
|
|
174
174
|
function getAllRowsQueryName(table) {
|
|
175
175
|
return (table.query?.all ||
|
package/core/custom-ast.js
CHANGED
|
@@ -42,6 +42,7 @@ exports.geometryAst = geometryAst;
|
|
|
42
42
|
exports.intervalAst = intervalAst;
|
|
43
43
|
exports.isIntervalType = isIntervalType;
|
|
44
44
|
const t = __importStar(require("gql-ast"));
|
|
45
|
+
const graphql_1 = require("graphql");
|
|
45
46
|
/**
|
|
46
47
|
* Get custom AST for MetaField type - handles PostgreSQL types that need subfield selections
|
|
47
48
|
*/
|
|
@@ -119,28 +120,28 @@ function geometryPointAst(name) {
|
|
|
119
120
|
function geometryCollectionAst(name) {
|
|
120
121
|
// Manually create inline fragment since gql-ast doesn't support it
|
|
121
122
|
const inlineFragment = {
|
|
122
|
-
kind:
|
|
123
|
+
kind: graphql_1.Kind.INLINE_FRAGMENT,
|
|
123
124
|
typeCondition: {
|
|
124
|
-
kind:
|
|
125
|
+
kind: graphql_1.Kind.NAMED_TYPE,
|
|
125
126
|
name: {
|
|
126
|
-
kind:
|
|
127
|
+
kind: graphql_1.Kind.NAME,
|
|
127
128
|
value: 'GeometryPoint',
|
|
128
129
|
},
|
|
129
130
|
},
|
|
130
131
|
selectionSet: {
|
|
131
|
-
kind:
|
|
132
|
+
kind: graphql_1.Kind.SELECTION_SET,
|
|
132
133
|
selections: [
|
|
133
134
|
{
|
|
134
|
-
kind:
|
|
135
|
+
kind: graphql_1.Kind.FIELD,
|
|
135
136
|
name: {
|
|
136
|
-
kind:
|
|
137
|
+
kind: graphql_1.Kind.NAME,
|
|
137
138
|
value: 'x',
|
|
138
139
|
},
|
|
139
140
|
},
|
|
140
141
|
{
|
|
141
|
-
kind:
|
|
142
|
+
kind: graphql_1.Kind.FIELD,
|
|
142
143
|
name: {
|
|
143
|
-
kind:
|
|
144
|
+
kind: graphql_1.Kind.NAME,
|
|
144
145
|
value: 'y',
|
|
145
146
|
},
|
|
146
147
|
},
|
package/core/database/index.js
CHANGED
|
@@ -56,11 +56,10 @@ async function buildSchemaFromDatabase(options) {
|
|
|
56
56
|
const { database, schemas, outDir, filename = 'schema.graphql' } = options;
|
|
57
57
|
// Ensure output directory exists
|
|
58
58
|
await fs.promises.mkdir(outDir, { recursive: true });
|
|
59
|
-
// Build schema SDL from database
|
|
59
|
+
// Build schema SDL from database (PostGraphile v5 preset-driven settings)
|
|
60
60
|
const sdl = await (0, graphql_server_1.buildSchemaSDL)({
|
|
61
61
|
database,
|
|
62
62
|
schemas,
|
|
63
|
-
graphile: { pgSettings: async () => ({ role: 'administrator' }) },
|
|
64
63
|
});
|
|
65
64
|
// Write schema to file
|
|
66
65
|
const schemaPath = path.join(outDir, filename);
|
|
@@ -77,9 +76,9 @@ async function buildSchemaFromDatabase(options) {
|
|
|
77
76
|
*/
|
|
78
77
|
async function buildSchemaSDLFromDatabase(options) {
|
|
79
78
|
const { database, schemas } = options;
|
|
79
|
+
// PostGraphile v5 resolves role/settings via preset configuration.
|
|
80
80
|
return (0, graphql_server_1.buildSchemaSDL)({
|
|
81
81
|
database,
|
|
82
82
|
schemas,
|
|
83
|
-
graphile: { pgSettings: async () => ({ role: 'administrator' }) },
|
|
84
83
|
});
|
|
85
84
|
}
|
package/core/index.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export { generate } from './generate';
|
|
|
9
9
|
export * from './types';
|
|
10
10
|
export * from './ast';
|
|
11
11
|
export * from './custom-ast';
|
|
12
|
+
/** @deprecated Legacy v4 query builder — use v5 ORM codegen instead */
|
|
12
13
|
export { MetaObject, QueryBuilder } from './query-builder';
|
|
14
|
+
/** @deprecated Legacy v4 meta-object utilities — v5 uses standard introspection */
|
|
13
15
|
export { convertFromMetaSchema, validateMetaObject } from './meta-object';
|
|
14
16
|
export * from './config';
|
|
15
17
|
export * from './codegen';
|
package/core/index.js
CHANGED
|
@@ -29,10 +29,12 @@ __exportStar(require("./types"), exports);
|
|
|
29
29
|
__exportStar(require("./ast"), exports);
|
|
30
30
|
__exportStar(require("./custom-ast"), exports);
|
|
31
31
|
// Query builder
|
|
32
|
+
/** @deprecated Legacy v4 query builder — use v5 ORM codegen instead */
|
|
32
33
|
var query_builder_1 = require("./query-builder");
|
|
33
34
|
Object.defineProperty(exports, "MetaObject", { enumerable: true, get: function () { return query_builder_1.MetaObject; } });
|
|
34
35
|
Object.defineProperty(exports, "QueryBuilder", { enumerable: true, get: function () { return query_builder_1.QueryBuilder; } });
|
|
35
36
|
// Meta object utilities
|
|
37
|
+
/** @deprecated Legacy v4 meta-object utilities — v5 uses standard introspection */
|
|
36
38
|
var meta_object_1 = require("./meta-object");
|
|
37
39
|
Object.defineProperty(exports, "convertFromMetaSchema", { enumerable: true, get: function () { return meta_object_1.convertFromMetaSchema; } });
|
|
38
40
|
Object.defineProperty(exports, "validateMetaObject", { enumerable: true, get: function () { return meta_object_1.validateMetaObject; } });
|
|
@@ -86,20 +86,19 @@ function inferTablesFromIntrospection(introspection, options = {}) {
|
|
|
86
86
|
const { types, queryType, mutationType } = schema;
|
|
87
87
|
// Build lookup maps for efficient access
|
|
88
88
|
const typeMap = buildTypeMap(types);
|
|
89
|
+
const { entityNames, entityToConnection, connectionToEntity } = buildEntityConnectionMaps(types, typeMap);
|
|
89
90
|
const queryFields = getTypeFields(typeMap.get(queryType.name));
|
|
90
91
|
const mutationFields = mutationType
|
|
91
92
|
? getTypeFields(typeMap.get(mutationType.name))
|
|
92
93
|
: [];
|
|
93
|
-
// Step 1:
|
|
94
|
-
const entityNames = detectEntityTypes(types);
|
|
95
|
-
// Step 2: Build CleanTable for each entity
|
|
94
|
+
// Step 1: Build CleanTable for each inferred entity
|
|
96
95
|
const tables = [];
|
|
97
96
|
for (const entityName of entityNames) {
|
|
98
97
|
const entityType = typeMap.get(entityName);
|
|
99
98
|
if (!entityType)
|
|
100
99
|
continue;
|
|
101
100
|
// Infer all metadata for this entity
|
|
102
|
-
const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields);
|
|
101
|
+
const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity);
|
|
103
102
|
// Only include tables that have at least one real operation
|
|
104
103
|
if (hasRealOperation) {
|
|
105
104
|
tables.push(table);
|
|
@@ -107,17 +106,17 @@ function inferTablesFromIntrospection(introspection, options = {}) {
|
|
|
107
106
|
}
|
|
108
107
|
return tables;
|
|
109
108
|
}
|
|
110
|
-
// ============================================================================
|
|
111
|
-
// Entity Detection
|
|
112
|
-
// ============================================================================
|
|
113
109
|
/**
|
|
114
|
-
*
|
|
110
|
+
* Infer entity <-> connection mappings from schema types.
|
|
115
111
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
112
|
+
* Prefer deriving entity names from the `nodes` field of connection types.
|
|
113
|
+
* This is robust across v4/v5 naming variations (e.g. `UsersConnection` and
|
|
114
|
+
* `UserConnection`).
|
|
118
115
|
*/
|
|
119
|
-
function
|
|
116
|
+
function buildEntityConnectionMaps(types, typeMap) {
|
|
120
117
|
const entityNames = new Set();
|
|
118
|
+
const entityToConnection = new Map();
|
|
119
|
+
const connectionToEntity = new Map();
|
|
121
120
|
const typeNames = new Set(types.map((t) => t.name));
|
|
122
121
|
for (const type of types) {
|
|
123
122
|
// Skip internal types
|
|
@@ -128,26 +127,58 @@ function detectEntityTypes(types) {
|
|
|
128
127
|
// Check for Connection pattern
|
|
129
128
|
const connectionMatch = type.name.match(PATTERNS.connection);
|
|
130
129
|
if (connectionMatch) {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
// Verify the entity type actually exists
|
|
134
|
-
if (typeNames.has(
|
|
135
|
-
|
|
130
|
+
const fallbackEntityName = (0, inflekt_1.singularize)(connectionMatch[1]);
|
|
131
|
+
const entityName = resolveEntityNameFromConnectionType(type, typeMap) ?? fallbackEntityName;
|
|
132
|
+
// Verify the entity type actually exists and is not a built-in/internal type.
|
|
133
|
+
if (typeNames.has(entityName) &&
|
|
134
|
+
!BUILTIN_TYPES.has(entityName) &&
|
|
135
|
+
!isInternalType(entityName)) {
|
|
136
|
+
entityNames.add(entityName);
|
|
137
|
+
entityToConnection.set(entityName, type.name);
|
|
138
|
+
connectionToEntity.set(type.name, entityName);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
entityNames,
|
|
144
|
+
entityToConnection,
|
|
145
|
+
connectionToEntity,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Attempt to resolve an entity name from a connection type by inspecting its
|
|
150
|
+
* `nodes` field.
|
|
151
|
+
*/
|
|
152
|
+
function resolveEntityNameFromConnectionType(connectionType, typeMap) {
|
|
153
|
+
if (!connectionType.fields)
|
|
154
|
+
return null;
|
|
155
|
+
const nodesField = connectionType.fields.find((field) => field.name === 'nodes');
|
|
156
|
+
if (!nodesField)
|
|
157
|
+
return null;
|
|
158
|
+
const nodeTypeName = (0, introspection_1.getBaseTypeName)(nodesField.type);
|
|
159
|
+
if (!nodeTypeName)
|
|
160
|
+
return null;
|
|
161
|
+
const nodeType = typeMap.get(nodeTypeName);
|
|
162
|
+
if (!nodeType || nodeType.kind !== 'OBJECT')
|
|
163
|
+
return null;
|
|
164
|
+
if (nodeTypeName.endsWith('Connection'))
|
|
165
|
+
return null;
|
|
166
|
+
if (BUILTIN_TYPES.has(nodeTypeName))
|
|
167
|
+
return null;
|
|
168
|
+
if (isInternalType(nodeTypeName))
|
|
169
|
+
return null;
|
|
170
|
+
return nodeTypeName;
|
|
140
171
|
}
|
|
141
172
|
/**
|
|
142
173
|
* Build a complete CleanTable from an entity type
|
|
143
174
|
*/
|
|
144
|
-
function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields) {
|
|
175
|
+
function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity) {
|
|
145
176
|
// Extract scalar fields from entity type
|
|
146
|
-
const fields = extractEntityFields(entityType, typeMap);
|
|
177
|
+
const fields = extractEntityFields(entityType, typeMap, entityToConnection);
|
|
147
178
|
// Infer relations from entity fields
|
|
148
|
-
const relations = inferRelations(entityType,
|
|
179
|
+
const relations = inferRelations(entityType, entityToConnection, connectionToEntity);
|
|
149
180
|
// Match query and mutation operations
|
|
150
|
-
const queryOps = matchQueryOperations(entityName, queryFields,
|
|
181
|
+
const queryOps = matchQueryOperations(entityName, queryFields, entityToConnection);
|
|
151
182
|
const mutationOps = matchMutationOperations(entityName, mutationFields);
|
|
152
183
|
// Check if we found at least one real operation (not a fallback)
|
|
153
184
|
const hasRealOperation = !!(queryOps.all ||
|
|
@@ -157,16 +188,19 @@ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationF
|
|
|
157
188
|
mutationOps.delete);
|
|
158
189
|
// Infer primary key from mutation inputs
|
|
159
190
|
const constraints = inferConstraints(entityName, typeMap);
|
|
191
|
+
// Infer the patch field name from UpdateXxxInput (e.g., "userPatch")
|
|
192
|
+
const patchFieldName = inferPatchFieldName(entityName, typeMap);
|
|
160
193
|
// Build inflection map from discovered types
|
|
161
|
-
const inflection = buildInflection(entityName, typeMap);
|
|
194
|
+
const inflection = buildInflection(entityName, typeMap, entityToConnection);
|
|
162
195
|
// Combine query operations with fallbacks for UI purposes
|
|
163
196
|
// (but hasRealOperation indicates if we should include this table)
|
|
164
197
|
const query = {
|
|
165
198
|
all: queryOps.all ?? (0, inflekt_1.lcFirst)((0, inflekt_1.pluralize)(entityName)),
|
|
166
|
-
one: queryOps.one
|
|
199
|
+
one: queryOps.one,
|
|
167
200
|
create: mutationOps.create ?? `create${entityName}`,
|
|
168
201
|
update: mutationOps.update,
|
|
169
202
|
delete: mutationOps.delete,
|
|
203
|
+
patchFieldName,
|
|
170
204
|
};
|
|
171
205
|
return {
|
|
172
206
|
table: {
|
|
@@ -187,7 +221,7 @@ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationF
|
|
|
187
221
|
* Extract scalar fields from an entity type
|
|
188
222
|
* Excludes relation fields (those returning other entity types or connections)
|
|
189
223
|
*/
|
|
190
|
-
function extractEntityFields(entityType, typeMap) {
|
|
224
|
+
function extractEntityFields(entityType, typeMap, entityToConnection) {
|
|
191
225
|
const fields = [];
|
|
192
226
|
if (!entityType.fields)
|
|
193
227
|
return fields;
|
|
@@ -200,7 +234,7 @@ function extractEntityFields(entityType, typeMap) {
|
|
|
200
234
|
if (fieldType?.kind === 'OBJECT') {
|
|
201
235
|
// Check if it's a Connection type (hasMany) or entity type (belongsTo)
|
|
202
236
|
if (baseTypeName.endsWith('Connection') ||
|
|
203
|
-
isEntityType(baseTypeName,
|
|
237
|
+
isEntityType(baseTypeName, entityToConnection)) {
|
|
204
238
|
continue; // Skip relation fields
|
|
205
239
|
}
|
|
206
240
|
}
|
|
@@ -215,9 +249,8 @@ function extractEntityFields(entityType, typeMap) {
|
|
|
215
249
|
/**
|
|
216
250
|
* Check if a type name is an entity type (has a corresponding Connection)
|
|
217
251
|
*/
|
|
218
|
-
function isEntityType(typeName,
|
|
219
|
-
|
|
220
|
-
return typeMap.has(connectionName);
|
|
252
|
+
function isEntityType(typeName, entityToConnection) {
|
|
253
|
+
return entityToConnection.has(typeName);
|
|
221
254
|
}
|
|
222
255
|
/**
|
|
223
256
|
* Convert IntrospectionTypeRef to CleanFieldType
|
|
@@ -238,7 +271,7 @@ function convertToCleanFieldType(typeRef) {
|
|
|
238
271
|
/**
|
|
239
272
|
* Infer relations from entity type fields
|
|
240
273
|
*/
|
|
241
|
-
function inferRelations(entityType,
|
|
274
|
+
function inferRelations(entityType, entityToConnection, connectionToEntity) {
|
|
242
275
|
const belongsTo = [];
|
|
243
276
|
const hasMany = [];
|
|
244
277
|
const manyToMany = [];
|
|
@@ -251,17 +284,17 @@ function inferRelations(entityType, typeMap) {
|
|
|
251
284
|
continue;
|
|
252
285
|
// Check for Connection type → hasMany or manyToMany
|
|
253
286
|
if (baseTypeName.endsWith('Connection')) {
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
manyToMany.push(
|
|
287
|
+
const resolvedRelation = inferHasManyOrManyToMany(field, baseTypeName, connectionToEntity);
|
|
288
|
+
if (resolvedRelation.type === 'manyToMany') {
|
|
289
|
+
manyToMany.push(resolvedRelation.relation);
|
|
257
290
|
}
|
|
258
291
|
else {
|
|
259
|
-
hasMany.push(
|
|
292
|
+
hasMany.push(resolvedRelation.relation);
|
|
260
293
|
}
|
|
261
294
|
continue;
|
|
262
295
|
}
|
|
263
296
|
// Check for entity type → belongsTo
|
|
264
|
-
if (isEntityType(baseTypeName,
|
|
297
|
+
if (isEntityType(baseTypeName, entityToConnection)) {
|
|
265
298
|
belongsTo.push({
|
|
266
299
|
fieldName: field.name,
|
|
267
300
|
isUnique: false, // Can't determine from introspection alone
|
|
@@ -279,11 +312,13 @@ function inferRelations(entityType, typeMap) {
|
|
|
279
312
|
* ManyToMany pattern: field name contains "By" and "And"
|
|
280
313
|
* e.g., "productsByOrderItemOrderIdAndProductId"
|
|
281
314
|
*/
|
|
282
|
-
function inferHasManyOrManyToMany(field, connectionTypeName,
|
|
283
|
-
//
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
315
|
+
function inferHasManyOrManyToMany(field, connectionTypeName, connectionToEntity) {
|
|
316
|
+
// Resolve the related entity from discovered connection mappings first.
|
|
317
|
+
const relatedEntityName = connectionToEntity.get(connectionTypeName) ?? (() => {
|
|
318
|
+
const match = connectionTypeName.match(PATTERNS.connection);
|
|
319
|
+
const relatedPluralName = match ? match[1] : connectionTypeName;
|
|
320
|
+
return (0, inflekt_1.singularize)(relatedPluralName);
|
|
321
|
+
})();
|
|
287
322
|
// Check for manyToMany pattern in field name
|
|
288
323
|
const isManyToMany = field.name.includes('By') && field.name.includes('And');
|
|
289
324
|
if (isManyToMany) {
|
|
@@ -329,9 +364,9 @@ function inferHasManyOrManyToMany(field, connectionTypeName, typeMap) {
|
|
|
329
364
|
* - List query: returns {PluralName}Connection (e.g., users → UsersConnection)
|
|
330
365
|
* - Single query: returns {EntityName} with id/nodeId arg (e.g., user → User)
|
|
331
366
|
*/
|
|
332
|
-
function matchQueryOperations(entityName, queryFields,
|
|
367
|
+
function matchQueryOperations(entityName, queryFields, entityToConnection) {
|
|
333
368
|
const pluralName = (0, inflekt_1.pluralize)(entityName);
|
|
334
|
-
const connectionTypeName = `${pluralName}Connection`;
|
|
369
|
+
const connectionTypeName = entityToConnection.get(entityName) ?? `${pluralName}Connection`;
|
|
335
370
|
let all = null;
|
|
336
371
|
let one = null;
|
|
337
372
|
for (const field of queryFields) {
|
|
@@ -415,21 +450,18 @@ function inferConstraints(entityName, typeMap) {
|
|
|
415
450
|
const deleteInputName = `Delete${entityName}Input`;
|
|
416
451
|
const updateInput = typeMap.get(updateInputName);
|
|
417
452
|
const deleteInput = typeMap.get(deleteInputName);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
],
|
|
431
|
-
});
|
|
432
|
-
}
|
|
453
|
+
const keyInputField = inferPrimaryKeyFromInputObject(updateInput) ||
|
|
454
|
+
inferPrimaryKeyFromInputObject(deleteInput);
|
|
455
|
+
if (keyInputField) {
|
|
456
|
+
primaryKey.push({
|
|
457
|
+
name: 'primary',
|
|
458
|
+
fields: [
|
|
459
|
+
{
|
|
460
|
+
name: keyInputField.name,
|
|
461
|
+
type: convertToCleanFieldType(keyInputField.type),
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
});
|
|
433
465
|
}
|
|
434
466
|
// If no PK found from inputs, try to find 'id' field in entity type
|
|
435
467
|
if (primaryKey.length === 0) {
|
|
@@ -455,16 +487,70 @@ function inferConstraints(entityName, typeMap) {
|
|
|
455
487
|
unique: [], // Would need constraint info to populate
|
|
456
488
|
};
|
|
457
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* Infer a single-row lookup key from an Update/Delete input object.
|
|
492
|
+
*
|
|
493
|
+
* Priority:
|
|
494
|
+
* 1. Canonical keys: id, nodeId, rowId
|
|
495
|
+
* 2. Single non-patch/non-clientMutationId scalar-ish field
|
|
496
|
+
*
|
|
497
|
+
* If multiple possible key fields remain, return null to avoid guessing.
|
|
498
|
+
*/
|
|
499
|
+
function inferPrimaryKeyFromInputObject(inputType) {
|
|
500
|
+
const inputFields = inputType?.inputFields ?? [];
|
|
501
|
+
if (inputFields.length === 0)
|
|
502
|
+
return null;
|
|
503
|
+
const canonicalKey = inputFields.find((field) => field.name === 'id' || field.name === 'nodeId' || field.name === 'rowId');
|
|
504
|
+
if (canonicalKey)
|
|
505
|
+
return canonicalKey;
|
|
506
|
+
const candidates = inputFields.filter((field) => {
|
|
507
|
+
if (field.name === 'clientMutationId')
|
|
508
|
+
return false;
|
|
509
|
+
const baseTypeName = (0, introspection_1.getBaseTypeName)(field.type);
|
|
510
|
+
const lowerName = field.name.toLowerCase();
|
|
511
|
+
// Exclude patch payload fields (patch / fooPatch)
|
|
512
|
+
if (lowerName === 'patch' || lowerName.endsWith('patch'))
|
|
513
|
+
return false;
|
|
514
|
+
if (baseTypeName?.endsWith('Patch'))
|
|
515
|
+
return false;
|
|
516
|
+
return true;
|
|
517
|
+
});
|
|
518
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Infer the patch field name from an Update input type.
|
|
522
|
+
*
|
|
523
|
+
* PostGraphile v5 uses entity-specific patch field names:
|
|
524
|
+
* UpdateUserInput → { id, userPatch: UserPatch }
|
|
525
|
+
* UpdateDatabaseInput → { id, databasePatch: DatabasePatch }
|
|
526
|
+
*
|
|
527
|
+
* Pattern: {lcFirst(entityTypeName)}Patch
|
|
528
|
+
*/
|
|
529
|
+
function inferPatchFieldName(entityName, typeMap) {
|
|
530
|
+
const updateInputName = `Update${entityName}Input`;
|
|
531
|
+
const updateInput = typeMap.get(updateInputName);
|
|
532
|
+
const inputFields = updateInput?.inputFields ?? [];
|
|
533
|
+
// Find the field whose type name ends in 'Patch'
|
|
534
|
+
const patchField = inputFields.find((f) => {
|
|
535
|
+
const baseName = (0, introspection_1.getBaseTypeName)(f.type);
|
|
536
|
+
return baseName?.endsWith('Patch');
|
|
537
|
+
});
|
|
538
|
+
if (patchField)
|
|
539
|
+
return patchField.name;
|
|
540
|
+
// Fallback: compute from entity name (v5 default inflection)
|
|
541
|
+
return (0, inflekt_1.lcFirst)(entityName) + 'Patch';
|
|
542
|
+
}
|
|
458
543
|
// ============================================================================
|
|
459
544
|
// Inflection Building
|
|
460
545
|
// ============================================================================
|
|
461
546
|
/**
|
|
462
547
|
* Build inflection map from discovered types
|
|
463
548
|
*/
|
|
464
|
-
function buildInflection(entityName, typeMap) {
|
|
549
|
+
function buildInflection(entityName, typeMap, entityToConnection) {
|
|
465
550
|
const pluralName = (0, inflekt_1.pluralize)(entityName);
|
|
466
551
|
const singularFieldName = (0, inflekt_1.lcFirst)(entityName);
|
|
467
552
|
const pluralFieldName = (0, inflekt_1.lcFirst)(pluralName);
|
|
553
|
+
const connectionTypeName = entityToConnection.get(entityName) ?? `${pluralName}Connection`;
|
|
468
554
|
// Check which types actually exist in the schema
|
|
469
555
|
const hasFilter = typeMap.has(`${entityName}Filter`);
|
|
470
556
|
const hasPatch = typeMap.has(`${entityName}Patch`);
|
|
@@ -478,7 +564,7 @@ function buildInflection(entityName, typeMap) {
|
|
|
478
564
|
allRows: pluralFieldName,
|
|
479
565
|
allRowsSimple: pluralFieldName,
|
|
480
566
|
conditionType: `${entityName}Condition`,
|
|
481
|
-
connection:
|
|
567
|
+
connection: connectionTypeName,
|
|
482
568
|
createField: `create${entityName}`,
|
|
483
569
|
createInputType: `Create${entityName}Input`,
|
|
484
570
|
createPayloadType: `Create${entityName}Payload`,
|
|
@@ -240,7 +240,9 @@ function getTableOperationNames(tables) {
|
|
|
240
240
|
if (table.query) {
|
|
241
241
|
// Add exact query names from _meta
|
|
242
242
|
queries.add(table.query.all);
|
|
243
|
-
|
|
243
|
+
if (table.query.one) {
|
|
244
|
+
queries.add(table.query.one);
|
|
245
|
+
}
|
|
244
246
|
// Add exact mutation names from _meta
|
|
245
247
|
mutations.add(table.query.create);
|
|
246
248
|
if (table.query.update)
|
package/esm/core/ast.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as t from 'gql-ast';
|
|
2
|
+
import { OperationTypeNode } from 'graphql';
|
|
2
3
|
import { camelize, singularize } from 'inflekt';
|
|
3
4
|
import { getCustomAst } from './custom-ast';
|
|
4
5
|
const NON_MUTABLE_PROPS = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
|
|
@@ -39,7 +40,7 @@ const createGqlMutation = ({ operationName, mutationName, selectArgs, selections
|
|
|
39
40
|
return t.document({
|
|
40
41
|
definitions: [
|
|
41
42
|
t.operationDefinition({
|
|
42
|
-
operation:
|
|
43
|
+
operation: OperationTypeNode.MUTATION,
|
|
43
44
|
name: mutationName,
|
|
44
45
|
variableDefinitions,
|
|
45
46
|
selectionSet: t.selectionSet({ selections: opSel }),
|
|
@@ -68,7 +69,7 @@ export const getAll = ({ queryName, operationName, selection, }) => {
|
|
|
68
69
|
const ast = t.document({
|
|
69
70
|
definitions: [
|
|
70
71
|
t.operationDefinition({
|
|
71
|
-
operation:
|
|
72
|
+
operation: OperationTypeNode.QUERY,
|
|
72
73
|
name: queryName,
|
|
73
74
|
selectionSet: t.selectionSet({ selections: opSel }),
|
|
74
75
|
}),
|
|
@@ -111,7 +112,7 @@ export const getCount = ({ queryName, operationName, query, }) => {
|
|
|
111
112
|
const ast = t.document({
|
|
112
113
|
definitions: [
|
|
113
114
|
t.operationDefinition({
|
|
114
|
-
operation:
|
|
115
|
+
operation: OperationTypeNode.QUERY,
|
|
115
116
|
name: queryName,
|
|
116
117
|
variableDefinitions,
|
|
117
118
|
selectionSet: t.selectionSet({ selections: opSel }),
|
|
@@ -210,7 +211,7 @@ export const getMany = ({ builder, queryName, operationName, query, selection, }
|
|
|
210
211
|
const ast = t.document({
|
|
211
212
|
definitions: [
|
|
212
213
|
t.operationDefinition({
|
|
213
|
-
operation:
|
|
214
|
+
operation: OperationTypeNode.QUERY,
|
|
214
215
|
name: queryName,
|
|
215
216
|
variableDefinitions,
|
|
216
217
|
selectionSet: t.selectionSet({
|
|
@@ -266,7 +267,7 @@ export const getOne = ({ queryName, operationName, query, selection, }) => {
|
|
|
266
267
|
const ast = t.document({
|
|
267
268
|
definitions: [
|
|
268
269
|
t.operationDefinition({
|
|
269
|
-
operation:
|
|
270
|
+
operation: OperationTypeNode.QUERY,
|
|
270
271
|
name: queryName,
|
|
271
272
|
variableDefinitions,
|
|
272
273
|
selectionSet: t.selectionSet({ selections: opSel }),
|