@stonecrop/schema 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/dist/cli.cjs +0 -41
- package/dist/cli.cjs.map +0 -1
- package/dist/converter-DNwpowpq.js +0 -459
- package/dist/converter-DNwpowpq.js.map +0 -1
- package/dist/index-0qWNlDQ5.js +0 -450
- package/dist/index-0qWNlDQ5.js.map +0 -1
- package/dist/index-2UVTfbcY.js +0 -448
- package/dist/index-2UVTfbcY.js.map +0 -1
- package/dist/index-9QjWZ_PC.js +0 -449
- package/dist/index-9QjWZ_PC.js.map +0 -1
- package/dist/index-BCADNO5M.js +0 -449
- package/dist/index-BCADNO5M.js.map +0 -1
- package/dist/index-BRQJGVFR.js +0 -453
- package/dist/index-BRQJGVFR.js.map +0 -1
- package/dist/index-BatnoC-J.js +0 -429
- package/dist/index-BatnoC-J.js.map +0 -1
- package/dist/index-BhMd2_xl.js +0 -454
- package/dist/index-BhMd2_xl.js.map +0 -1
- package/dist/index-COrltkHl.js +0 -401
- package/dist/index-COrltkHl.js.map +0 -1
- package/dist/index-CvN9xK1B.js +0 -453
- package/dist/index-CvN9xK1B.js.map +0 -1
- package/dist/index-CzoRIy1-.js +0 -408
- package/dist/index-CzoRIy1-.js.map +0 -1
- package/dist/index-D6Up-BP5.js +0 -435
- package/dist/index-D6Up-BP5.js.map +0 -1
- package/dist/index-DIb_Z0wI.js +0 -476
- package/dist/index-DIb_Z0wI.js.map +0 -1
- package/dist/index-DNROIEMe.js +0 -449
- package/dist/index-DNROIEMe.js.map +0 -1
- package/dist/index-DUFcQC-H.js +0 -449
- package/dist/index-DUFcQC-H.js.map +0 -1
- package/dist/index-DmD8vmuB.js +0 -449
- package/dist/index-DmD8vmuB.js.map +0 -1
- package/dist/index-XZZbfFxT.js +0 -434
- package/dist/index-XZZbfFxT.js.map +0 -1
- package/dist/index-aeXXzPET.cjs +0 -2
- package/dist/index-aeXXzPET.cjs.map +0 -1
- package/dist/index-neVkjlgB.js +0 -446
- package/dist/index-neVkjlgB.js.map +0 -1
- package/dist/index-xdlVWldg.js +0 -408
- package/dist/index-xdlVWldg.js.map +0 -1
- package/dist/index.cjs +0 -2
- package/dist/index.cjs.map +0 -1
- package/dist/jsonschema.js +0 -225
- package/dist/src/cli.js +0 -210
- package/dist/src/converter/heuristics.js +0 -256
- package/dist/src/converter/index.js +0 -164
- package/dist/src/converter/scalars.js +0 -86
- package/dist/src/converter/types.js +0 -5
- package/dist/src/doctype.js +0 -67
- package/dist/src/field.js +0 -108
- package/dist/src/fieldtype.js +0 -75
- package/dist/src/index.js +0 -12
- package/dist/src/jsonschema.d.ts +0 -95
- package/dist/src/jsonschema.d.ts.map +0 -1
- package/dist/src/naming.js +0 -106
- package/dist/src/tsdoc-metadata.json +0 -11
- package/dist/src/validation.js +0 -60
|
@@ -1,256 +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, } from 'graphql';
|
|
13
|
-
import { buildScalarMap, INTERNAL_SCALARS } from './scalars';
|
|
14
|
-
import { toSlug, camelToLabel } 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
|
-
const SKIP_FIELDS = new Set(['nodeId', '__typename', 'clientMutationId']);
|
|
86
|
-
/**
|
|
87
|
-
* Default heuristic to filter fields on entity types.
|
|
88
|
-
* Skips internal fields that don't represent meaningful data.
|
|
89
|
-
*
|
|
90
|
-
* @param fieldName - The GraphQL field name
|
|
91
|
-
* @param _field - The GraphQL field definition (unused in default implementation)
|
|
92
|
-
* @param _parentType - The parent entity type (unused in default implementation)
|
|
93
|
-
* @returns `true` if this field should be included
|
|
94
|
-
* @public
|
|
95
|
-
*/
|
|
96
|
-
export function defaultIsEntityField(fieldName, _field, _parentType) {
|
|
97
|
-
return !SKIP_FIELDS.has(fieldName);
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Unwrap NonNull and List wrappers from a GraphQL type, tracking nullability.
|
|
101
|
-
*
|
|
102
|
-
* @param type - The GraphQL output type
|
|
103
|
-
* @returns The unwrapped named type, whether it's required, and whether it's a list
|
|
104
|
-
* @internal
|
|
105
|
-
*/
|
|
106
|
-
function unwrapType(type) {
|
|
107
|
-
let required = false;
|
|
108
|
-
let isList = false;
|
|
109
|
-
let current = type;
|
|
110
|
-
// Unwrap outer NonNull
|
|
111
|
-
if (isNonNullType(current)) {
|
|
112
|
-
required = true;
|
|
113
|
-
current = current.ofType;
|
|
114
|
-
}
|
|
115
|
-
// Unwrap List
|
|
116
|
-
if (isListType(current)) {
|
|
117
|
-
isList = true;
|
|
118
|
-
current = current.ofType;
|
|
119
|
-
// Unwrap inner NonNull (e.g., [Type!])
|
|
120
|
-
if (isNonNullType(current)) {
|
|
121
|
-
current = current.ofType;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// At this point, current should be a named type
|
|
125
|
-
return { namedType: current, required, isList };
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Check if a GraphQL object type looks like a Relay Connection type.
|
|
129
|
-
* A connection type has an `edges` field returning a list of edge types,
|
|
130
|
-
* where each edge has a `node` field.
|
|
131
|
-
*
|
|
132
|
-
* @param type - The GraphQL object type to check
|
|
133
|
-
* @returns The node type name if this is a connection, or `undefined`
|
|
134
|
-
* @internal
|
|
135
|
-
*/
|
|
136
|
-
function getConnectionNodeType(type) {
|
|
137
|
-
const fields = type.getFields();
|
|
138
|
-
// Must have an 'edges' field
|
|
139
|
-
const edgesField = fields['edges'];
|
|
140
|
-
if (!edgesField)
|
|
141
|
-
return undefined;
|
|
142
|
-
// edges must be a list
|
|
143
|
-
const { namedType: edgesType, isList: edgesIsList } = unwrapType(edgesField.type);
|
|
144
|
-
if (!edgesIsList || !isObjectType(edgesType))
|
|
145
|
-
return undefined;
|
|
146
|
-
// Each edge must have a 'node' field
|
|
147
|
-
const edgeFields = edgesType.getFields();
|
|
148
|
-
const nodeField = edgeFields['node'];
|
|
149
|
-
if (!nodeField)
|
|
150
|
-
return undefined;
|
|
151
|
-
const { namedType: nodeType } = unwrapType(nodeField.type);
|
|
152
|
-
if (!isObjectType(nodeType))
|
|
153
|
-
return undefined;
|
|
154
|
-
return nodeType.name;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Classify a single GraphQL field into a Stonecrop field definition.
|
|
158
|
-
*
|
|
159
|
-
* Classification rules (in order):
|
|
160
|
-
* 1. Scalar types → look up in merged scalar map
|
|
161
|
-
* 2. Enum types → `Select` with enum values as options
|
|
162
|
-
* 3. Object types that are entities → `Link` with slug as options
|
|
163
|
-
* 4. Object types that are Connections → `Doctype` with node type slug as options
|
|
164
|
-
* 5. List of entity type → `Doctype` with item type slug as options
|
|
165
|
-
* 6. Anything else → `Data` with `_unmapped: true`
|
|
166
|
-
*
|
|
167
|
-
* @param fieldName - The GraphQL field name
|
|
168
|
-
* @param field - The GraphQL field definition
|
|
169
|
-
* @param entityTypes - Set of type names classified as entities
|
|
170
|
-
* @param options - Conversion options (for custom scalars, unmapped meta, etc.)
|
|
171
|
-
* @returns The Stonecrop field definition
|
|
172
|
-
* @public
|
|
173
|
-
*/
|
|
174
|
-
export function classifyFieldType(fieldName, field, entityTypes, options = {}) {
|
|
175
|
-
const { namedType, required, isList } = unwrapType(field.type);
|
|
176
|
-
const scalarMap = buildScalarMap(options.customScalars);
|
|
177
|
-
const base = {
|
|
178
|
-
fieldname: fieldName,
|
|
179
|
-
label: camelToLabel(fieldName),
|
|
180
|
-
component: 'ATextInput',
|
|
181
|
-
fieldtype: 'Data',
|
|
182
|
-
};
|
|
183
|
-
if (required) {
|
|
184
|
-
base.required = true;
|
|
185
|
-
}
|
|
186
|
-
// 1. Scalar types
|
|
187
|
-
if (isScalarType(namedType)) {
|
|
188
|
-
// Skip internal scalars (e.g., Cursor)
|
|
189
|
-
if (INTERNAL_SCALARS.has(namedType.name)) {
|
|
190
|
-
base._unmapped = true;
|
|
191
|
-
if (options.includeUnmappedMeta) {
|
|
192
|
-
base._graphqlType = namedType.name;
|
|
193
|
-
}
|
|
194
|
-
return base;
|
|
195
|
-
}
|
|
196
|
-
const template = scalarMap[namedType.name];
|
|
197
|
-
if (template) {
|
|
198
|
-
base.component = template.component;
|
|
199
|
-
base.fieldtype = template.fieldtype;
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
// Unknown scalar — default to Data with unmapped marker
|
|
203
|
-
base._unmapped = true;
|
|
204
|
-
if (options.includeUnmappedMeta) {
|
|
205
|
-
base._graphqlType = namedType.name;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return base;
|
|
209
|
-
}
|
|
210
|
-
// 2. Enum types → Select
|
|
211
|
-
if (isEnumType(namedType)) {
|
|
212
|
-
base.component = 'ADropdown';
|
|
213
|
-
base.fieldtype = 'Select';
|
|
214
|
-
base.options = namedType.getValues().map(v => v.name);
|
|
215
|
-
return base;
|
|
216
|
-
}
|
|
217
|
-
// 3–5. Object types
|
|
218
|
-
if (isObjectType(namedType)) {
|
|
219
|
-
// 3. Direct reference to an entity type → Link
|
|
220
|
-
if (!isList && entityTypes.has(namedType.name)) {
|
|
221
|
-
base.component = 'ALink';
|
|
222
|
-
base.fieldtype = 'Link';
|
|
223
|
-
base.options = toSlug(namedType.name);
|
|
224
|
-
return base;
|
|
225
|
-
}
|
|
226
|
-
// 4. Connection type → Doctype (child table)
|
|
227
|
-
const connectionNodeTypeName = getConnectionNodeType(namedType);
|
|
228
|
-
if (connectionNodeTypeName && entityTypes.has(connectionNodeTypeName)) {
|
|
229
|
-
base.component = 'ATable';
|
|
230
|
-
base.fieldtype = 'Doctype';
|
|
231
|
-
base.options = toSlug(connectionNodeTypeName);
|
|
232
|
-
base.cardinality = 'many';
|
|
233
|
-
return base;
|
|
234
|
-
}
|
|
235
|
-
// 5. List of entity type → Doctype
|
|
236
|
-
if (isList && entityTypes.has(namedType.name)) {
|
|
237
|
-
base.component = 'ATable';
|
|
238
|
-
base.fieldtype = 'Doctype';
|
|
239
|
-
base.options = toSlug(namedType.name);
|
|
240
|
-
base.cardinality = 'many';
|
|
241
|
-
return base;
|
|
242
|
-
}
|
|
243
|
-
// Unknown object type — mark as unmapped
|
|
244
|
-
base._unmapped = true;
|
|
245
|
-
if (options.includeUnmappedMeta) {
|
|
246
|
-
base._graphqlType = namedType.name;
|
|
247
|
-
}
|
|
248
|
-
return base;
|
|
249
|
-
}
|
|
250
|
-
// Fallback — shouldn't normally be reached
|
|
251
|
-
base._unmapped = true;
|
|
252
|
-
if (options.includeUnmappedMeta) {
|
|
253
|
-
base._graphqlType = namedType.name;
|
|
254
|
-
}
|
|
255
|
-
return base;
|
|
256
|
-
}
|
|
@@ -1,164 +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, pascalToSnake } 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: 'ADecimalInput', fieldtype: 'Decimal' }
|
|
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 deriveTableName = options.deriveTableName ?? ((typeName) => pascalToSnake(typeName));
|
|
84
|
-
const doctypes = [];
|
|
85
|
-
for (const typeName of filteredEntityTypes) {
|
|
86
|
-
const type = typeMap[typeName];
|
|
87
|
-
if (!isObjectType(type))
|
|
88
|
-
continue;
|
|
89
|
-
const fields = type.getFields();
|
|
90
|
-
const typeOverrides = options.typeOverrides?.[typeName];
|
|
91
|
-
const convertedFields = Object.entries(fields)
|
|
92
|
-
.filter(([fieldName, field]) => isEntityField(fieldName, field, type))
|
|
93
|
-
.map(([fieldName, field]) => {
|
|
94
|
-
// Check for full custom classification first
|
|
95
|
-
if (options.classifyField) {
|
|
96
|
-
const custom = options.classifyField(fieldName, field, type);
|
|
97
|
-
if (custom !== null && custom !== undefined) {
|
|
98
|
-
return {
|
|
99
|
-
fieldname: fieldName,
|
|
100
|
-
label: custom.label ?? fieldName,
|
|
101
|
-
component: custom.component ?? 'ATextInput',
|
|
102
|
-
fieldtype: custom.fieldtype ?? 'Data',
|
|
103
|
-
...custom,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Default classification
|
|
108
|
-
const classified = classifyFieldType(fieldName, field, entityTypes, options);
|
|
109
|
-
// Apply per-field overrides
|
|
110
|
-
if (typeOverrides?.[fieldName]) {
|
|
111
|
-
return { ...classified, ...typeOverrides[fieldName] };
|
|
112
|
-
}
|
|
113
|
-
return classified;
|
|
114
|
-
})
|
|
115
|
-
// Clean up internal metadata unless requested
|
|
116
|
-
.map(field => {
|
|
117
|
-
if (!options.includeUnmappedMeta) {
|
|
118
|
-
const { _graphqlType, _unmapped, ...clean } = field;
|
|
119
|
-
return clean;
|
|
120
|
-
}
|
|
121
|
-
return field;
|
|
122
|
-
});
|
|
123
|
-
const doctype = {
|
|
124
|
-
name: typeName,
|
|
125
|
-
slug: toSlug(typeName),
|
|
126
|
-
fields: convertedFields,
|
|
127
|
-
};
|
|
128
|
-
const tableName = deriveTableName(typeName);
|
|
129
|
-
if (tableName) {
|
|
130
|
-
doctype.tableName = tableName;
|
|
131
|
-
}
|
|
132
|
-
if (options.includeUnmappedMeta) {
|
|
133
|
-
doctype._graphqlTypeName = typeName;
|
|
134
|
-
}
|
|
135
|
-
doctypes.push(doctype);
|
|
136
|
-
}
|
|
137
|
-
return doctypes;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Build a GraphQLSchema from either an introspection result or SDL string.
|
|
141
|
-
*
|
|
142
|
-
* @param source - IntrospectionQuery object or SDL string
|
|
143
|
-
* @returns A complete GraphQLSchema
|
|
144
|
-
* @internal
|
|
145
|
-
*/
|
|
146
|
-
function buildGraphQLSchema(source) {
|
|
147
|
-
if (typeof source === 'string') {
|
|
148
|
-
// SDL string
|
|
149
|
-
return buildSchema(source);
|
|
150
|
-
}
|
|
151
|
-
// IntrospectionQuery result
|
|
152
|
-
return buildClientSchema(source);
|
|
153
|
-
}
|
|
154
|
-
// ═══════════════════════════════════════════════════════════════
|
|
155
|
-
// Re-exports
|
|
156
|
-
// ═══════════════════════════════════════════════════════════════
|
|
157
|
-
// Main converter (this file)
|
|
158
|
-
export { convertGraphQLSchema as default };
|
|
159
|
-
// Scalar maps
|
|
160
|
-
export { GQL_SCALAR_MAP, WELL_KNOWN_SCALARS, INTERNAL_SCALARS, buildScalarMap } from './scalars';
|
|
161
|
-
// Heuristics
|
|
162
|
-
export { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
|
|
163
|
-
// Naming utilities
|
|
164
|
-
export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from '../naming';
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GraphQL Scalar Type Mappings
|
|
3
|
-
*
|
|
4
|
-
* Maps standard GraphQL scalars and well-known custom scalars to Stonecrop field types.
|
|
5
|
-
* Source-agnostic — covers scalars commonly emitted by PostGraphile, Hasura, Apollo, etc.
|
|
6
|
-
*
|
|
7
|
-
* Users can extend these via the `customScalars` option in `GraphQLConversionOptions`.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Mapping from standard GraphQL scalar types to Stonecrop field types.
|
|
13
|
-
* These are defined by the GraphQL specification and are always available.
|
|
14
|
-
*
|
|
15
|
-
* @public
|
|
16
|
-
*/
|
|
17
|
-
export const GQL_SCALAR_MAP = {
|
|
18
|
-
String: { component: 'ATextInput', fieldtype: 'Data' },
|
|
19
|
-
Int: { component: 'ANumericInput', fieldtype: 'Int' },
|
|
20
|
-
Float: { component: 'ANumericInput', fieldtype: 'Float' },
|
|
21
|
-
Boolean: { component: 'ACheckbox', fieldtype: 'Check' },
|
|
22
|
-
ID: { component: 'ATextInput', fieldtype: 'Data' },
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Mapping from well-known custom GraphQL scalars to Stonecrop field types.
|
|
26
|
-
* These cover scalars commonly used across GraphQL servers (PostGraphile, Hasura, etc.)
|
|
27
|
-
* without baking in knowledge of any specific server.
|
|
28
|
-
*
|
|
29
|
-
* Entries here have lower precedence than `customScalars` from options, but higher
|
|
30
|
-
* precedence than unknown/unmapped scalars.
|
|
31
|
-
*
|
|
32
|
-
* @public
|
|
33
|
-
*/
|
|
34
|
-
export const WELL_KNOWN_SCALARS = {
|
|
35
|
-
// Arbitrary precision / large numbers
|
|
36
|
-
BigFloat: { component: 'ADecimalInput', fieldtype: 'Decimal' },
|
|
37
|
-
BigDecimal: { component: 'ADecimalInput', fieldtype: 'Decimal' },
|
|
38
|
-
Decimal: { component: 'ADecimalInput', fieldtype: 'Decimal' },
|
|
39
|
-
BigInt: { component: 'ANumericInput', fieldtype: 'Int' },
|
|
40
|
-
Long: { component: 'ANumericInput', fieldtype: 'Int' },
|
|
41
|
-
// Identifiers
|
|
42
|
-
UUID: { component: 'ATextInput', fieldtype: 'Data' },
|
|
43
|
-
// Date / Time
|
|
44
|
-
DateTime: { component: 'ADatetimePicker', fieldtype: 'Datetime' },
|
|
45
|
-
Datetime: { component: 'ADatetimePicker', fieldtype: 'Datetime' },
|
|
46
|
-
Date: { component: 'ADate', fieldtype: 'Date' },
|
|
47
|
-
Time: { component: 'ATimeInput', fieldtype: 'Time' },
|
|
48
|
-
Interval: { component: 'ADurationInput', fieldtype: 'Duration' },
|
|
49
|
-
Duration: { component: 'ADurationInput', fieldtype: 'Duration' },
|
|
50
|
-
// Structured data
|
|
51
|
-
JSON: { component: 'ACodeEditor', fieldtype: 'JSON' },
|
|
52
|
-
JSONObject: { component: 'ACodeEditor', fieldtype: 'JSON' },
|
|
53
|
-
JsonNode: { component: 'ACodeEditor', fieldtype: 'JSON' },
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Set of scalar type names that are internal to GraphQL servers and should be skipped
|
|
57
|
-
* during field conversion (they don't represent meaningful data fields).
|
|
58
|
-
*
|
|
59
|
-
* @public
|
|
60
|
-
*/
|
|
61
|
-
export const INTERNAL_SCALARS = new Set(['Cursor']);
|
|
62
|
-
/**
|
|
63
|
-
* Build a merged scalar map from the built-in maps and user-provided custom scalars.
|
|
64
|
-
* Precedence (highest to lowest): customScalars → GQL_SCALAR_MAP → WELL_KNOWN_SCALARS
|
|
65
|
-
*
|
|
66
|
-
* @param customScalars - User-provided scalar overrides
|
|
67
|
-
* @returns Merged scalar map
|
|
68
|
-
* @public
|
|
69
|
-
*/
|
|
70
|
-
export function buildScalarMap(customScalars) {
|
|
71
|
-
const merged = { ...WELL_KNOWN_SCALARS };
|
|
72
|
-
// Standard scalars override well-known
|
|
73
|
-
for (const [key, value] of Object.entries(GQL_SCALAR_MAP)) {
|
|
74
|
-
merged[key] = value;
|
|
75
|
-
}
|
|
76
|
-
// Custom scalars override everything
|
|
77
|
-
if (customScalars) {
|
|
78
|
-
for (const [key, value] of Object.entries(customScalars)) {
|
|
79
|
-
merged[key] = {
|
|
80
|
-
component: value.component ?? 'ATextInput',
|
|
81
|
-
fieldtype: value.fieldtype ?? 'Data',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return merged;
|
|
86
|
-
}
|
package/dist/src/doctype.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { FieldMeta } from './field';
|
|
3
|
-
/**
|
|
4
|
-
* Action definition within a workflow
|
|
5
|
-
* @public
|
|
6
|
-
*/
|
|
7
|
-
export const ActionDefinition = z
|
|
8
|
-
.object({
|
|
9
|
-
/** Display label for the action */
|
|
10
|
-
label: z.string().min(1),
|
|
11
|
-
/** Handler function name or path */
|
|
12
|
-
handler: z.string().min(1),
|
|
13
|
-
/** Fields that must have values before action can execute */
|
|
14
|
-
requiredFields: z.array(z.string()).optional(),
|
|
15
|
-
/** Workflow states where this action is available */
|
|
16
|
-
allowedStates: z.array(z.string()).optional(),
|
|
17
|
-
/** Whether to show a confirmation dialog */
|
|
18
|
-
confirm: z.boolean().optional(),
|
|
19
|
-
/** Additional arguments for the action */
|
|
20
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
21
|
-
})
|
|
22
|
-
.meta({
|
|
23
|
-
title: 'ActionDefinition',
|
|
24
|
-
description: 'Action definition within a workflow',
|
|
25
|
-
});
|
|
26
|
-
/**
|
|
27
|
-
* Workflow metadata - states and actions for a doctype
|
|
28
|
-
* @public
|
|
29
|
-
*/
|
|
30
|
-
export const WorkflowMeta = z
|
|
31
|
-
.object({
|
|
32
|
-
/** List of workflow states */
|
|
33
|
-
states: z.array(z.string()).optional(),
|
|
34
|
-
/** Actions available in this workflow */
|
|
35
|
-
actions: z.record(z.string(), ActionDefinition).optional(),
|
|
36
|
-
})
|
|
37
|
-
.meta({
|
|
38
|
-
title: 'WorkflowMeta',
|
|
39
|
-
description: 'Workflow metadata - states and actions for a doctype',
|
|
40
|
-
});
|
|
41
|
-
/**
|
|
42
|
-
* Doctype metadata - complete definition of a doctype
|
|
43
|
-
* @public
|
|
44
|
-
*/
|
|
45
|
-
export const DoctypeMeta = z
|
|
46
|
-
.object({
|
|
47
|
-
/** Display name of the doctype */
|
|
48
|
-
name: z.string().min(1),
|
|
49
|
-
/** URL-friendly slug (kebab-case) */
|
|
50
|
-
slug: z.string().min(1).optional(),
|
|
51
|
-
/** Database table name */
|
|
52
|
-
tableName: z.string().optional(),
|
|
53
|
-
/** Field definitions */
|
|
54
|
-
fields: z.array(FieldMeta),
|
|
55
|
-
/** Workflow configuration */
|
|
56
|
-
workflow: WorkflowMeta.optional(),
|
|
57
|
-
/** Parent doctype for inheritance */
|
|
58
|
-
inherits: z.string().optional(),
|
|
59
|
-
/** Doctype to use for list views */
|
|
60
|
-
listDoctype: z.string().optional(),
|
|
61
|
-
/** Parent doctype for child tables */
|
|
62
|
-
parentDoctype: z.string().optional(),
|
|
63
|
-
})
|
|
64
|
-
.meta({
|
|
65
|
-
title: 'DoctypeMeta',
|
|
66
|
-
description: 'Doctype metadata - complete definition of a doctype',
|
|
67
|
-
});
|
package/dist/src/field.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { StonecropFieldType } from './fieldtype';
|
|
3
|
-
/**
|
|
4
|
-
* Field options - flexible bag for type-specific configuration.
|
|
5
|
-
*
|
|
6
|
-
* Usage by fieldtype:
|
|
7
|
-
* - Link/Doctype: target doctype slug as string ("customer", "sales-order-item")
|
|
8
|
-
* - Select: array of choices (["Draft", "Submitted", "Cancelled"])
|
|
9
|
-
* - Decimal: config object (\{ precision: 10, scale: 2 \})
|
|
10
|
-
* - Code: config object (\{ language: "python" \})
|
|
11
|
-
*
|
|
12
|
-
* @public
|
|
13
|
-
*/
|
|
14
|
-
export const FieldOptions = z
|
|
15
|
-
.union([
|
|
16
|
-
z.string(), // Link/Doctype target: "customer"
|
|
17
|
-
z.array(z.string()), // Select choices: ["A", "B", "C"]
|
|
18
|
-
z.record(z.string(), z.unknown()), // Config: \{ precision: 10, scale: 2 \}
|
|
19
|
-
])
|
|
20
|
-
.meta({
|
|
21
|
-
title: 'FieldOptions',
|
|
22
|
-
description: 'Field options - flexible bag for type-specific configuration',
|
|
23
|
-
});
|
|
24
|
-
/**
|
|
25
|
-
* Validation configuration for form fields
|
|
26
|
-
* @public
|
|
27
|
-
*/
|
|
28
|
-
export const FieldValidation = z
|
|
29
|
-
.looseObject({
|
|
30
|
-
/** Error message to display when validation fails */
|
|
31
|
-
errorMessage: z.string(),
|
|
32
|
-
})
|
|
33
|
-
.meta({
|
|
34
|
-
title: 'FieldValidation',
|
|
35
|
-
description: 'Validation configuration for form fields',
|
|
36
|
-
});
|
|
37
|
-
/**
|
|
38
|
-
* Unified field metadata - the single source of truth for field definitions.
|
|
39
|
-
* Works for both forms (AForm) and tables (ATable).
|
|
40
|
-
*
|
|
41
|
-
* Core principle: "Text" is "Text" regardless of rendering context.
|
|
42
|
-
*
|
|
43
|
-
* @public
|
|
44
|
-
*/
|
|
45
|
-
export const FieldMeta = z
|
|
46
|
-
.object({
|
|
47
|
-
// === CORE (required) ===
|
|
48
|
-
/** Unique identifier for the field within its doctype */
|
|
49
|
-
fieldname: z.string().min(1),
|
|
50
|
-
/** Semantic field type - determines behavior and default component */
|
|
51
|
-
fieldtype: StonecropFieldType,
|
|
52
|
-
// === COMPONENT (optional - derived from fieldtype when not specified) ===
|
|
53
|
-
/** Vue component to render this field. If not specified, derived from TYPE_MAP */
|
|
54
|
-
component: z.string().optional(),
|
|
55
|
-
// === DISPLAY ===
|
|
56
|
-
/** Human-readable label for the field */
|
|
57
|
-
label: z.string().optional(),
|
|
58
|
-
/** Width of the field (CSS value, e.g., "40ch", "200px") */
|
|
59
|
-
width: z.string().optional(),
|
|
60
|
-
/** Text alignment within the field */
|
|
61
|
-
align: z.enum(['left', 'center', 'right', 'start', 'end']).optional(),
|
|
62
|
-
// === BEHAVIOR ===
|
|
63
|
-
/** Whether the field is required */
|
|
64
|
-
required: z.boolean().optional(),
|
|
65
|
-
/** Whether the field is read-only */
|
|
66
|
-
readOnly: z.boolean().optional(),
|
|
67
|
-
/** Whether the field is editable (for table cells) */
|
|
68
|
-
edit: z.boolean().optional(),
|
|
69
|
-
/** Whether the field is hidden from the UI */
|
|
70
|
-
hidden: z.boolean().optional(),
|
|
71
|
-
// === VALUE ===
|
|
72
|
-
/** Current value of the field */
|
|
73
|
-
value: z.unknown().optional(),
|
|
74
|
-
/** Default value for new records */
|
|
75
|
-
default: z.unknown().optional(),
|
|
76
|
-
// === TYPE-SPECIFIC ===
|
|
77
|
-
/**
|
|
78
|
-
* Type-specific options:
|
|
79
|
-
* - Link: target doctype slug ("customer")
|
|
80
|
-
* - Doctype: child doctype slug ("sales-order-item")
|
|
81
|
-
* - Select: choices array (["Draft", "Submitted"])
|
|
82
|
-
* - Decimal: \{ precision, scale \}
|
|
83
|
-
* - Code: \{ language \}
|
|
84
|
-
*/
|
|
85
|
-
options: FieldOptions.optional(),
|
|
86
|
-
/**
|
|
87
|
-
* Cardinality for Doctype fields:
|
|
88
|
-
* - 'one': 1:1 nested form (default)
|
|
89
|
-
* - 'many': 1:many child table
|
|
90
|
-
*/
|
|
91
|
-
cardinality: z.enum(['one', 'many']).optional(),
|
|
92
|
-
/**
|
|
93
|
-
* Input mask pattern. Accepts either a plain mask string or a stringified
|
|
94
|
-
* arrow function that receives `locale` and returns a mask string.
|
|
95
|
-
*
|
|
96
|
-
* Plain pattern: `"##/##/####"`
|
|
97
|
-
*
|
|
98
|
-
* Function pattern: `"(locale) => locale === 'en-US' ? '(###) ###-####' : '####-######'"`
|
|
99
|
-
*/
|
|
100
|
-
mask: z.string().optional(),
|
|
101
|
-
// === VALIDATION ===
|
|
102
|
-
/** Validation configuration */
|
|
103
|
-
validation: FieldValidation.optional(),
|
|
104
|
-
})
|
|
105
|
-
.meta({
|
|
106
|
-
title: 'FieldMeta',
|
|
107
|
-
description: 'Unified field metadata - the single source of truth for field definitions, works for both forms (AForm) and tables (ATable)',
|
|
108
|
-
});
|