@stonecrop/schema 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +10 -10
  2. package/dist/cli.js +194 -121
  3. package/dist/cli.js.map +1 -1
  4. package/dist/converter-CLwduvT_.js +2102 -0
  5. package/dist/converter-CLwduvT_.js.map +1 -0
  6. package/dist/flatten-Bx2cfvw3.js +37 -0
  7. package/dist/flatten-Bx2cfvw3.js.map +1 -0
  8. package/dist/index.js +123 -117
  9. package/dist/index.js.map +1 -1
  10. package/dist/record-BQOOi83C.js +134 -0
  11. package/dist/record-BQOOi83C.js.map +1 -0
  12. package/dist/record.js +2 -6
  13. package/dist/tsdoc-metadata.json +1 -1
  14. package/package.json +34 -18
  15. package/dist/flatten-C1MjkzFh.js +0 -10
  16. package/dist/flatten-C1MjkzFh.js.map +0 -1
  17. package/dist/record-Bc0lI9Rq.js +0 -61
  18. package/dist/record-Bc0lI9Rq.js.map +0 -1
  19. package/dist/record.js.map +0 -1
  20. package/dist/schema.tsbuildinfo +0 -1
  21. package/dist/src/badge.d.ts +0 -74
  22. package/dist/src/badge.d.ts.map +0 -1
  23. package/dist/src/badge.js +0 -158
  24. package/dist/src/cli.d.ts +0 -3
  25. package/dist/src/cli.d.ts.map +0 -1
  26. package/dist/src/cli.js +0 -292
  27. package/dist/src/column-schema.d.ts +0 -163
  28. package/dist/src/column-schema.d.ts.map +0 -1
  29. package/dist/src/column-schema.js +0 -0
  30. package/dist/src/component-meta.d.ts +0 -96
  31. package/dist/src/component-meta.d.ts.map +0 -1
  32. package/dist/src/component-meta.js +0 -88
  33. package/dist/src/converter/aggregate.d.ts +0 -127
  34. package/dist/src/converter/aggregate.d.ts.map +0 -1
  35. package/dist/src/converter/aggregate.js +0 -235
  36. package/dist/src/converter/authored.d.ts +0 -43
  37. package/dist/src/converter/authored.d.ts.map +0 -1
  38. package/dist/src/converter/authored.js +0 -52
  39. package/dist/src/converter/heuristics.d.ts +0 -60
  40. package/dist/src/converter/heuristics.d.ts.map +0 -1
  41. package/dist/src/converter/heuristics.js +0 -304
  42. package/dist/src/converter/index.d.ts +0 -51
  43. package/dist/src/converter/index.d.ts.map +0 -1
  44. package/dist/src/converter/index.js +0 -195
  45. package/dist/src/converter/merge.d.ts +0 -102
  46. package/dist/src/converter/merge.d.ts.map +0 -1
  47. package/dist/src/converter/merge.js +0 -136
  48. package/dist/src/converter/scalars.d.ts +0 -46
  49. package/dist/src/converter/scalars.d.ts.map +0 -1
  50. package/dist/src/converter/scalars.js +0 -83
  51. package/dist/src/converter/types.d.ts +0 -157
  52. package/dist/src/converter/types.d.ts.map +0 -1
  53. package/dist/src/converter/types.js +0 -5
  54. package/dist/src/doctype.d.ts +0 -516
  55. package/dist/src/doctype.d.ts.map +0 -1
  56. package/dist/src/doctype.js +0 -343
  57. package/dist/src/field.d.ts +0 -423
  58. package/dist/src/field.d.ts.map +0 -1
  59. package/dist/src/field.js +0 -378
  60. package/dist/src/flatten.d.ts +0 -29
  61. package/dist/src/flatten.d.ts.map +0 -1
  62. package/dist/src/flatten.js +0 -38
  63. package/dist/src/index.d.ts +0 -16
  64. package/dist/src/index.d.ts.map +0 -1
  65. package/dist/src/index.js +0 -20
  66. package/dist/src/mode.d.ts +0 -15
  67. package/dist/src/mode.d.ts.map +0 -1
  68. package/dist/src/mode.js +0 -0
  69. package/dist/src/naming.d.ts +0 -80
  70. package/dist/src/naming.d.ts.map +0 -1
  71. package/dist/src/naming.js +0 -106
  72. package/dist/src/record.d.ts +0 -29
  73. package/dist/src/record.d.ts.map +0 -1
  74. package/dist/src/record.js +0 -55
  75. package/dist/src/table.d.ts +0 -33
  76. package/dist/src/table.d.ts.map +0 -1
  77. package/dist/src/table.js +0 -25
  78. package/dist/src/validation.d.ts +0 -54
  79. package/dist/src/validation.d.ts.map +0 -1
  80. package/dist/src/validation.js +0 -60
  81. package/dist/validation-C9P__pRF.js +0 -994
  82. package/dist/validation-C9P__pRF.js.map +0 -1
