@stonecrop/schema 0.30.0 → 0.32.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 +10 -10
- package/dist/cli.js +194 -121
- package/dist/cli.js.map +1 -1
- package/dist/converter-CLwduvT_.js +2102 -0
- package/dist/converter-CLwduvT_.js.map +1 -0
- package/dist/flatten-Bx2cfvw3.js +37 -0
- package/dist/flatten-Bx2cfvw3.js.map +1 -0
- package/dist/index.js +123 -117
- package/dist/index.js.map +1 -1
- package/dist/record-BQOOi83C.js +134 -0
- package/dist/record-BQOOi83C.js.map +1 -0
- package/dist/record.js +2 -6
- package/dist/tsdoc-metadata.json +1 -1
- package/package.json +34 -18
- package/dist/flatten-C1MjkzFh.js +0 -10
- package/dist/flatten-C1MjkzFh.js.map +0 -1
- package/dist/record-Bc0lI9Rq.js +0 -61
- package/dist/record-Bc0lI9Rq.js.map +0 -1
- package/dist/record.js.map +0 -1
- package/dist/schema.tsbuildinfo +0 -1
- package/dist/src/badge.d.ts +0 -74
- package/dist/src/badge.d.ts.map +0 -1
- package/dist/src/badge.js +0 -158
- package/dist/src/cli.d.ts +0 -3
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -292
- package/dist/src/column-schema.d.ts +0 -163
- package/dist/src/column-schema.d.ts.map +0 -1
- package/dist/src/column-schema.js +0 -0
- package/dist/src/component-meta.d.ts +0 -96
- package/dist/src/component-meta.d.ts.map +0 -1
- package/dist/src/component-meta.js +0 -88
- package/dist/src/converter/aggregate.d.ts +0 -127
- package/dist/src/converter/aggregate.d.ts.map +0 -1
- package/dist/src/converter/aggregate.js +0 -235
- package/dist/src/converter/authored.d.ts +0 -43
- package/dist/src/converter/authored.d.ts.map +0 -1
- package/dist/src/converter/authored.js +0 -52
- package/dist/src/converter/heuristics.d.ts +0 -60
- package/dist/src/converter/heuristics.d.ts.map +0 -1
- package/dist/src/converter/heuristics.js +0 -304
- package/dist/src/converter/index.d.ts +0 -51
- package/dist/src/converter/index.d.ts.map +0 -1
- package/dist/src/converter/index.js +0 -195
- package/dist/src/converter/merge.d.ts +0 -102
- package/dist/src/converter/merge.d.ts.map +0 -1
- package/dist/src/converter/merge.js +0 -136
- package/dist/src/converter/scalars.d.ts +0 -46
- package/dist/src/converter/scalars.d.ts.map +0 -1
- package/dist/src/converter/scalars.js +0 -83
- package/dist/src/converter/types.d.ts +0 -157
- package/dist/src/converter/types.d.ts.map +0 -1
- package/dist/src/converter/types.js +0 -5
- package/dist/src/doctype.d.ts +0 -516
- package/dist/src/doctype.d.ts.map +0 -1
- package/dist/src/doctype.js +0 -343
- package/dist/src/field.d.ts +0 -423
- package/dist/src/field.d.ts.map +0 -1
- package/dist/src/field.js +0 -378
- package/dist/src/flatten.d.ts +0 -29
- package/dist/src/flatten.d.ts.map +0 -1
- package/dist/src/flatten.js +0 -38
- package/dist/src/index.d.ts +0 -16
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -20
- package/dist/src/mode.d.ts +0 -15
- package/dist/src/mode.d.ts.map +0 -1
- package/dist/src/mode.js +0 -0
- package/dist/src/naming.d.ts +0 -80
- package/dist/src/naming.d.ts.map +0 -1
- package/dist/src/naming.js +0 -106
- package/dist/src/record.d.ts +0 -29
- package/dist/src/record.d.ts.map +0 -1
- package/dist/src/record.js +0 -55
- package/dist/src/table.d.ts +0 -33
- package/dist/src/table.d.ts.map +0 -1
- package/dist/src/table.js +0 -25
- package/dist/src/validation.d.ts +0 -54
- package/dist/src/validation.d.ts.map +0 -1
- package/dist/src/validation.js +0 -60
- package/dist/validation-C9P__pRF.js +0 -994
- 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"}
|