@@ -1,304 +0,0 @@
1
- /**
2
- * Default heuristics for identifying entity types and fields in a GraphQL schema.
3
- *
4
- * These heuristics work across common GraphQL servers (PostGraphile, Hasura, Apollo, etc.)
5
- * by detecting widely-adopted conventions like the Relay connection pattern.
6
- *
7
- * All heuristics can be overridden via the `isEntityType`, `isEntityField`, and
8
- * `classifyField` options in `GraphQLConversionOptions`.
9
- *
10
- * @packageDocumentation
11
- */
12
- import { isScalarType, isEnumType, isObjectType, isListType, isNonNullType, isNamedType, } from 'graphql';
13
- import { buildScalarMap, INTERNAL_SCALARS } from './scalars';
14
- import { toSlug, camelToLabel, toPascalCase } from '../naming';
15
- /**
16
- * Suffixes that identify synthetic/framework types generated by GraphQL servers.
17
- * Types ending with these suffixes are typically not entities.
18
- */
19
- const SYNTHETIC_SUFFIXES = [
20
- 'Connection',
21
- 'Edge',
22
- 'Input',
23
- 'Patch',
24
- 'Payload',
25
- 'Condition',
26
- 'Filter',
27
- 'OrderBy',
28
- 'Aggregate',
29
- 'AggregateResult',
30
- 'AggregateFilter',
31
- 'DeleteResponse',
32
- 'InsertResponse',
33
- 'UpdateResponse',
34
- 'MutationResponse',
35
- ];
36
- /**
37
- * Root operation type names that are never entities.
38
- */
39
- const ROOT_TYPE_NAMES = new Set(['Query', 'Mutation', 'Subscription']);
40
- /**
41
- * Default heuristic to determine if a GraphQL object type represents an entity.
42
- * An entity type becomes a Stonecrop doctype.
43
- *
44
- * This heuristic excludes:
45
- * - Introspection types (`__*`)
46
- * - Root operation types (`Query`, `Mutation`, `Subscription`)
47
- * - Types with synthetic suffixes (e.g., `*Connection`, `*Edge`, `*Input`)
48
- * - Types starting with `Node` interface marker (exact match only)
49
- *
50
- * @param typeName - The GraphQL type name
51
- * @param type - The GraphQL object type definition
52
- * @returns `true` if this type should become a Stonecrop doctype
53
- * @public
54
- */
55
- export function defaultIsEntityType(typeName, type) {
56
- // Exclude introspection types
57
- if (typeName.startsWith('__')) {
58
- return false;
59
- }
60
- // Exclude root operation types
61
- if (ROOT_TYPE_NAMES.has(typeName)) {
62
- return false;
63
- }
64
- // Exclude the Node interface marker type
65
- if (typeName === 'Node') {
66
- return false;
67
- }
68
- // Exclude types matching synthetic suffixes
69
- for (const suffix of SYNTHETIC_SUFFIXES) {
70
- if (typeName.endsWith(suffix)) {
71
- return false;
72
- }
73
- }
74
- // Must have at least one field
75
- const fields = type.getFields();
76
- if (Object.keys(fields).length === 0) {
77
- return false;
78
- }
79
- return true;
80
- }
81
- /**
82
- * Fields to skip by default on entity types.
83
- * These are internal to GraphQL servers and don't represent semantic data.
84
- *
85
- * Relay's global object identifier is deliberately absent: which field carries it is a
86
- * declaration, not a name. See {@link relayNodeIdField}.
87
- */
88
- const SKIP_FIELDS = new Set(['__typename', 'clientMutationId']);
89
- /**
90
- * The name Relay's Object Identification spec gives its marker interface.
91
- */
92
- const RELAY_NODE_INTERFACE = 'Node';
93
- /**
94
- * The field carrying Relay's global object identifier on this type, or `undefined` for a type that
95
- * declares none.
96
- *
97
- * Read off the interface rather than matched against a list of names, because the name is a server
98
- * setting: PostGraphile exposes it as `nodeIdFieldName`, which is `id` under the un-overridden
99
- * Amber preset, `nodeId` under Stonecrop's, and whatever a foreign host chose under theirs. A
100
- * hardcoded name is a snapshot of one of those, and gets it wrong in both directions at once — it
101
- * emits an opaque identifier as a column (whose every read then fails on a column that does not
102
- * exist), and drops a real column that happens to share the name.
103
- *
104
- * The interface must be Relay's marker and not a domain interface that shares its name, so it has
105
- * to declare exactly one field, a non-null `ID`, and nothing else — anything carrying domain fields
106
- * is a different interface, and skipping against it would drop real columns.
107
- *
108
- * @internal
109
- */
110
- function relayNodeIdField(type) {
111
- for (const iface of type.getInterfaces()) {
112
- if (iface.name !== RELAY_NODE_INTERFACE)
113
- continue;
114
- const declared = Object.values(iface.getFields());
115
- if (declared.length !== 1)
116
- continue;
117
- const { namedType, required, isList } = unwrapType(declared[0].type);
118
- if (required && !isList && namedType.name === 'ID')
119
- return declared[0].name;
120
- }
121
- return undefined;
122
- }
123
- /**
124
- * Default heuristic to filter fields on entity types.
125
- * Skips internal fields that don't represent meaningful data.
126
- *
127
- * @param fieldName - The GraphQL field name
128
- * @param _field - The GraphQL field definition (unused in default implementation)
129
- * @param parentType - The parent entity type, whose interfaces declare its Relay identifier
130
- * @returns `true` if this field should be included
131
- * @public
132
- */
133
- export function defaultIsEntityField(fieldName, _field, parentType) {
134
- if (SKIP_FIELDS.has(fieldName))
135
- return false;
136
- return fieldName !== relayNodeIdField(parentType);
137
- }
138
- /**
139
- * Unwrap NonNull and List wrappers from a GraphQL type, tracking nullability.
140
- *
141
- * @param type - The GraphQL output type
142
- * @returns The unwrapped named type, whether it's required, and whether it's a list
143
- * @internal
144
- */
145
- function unwrapType(type) {
146
- let required = false;
147
- let isList = false;
148
- let current = type;
149
- // Unwrap outer NonNull
150
- if (isNonNullType(current)) {
151
- required = true;
152
- current = current.ofType;
153
- }
154
- // Unwrap List
155
- if (isListType(current)) {
156
- isList = true;
157
- current = current.ofType;
158
- // Unwrap inner NonNull (e.g., [Type!])
159
- if (isNonNullType(current)) {
160
- current = current.ofType;
161
- }
162
- }
163
- // At this point, current should be a named type (scalar, enum, or object)
164
- if (!isNamedType(current)) {
165
- throw new Error(`Expected a named GraphQL type, got: ${String(current)}`);
166
- }
167
- return { namedType: current, required, isList };
168
- }
169
- /**
170
- * Check if a GraphQL object type looks like a Relay Connection type.
171
- * A connection type has an `edges` field returning a list of edge types,
172
- * where each edge has a `node` field.
173
- *
174
- * @param type - The GraphQL object type to check
175
- * @returns The node type name if this is a connection, or `undefined`
176
- * @internal
177
- */
178
- function getConnectionNodeType(type) {
179
- const fields = type.getFields();
180
- // Must have an 'edges' field
181
- const edgesField = fields['edges'];
182
- if (!edgesField)
183
- return undefined;
184
- // edges must be a list
185
- const { namedType: edgesType, isList: edgesIsList } = unwrapType(edgesField.type);
186
- if (!edgesIsList || !isObjectType(edgesType))
187
- return undefined;
188
- // Each edge must have a 'node' field
189
- const edgeFields = edgesType.getFields();
190
- const nodeField = edgeFields['node'];
191
- if (!nodeField)
192
- return undefined;
193
- const { namedType: nodeType } = unwrapType(nodeField.type);
194
- if (!isObjectType(nodeType))
195
- return undefined;
196
- return nodeType.name;
197
- }
198
- /**
199
- * Classify a single GraphQL field into a Stonecrop field definition.
200
- *
201
- * Classification rules (in order):
202
- * 1. Scalar types → look up in merged scalar map
203
- * 2. Enum types → `Select` with enum values as options
204
- * 3. Object types that are entities → `Link` with slug as options
205
- * 4. Object types that are Connections → `Doctype` with node type slug as options
206
- * 5. List of entity type → `Doctype` with item type slug as options
207
- * 6. Anything else → `Data` with `_unmapped: true`
208
- *
209
- * @param fieldName - The GraphQL field name
210
- * @param field - The GraphQL field definition
211
- * @param entityTypes - Set of type names classified as entities
212
- * @param options - Conversion options (for custom scalars, unmapped meta, etc.)
213
- * @returns The Stonecrop field definition
214
- * @public
215
- */
216
- export function classifyFieldType(fieldName, field, entityTypes, options = {}) {
217
- const { namedType, required, isList } = unwrapType(field.type);
218
- const scalarMap = buildScalarMap(options.customScalars);
219
- const base = {
220
- kind: 'field',
221
- fieldname: fieldName,
222
- label: camelToLabel(fieldName),
223
- component: 'ATextInput',
224
- };
225
- if (required) {
226
- base.required = true;
227
- }
228
- // 1. Scalar types
229
- if (isScalarType(namedType)) {
230
- // Skip internal scalars (e.g., Cursor)
231
- if (INTERNAL_SCALARS.has(namedType.name)) {
232
- base._unmapped = true;
233
- if (options.includeUnmappedMeta) {
234
- base._graphqlType = namedType.name;
235
- }
236
- return base;
237
- }
238
- // Special case: ID fields that reference an entity type → Link
239
- if (namedType.name === 'ID') {
240
- const candidateTypeName = toPascalCase(fieldName);
241
- if (entityTypes.has(candidateTypeName)) {
242
- base.component = 'AFormLink';
243
- base.doctype = toSlug(candidateTypeName);
244
- return base;
245
- }
246
- }
247
- const template = scalarMap[namedType.name];
248
- if (template) {
249
- base.component = template.component;
250
- }
251
- else {
252
- // Unknown scalar — default to Data with unmapped marker
253
- base._unmapped = true;
254
- if (options.includeUnmappedMeta) {
255
- base._graphqlType = namedType.name;
256
- }
257
- }
258
- return base;
259
- }
260
- // 2. Enum types → Select
261
- if (isEnumType(namedType)) {
262
- base.component = 'ADropdown';
263
- base.options = namedType.getValues().map(v => v.name);
264
- return base;
265
- }
266
- // 3–5. Object types
267
- if (isObjectType(namedType)) {
268
- // 3. Direct reference to an entity type → Link
269
- if (!isList && entityTypes.has(namedType.name)) {
270
- base.component = 'AFormLink';
271
- base.doctype = toSlug(namedType.name);
272
- return base;
273
- }
274
- // 4. Connection type → link (child table)
275
- const connectionNodeTypeName = getConnectionNodeType(namedType);
276
- if (connectionNodeTypeName && entityTypes.has(connectionNodeTypeName)) {
277
- base.component = 'ATable';
278
- base._isLink = true;
279
- base.doctype = toSlug(connectionNodeTypeName);
280
- base.cardinality = 'noneOrMany';
281
- return base;
282
- }
283
- // 5. List of entity type → link
284
- if (isList && entityTypes.has(namedType.name)) {
285
- base.component = 'ATable';
286
- base._isLink = true;
287
- base.doctype = toSlug(namedType.name);
288
- base.cardinality = 'noneOrMany';
289
- return base;
290
- }
291
- // Unknown object type — mark as unmapped
292
- base._unmapped = true;
293
- if (options.includeUnmappedMeta) {
294
- base._graphqlType = namedType.name;
295
- }
296
- return base;
297
- }
298
- // Fallback — shouldn't normally be reached
299
- base._unmapped = true;
300
- if (options.includeUnmappedMeta) {
301
- base._graphqlType = namedType.name;
302
- }
303
- return base;
304
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * GraphQL Introspection to Stonecrop Schema Converter
3
- *
4
- * Converts a standard GraphQL introspection result (or SDL string) into
5
- * Stonecrop doctype schemas. Source-agnostic — works with any GraphQL server.
6
- *
7
- * @packageDocumentation
8
- */
9
- import type { IntrospectionSource, GraphQLConversionOptions, ConvertedGraphQLDoctype } from './types';
10
- /**
11
- * Convert a GraphQL schema to Stonecrop doctype schemas.
12
- *
13
- * Accepts either an `IntrospectionQuery` result object or an SDL string.
14
- * Entity types are identified using heuristics (or a custom `isEntityType` function)
15
- * and converted to `DoctypeMeta`-compatible JSON objects.
16
- *
17
- * @param source - GraphQL introspection result or SDL string
18
- * @param options - Conversion options for controlling output format and behavior
19
- * @returns Array of converted Stonecrop doctype definitions
20
- *
21
- * @example
22
- * ```typescript
23
- * // From introspection result (fetched from any GraphQL server)
24
- * const introspection = await fetchIntrospection('http://localhost:5000/graphql')
25
- * const doctypes = convertGraphQLSchema(introspection)
26
- *
27
- * // From SDL string
28
- * const sdl = fs.readFileSync('schema.graphql', 'utf-8')
29
- * const doctypes = convertGraphQLSchema(sdl)
30
- *
31
- * // With PostGraphile custom scalars
32
- * const doctypes = convertGraphQLSchema(introspection, {
33
- * customScalars: {
34
- * BigFloat: { component: 'ANumericInput' }
35
- * }
36
- * })
37
- * ```
38
- *
39
- * @public
40
- */
41
- export declare function convertGraphQLSchema(source: IntrospectionSource, options?: GraphQLConversionOptions): ConvertedGraphQLDoctype[];
42
- export { convertGraphQLSchema as default };
43
- export type { IntrospectionSource, GraphQLConversionOptions, GraphQLConversionFieldMeta, ConvertedGraphQLDoctype, } from './types';
44
- export { GQL_SCALAR_MAP, WELL_KNOWN_SCALARS, INTERNAL_SCALARS, buildScalarMap } from './scalars';
45
- export { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
46
- export { aggregateDoctypeName, buildAggregateDoctype, planGeneration } from './aggregate';
47
- export type { GenerationPlanEntry, GenerationPlanOptions } from './aggregate';
48
- export { mergeIntrospectedDoctype, formatDoctypeDrift } from './merge';
49
- export type { AuthoredDoctype, DoctypeDrift, MergeOptions, MergeResult } from './merge';
50
- export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from '../naming';
51
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/converter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAIrG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,GAAE,wBAA6B,GACpC,uBAAuB,EAAE,CAkJ3B;AAwBD,OAAO,EAAE,oBAAoB,IAAI,OAAO,EAAE,CAAA;AAG1C,YAAY,EACX,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACvB,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAGhG,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAG3F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACzF,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAG7E,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAGvF,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
@@ -1,195 +0,0 @@
1
- /**
2
- * GraphQL Introspection to Stonecrop Schema Converter
3
- *
4
- * Converts a standard GraphQL introspection result (or SDL string) into
5
- * Stonecrop doctype schemas. Source-agnostic — works with any GraphQL server.
6
- *
7
- * @packageDocumentation
8
- */
9
- import { buildClientSchema, buildSchema, isObjectType } from 'graphql';
10
- import { toSlug } from '../naming';
11
- import { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
12
- /**
13
- * Convert a GraphQL schema to Stonecrop doctype schemas.
14
- *
15
- * Accepts either an `IntrospectionQuery` result object or an SDL string.
16
- * Entity types are identified using heuristics (or a custom `isEntityType` function)
17
- * and converted to `DoctypeMeta`-compatible JSON objects.
18
- *
19
- * @param source - GraphQL introspection result or SDL string
20
- * @param options - Conversion options for controlling output format and behavior
21
- * @returns Array of converted Stonecrop doctype definitions
22
- *
23
- * @example
24
- * ```typescript
25
- * // From introspection result (fetched from any GraphQL server)
26
- * const introspection = await fetchIntrospection('http://localhost:5000/graphql')
27
- * const doctypes = convertGraphQLSchema(introspection)
28
- *
29
- * // From SDL string
30
- * const sdl = fs.readFileSync('schema.graphql', 'utf-8')
31
- * const doctypes = convertGraphQLSchema(sdl)
32
- *
33
- * // With PostGraphile custom scalars
34
- * const doctypes = convertGraphQLSchema(introspection, {
35
- * customScalars: {
36
- * BigFloat: { component: 'ANumericInput' }
37
- * }
38
- * })
39
- * ```
40
- *
41
- * @public
42
- */
43
- export function convertGraphQLSchema(source, options = {}) {
44
- const schema = buildGraphQLSchema(source);
45
- const typeMap = schema.getTypeMap();
46
- // Determine the root operation type names to exclude
47
- const rootTypeNames = new Set();
48
- const queryType = schema.getQueryType();
49
- const mutationType = schema.getMutationType();
50
- const subscriptionType = schema.getSubscriptionType();
51
- if (queryType)
52
- rootTypeNames.add(queryType.name);
53
- if (mutationType)
54
- rootTypeNames.add(mutationType.name);
55
- if (subscriptionType)
56
- rootTypeNames.add(subscriptionType.name);
57
- // Use custom or default entity type detector
58
- const isEntityType = options.isEntityType ?? defaultIsEntityType;
59
- // Phase 1: Identify all entity types
60
- const entityTypes = new Set();
61
- for (const [typeName, type] of Object.entries(typeMap)) {
62
- if (!isObjectType(type))
63
- continue;
64
- // Always skip root operation types (even if custom isEntityType doesn't)
65
- if (rootTypeNames.has(typeName))
66
- continue;
67
- if (isEntityType(typeName, type)) {
68
- entityTypes.add(typeName);
69
- }
70
- }
71
- // Phase 2: Apply include/exclude filters
72
- let filteredEntityTypes = entityTypes;
73
- if (options.include) {
74
- const includeSet = new Set(options.include);
75
- filteredEntityTypes = new Set([...entityTypes].filter(t => includeSet.has(t)));
76
- }
77
- if (options.exclude) {
78
- const excludeSet = new Set(options.exclude);
79
- filteredEntityTypes = new Set([...filteredEntityTypes].filter(t => !excludeSet.has(t)));
80
- }
81
- // Phase 3: Convert each entity type to a doctype
82
- const isEntityField = options.isEntityField ?? defaultIsEntityField;
83
- const doctypes = [];
84
- for (const typeName of filteredEntityTypes) {
85
- const type = typeMap[typeName];
86
- if (!isObjectType(type))
87
- continue;
88
- const fields = type.getFields();
89
- // A type carrying BOTH `id` and `rowId` is PostGraphile Amber with its default inflection:
90
- // the Relay global identifier has taken `id`, displacing the real column to `rowId`. Neither
91
- // name can be emitted as-is — `id` is an opaque node id, and `rowId` does not name a column.
92
- // Refuse to guess: drop the Relay field and tell the caller to fix it at the inflector, where
93
- // it belongs. Normalizing here would bake a database fact into the doctype.
94
- const isUnnormalizedPostGraphile = 'id' in fields && 'rowId' in fields;
95
- if (isUnnormalizedPostGraphile) {
96
- options.onWarning?.(`${typeName}: schema exposes both 'id' (Relay identifier) and 'rowId' (the real column). ` +
97
- `Skipping 'id' and emitting 'rowId' verbatim — no primary key can be derived. ` +
98
- `Override the '_attributeName' and 'nodeIdFieldName' inflectors so the column keeps its own name.`);
99
- }
100
- const entityFields = Object.entries(fields).filter(([fieldName, field]) => isEntityField(fieldName, field, type) && !(isUnnormalizedPostGraphile && fieldName === 'id'));
101
- // oxlint-disable-next-line oxc/no-map-spread -- ...custom spread required; Object.assign cannot preserve the metadata-carrying inferred union type from classifyField
102
- const allClassifiedFields = entityFields.map(([fieldName, field]) => {
103
- // Check for full custom classification first
104
- if (options.classifyField) {
105
- const custom = options.classifyField(fieldName, field, type);
106
- if (custom !== null && custom !== undefined) {
107
- return {
108
- kind: 'field',
109
- fieldname: fieldName,
110
- label: custom.label ?? fieldName,
111
- component: custom.component ?? 'ATextInput',
112
- ...custom,
113
- };
114
- }
115
- }
116
- // Default classification
117
- return classifyFieldType(fieldName, field, entityTypes, options);
118
- });
119
- // Derive the primary key, but only for the one case SDL actually settles: a non-null `id`
120
- // that is a plain scalar. A natural key is typically a UNIQUE constraint indistinguishable
121
- // from any other column here, and a table may carry several — so anything else is left for
122
- // the author to declare. Emitting a guess would be worse than emitting nothing, because the
123
- // middleware builds its identity predicate from this and the client keys records by it.
124
- const primaryKeyFieldname = allClassifiedFields.find(field => field.fieldname === 'id' && field.required && !field.doctype && !field._isLink)?.fieldname;
125
- // Separate scalar fields from link fields
126
- const links = {};
127
- const convertedFields = allClassifiedFields
128
- .filter(field => {
129
- if (field._isLink && field.doctype && field.cardinality) {
130
- links[field.fieldname] = {
131
- target: field.doctype,
132
- cardinality: field.cardinality,
133
- };
134
- return false;
135
- }
136
- return true;
137
- })
138
- // Clean up internal metadata unless requested, and stamp identity + provenance.
139
- // Stamped last so every classification path (default, classifyField) carries the marker —
140
- // the docbuilder's identity lock keys off it, and no classifier may unset it.
141
- .map(field => {
142
- const identity = field.fieldname === primaryKeyFieldname ? { primaryKey: true } : {};
143
- if (!options.includeUnmappedMeta) {
144
- const { _graphqlType, _unmapped, _isLink, ...clean } = field;
145
- return Object.assign(clean, identity, { source: 'introspected' });
146
- }
147
- const { _isLink, ...rest } = field;
148
- return Object.assign(rest, identity, { source: 'introspected' });
149
- });
150
- const doctypeName = options.doctypeNames?.[typeName] ?? typeName;
151
- const doctype = {
152
- name: doctypeName,
153
- slug: toSlug(doctypeName),
154
- fields: convertedFields,
155
- };
156
- if (Object.keys(links).length > 0) {
157
- doctype.links = links;
158
- }
159
- if (options.includeUnmappedMeta) {
160
- doctype._graphqlTypeName = typeName;
161
- }
162
- doctypes.push(doctype);
163
- }
164
- return doctypes;
165
- }
166
- /**
167
- * Build a GraphQLSchema from either an introspection result or SDL string.
168
- *
169
- * @param source - IntrospectionQuery object or SDL string
170
- * @returns A complete GraphQLSchema
171
- * @internal
172
- */
173
- function buildGraphQLSchema(source) {
174
- if (typeof source === 'string') {
175
- // SDL string
176
- return buildSchema(source);
177
- }
178
- // IntrospectionQuery result
179
- return buildClientSchema(source);
180
- }
181
- // ═══════════════════════════════════════════════════════════════
182
- // Re-exports
183
- // ═══════════════════════════════════════════════════════════════
184
- // Main converter (this file)
185
- export { convertGraphQLSchema as default };
186
- // Scalar maps
187
- export { GQL_SCALAR_MAP, WELL_KNOWN_SCALARS, INTERNAL_SCALARS, buildScalarMap } from './scalars';
188
- // Heuristics
189
- export { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
190
- // Aggregate — the collection-view doctype derived from an entity, emitted as its own file
191
- export { aggregateDoctypeName, buildAggregateDoctype, planGeneration } from './aggregate';
192
- // Merge — verifies an authored doctype against the schema and stamps provenance
193
- export { mergeIntrospectedDoctype, formatDoctypeDrift } from './merge';
194
- // Naming utilities
195
- export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from '../naming';
@@ -1,102 +0,0 @@
1
- /**
2
- * Merge introspected schema facts into an already-authored doctype.
3
- *
4
- * The authored doctype is the source of truth. Generation **verifies** it and stamps provenance;
5
- * it does not overwrite. That polarity is deliberate and load-bearing — a doctype legitimately
6
- * declares a `primaryKey` the schema cannot express. A natural business key is very often a
7
- * `UNIQUE` constraint rather than the table's `PRIMARY KEY`, and where a table carries several
8
- * uniques no rule can pick between them. Overwriting identity from the schema would silently
9
- * re-key such a doctype on every regeneration and break the handlers that key on the old value.
10
- *
11
- * So divergence is **reported, never applied** — a human decides. The only mutation this performs
12
- * is adding `source: 'introspected'` to fields confirmed to exist in the GraphQL schema.
13
- *
14
- * @packageDocumentation
15
- */
16
- import type { AuthoredDoctype } from './authored';
17
- import type { ConvertedGraphQLDoctype } from './types';
18
- export type { AuthoredDoctype };
19
- /**
20
- * What generation found that the authored doctype does not agree with. Every bucket is advisory —
21
- * nothing here is applied automatically.
22
- *
23
- * @public
24
- */
25
- export interface DoctypeDrift {
26
- /** The authored doctype's name. */
27
- doctype: string;
28
- /**
29
- * `clean` — the authored primary key is the one generation would derive.
30
- * `partial` — the doctype declares an identity generation cannot derive, so identity was left alone.
31
- */
32
- mode: 'clean' | 'partial';
33
- /** Why the mode is `partial`, when it is. */
34
- reason?: string;
35
- /** Fieldnames confirmed against the schema and stamped. */
36
- tagged: string[];
37
- /** Authored fields with no matching schema field — app components, fieldsets, or stale entries. */
38
- orphan: string[];
39
- /** Schema fields absent from the doctype. Usually deliberate curation, occasionally an oversight. */
40
- omitted: string[];
41
- /** `fieldname: authored=… schema=…` where the chosen component differs from the scalar mapping. */
42
- componentDrift: string[];
43
- /** `fieldname: authored=… schema=…` where nullability disagrees. */
44
- requiredDrift: string[];
45
- /** Identity properties that differ. These are the ones a human must adjudicate. */
46
- identityDrift: string[];
47
- }
48
- /**
49
- * How to verify the authored doctype against the schema.
50
- *
51
- * @public
52
- */
53
- export interface MergeOptions {
54
- /**
55
- * The authored doctype is a curated **subset** of the schema's columns rather than a model of
56
- * all of them — an aggregate being the case this exists for.
57
- *
58
- * This changes what counts as drift in both directions, so `generated` must be passed the
59
- * *entity's* full field set, not the subset's. A column the author added to an aggregate is
60
- * then confirmed against the real table (so a genuinely dropped column still reports as an
61
- * orphan), while the columns deliberately left out stop reporting as omissions. Without it an
62
- * aggregate reports phantom drift on every run, which both spams `--check` and buries the one
63
- * finding that matters.
64
- */
65
- subset?: boolean;
66
- }
67
- /** Outcome of a merge: the doctype to write, plus what generation disagreed with. @public */
68
- export interface MergeResult {
69
- /** The authored doctype with `source` markers added and nothing else changed. */
70
- doctype: AuthoredDoctype;
71
- /** Advisory report. Never applied. */
72
- drift: DoctypeDrift;
73
- }
74
- /**
75
- * Verify an authored doctype against freshly generated output and stamp provenance.
76
- *
77
- * @param authored - the doctype as it exists on disk; every key not named below is preserved verbatim
78
- * @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type. For a
79
- * `subset` merge this is the **entity**, whose fields are the set the subset is curated from
80
- * @param options - see {@link MergeOptions}
81
- * @returns the doctype to write, plus a drift report
82
- *
83
- * @example
84
- * ```ts
85
- * const [generated] = convertGraphQLSchema(introspection, { include: ['Uom'] })
86
- * const { doctype, drift } = mergeIntrospectedDoctype(JSON.parse(onDisk), generated)
87
- * if (drift.identityDrift.length) console.warn(drift.identityDrift.join('\n'))
88
- * ```
89
- *
90
- * @public
91
- */
92
- export declare function mergeIntrospectedDoctype(authored: AuthoredDoctype, generated: ConvertedGraphQLDoctype, options?: MergeOptions): MergeResult;
93
- /**
94
- * Render a drift report as human-readable lines. Empty when generation agrees with the doctype.
95
- *
96
- * @param drift - a report from {@link mergeIntrospectedDoctype}
97
- * @returns one line per finding, ready to print
98
- *
99
- * @public
100
- */
101
- export declare function formatDoctypeDrift(drift: DoctypeDrift): string[];
102
- //# sourceMappingURL=merge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/converter/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC5B,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;IACzB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,mGAAmG;IACnG,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,qGAAqG;IACrG,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,mGAAmG;IACnG,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,oEAAoE;IACpE,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,mFAAmF;IACnF,aAAa,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,6FAA6F;AAC7F,MAAM,WAAW,WAAW;IAC3B,iFAAiF;IACjF,OAAO,EAAE,eAAe,CAAA;IACxB,sCAAsC;IACtC,KAAK,EAAE,YAAY,CAAA;CACnB;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACvC,QAAQ,EAAE,eAAe,EACzB,SAAS,EAAE,uBAAuB,EAClC,OAAO,GAAE,YAAiB,GACxB,WAAW,CAgFb;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,EAAE,CAYhE"}