@stonecrop/schema 0.11.7 → 0.11.8
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/dist/schema.tsbuildinfo +1 -1
- package/dist/src/cli.js +210 -0
- package/dist/src/index.js +8 -0
- package/dist/tsdoc-metadata.json +1 -1
- package/package.json +10 -10
- /package/dist/{converter → src/converter}/heuristics.js +0 -0
- /package/dist/{converter → src/converter}/index.js +0 -0
- /package/dist/{converter → src/converter}/scalars.js +0 -0
- /package/dist/{converter → src/converter}/types.js +0 -0
- /package/dist/{doctype.js → src/doctype.js} +0 -0
- /package/dist/{field.js → src/field.js} +0 -0
- /package/dist/{fieldtype.js → src/fieldtype.js} +0 -0
- /package/dist/{naming.js → src/naming.js} +0 -0
- /package/dist/{validation.js → src/validation.js} +0 -0
package/dist/schema.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"6.0.3"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stonecrop Schema CLI
|
|
4
|
+
*
|
|
5
|
+
* Converts GraphQL introspection results to Stonecrop doctype JSON schemas.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* stonecrop-schema generate --endpoint <url> --output <dir>
|
|
9
|
+
* stonecrop-schema generate --introspection <file.json> --output <dir>
|
|
10
|
+
* stonecrop-schema generate --sdl <file.graphql> --output <dir>
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { resolve, join } from 'node:path';
|
|
16
|
+
import { parseArgs } from 'node:util';
|
|
17
|
+
import { getIntrospectionQuery } from 'graphql';
|
|
18
|
+
import { convertGraphQLSchema } from './converter/index';
|
|
19
|
+
import { validateDoctype } from './validation';
|
|
20
|
+
/**
|
|
21
|
+
* Fetch an introspection result from a live GraphQL endpoint.
|
|
22
|
+
*
|
|
23
|
+
* @param endpoint - The GraphQL endpoint URL
|
|
24
|
+
* @param headers - Optional HTTP headers
|
|
25
|
+
* @returns The introspection query result
|
|
26
|
+
*/
|
|
27
|
+
async function fetchIntrospection(endpoint, headers) {
|
|
28
|
+
const response = await fetch(endpoint, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...headers,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
query: getIntrospectionQuery(),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const json = (await response.json());
|
|
42
|
+
if (json.errors?.length) {
|
|
43
|
+
throw new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
if (!json.data) {
|
|
46
|
+
throw new Error('No data in introspection response');
|
|
47
|
+
}
|
|
48
|
+
return json.data;
|
|
49
|
+
}
|
|
50
|
+
async function main() {
|
|
51
|
+
const { values, positionals } = parseArgs({
|
|
52
|
+
allowPositionals: true,
|
|
53
|
+
options: {
|
|
54
|
+
endpoint: { type: 'string', short: 'e' },
|
|
55
|
+
introspection: { type: 'string', short: 'i' },
|
|
56
|
+
sdl: { type: 'string', short: 's' },
|
|
57
|
+
output: { type: 'string', short: 'o' },
|
|
58
|
+
include: { type: 'string' },
|
|
59
|
+
exclude: { type: 'string' },
|
|
60
|
+
overrides: { type: 'string' },
|
|
61
|
+
'custom-scalars': { type: 'string' },
|
|
62
|
+
'include-unmapped': { type: 'boolean', default: false },
|
|
63
|
+
help: { type: 'boolean', short: 'h' },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const command = positionals[0];
|
|
67
|
+
if (values.help || !command) {
|
|
68
|
+
printHelp();
|
|
69
|
+
process.exit(command ? 0 : 1);
|
|
70
|
+
}
|
|
71
|
+
if (command !== 'generate') {
|
|
72
|
+
console.error(`Unknown command: ${command}`);
|
|
73
|
+
console.error('Available commands: generate');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
// Determine source
|
|
77
|
+
const sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length;
|
|
78
|
+
if (sourceCount !== 1) {
|
|
79
|
+
console.error('Exactly one of --endpoint, --introspection, or --sdl must be provided');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
if (!values.output) {
|
|
83
|
+
console.error('--output <dir> is required');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const outputDir = resolve(values.output);
|
|
87
|
+
// Build conversion options
|
|
88
|
+
const options = {
|
|
89
|
+
includeUnmappedMeta: values['include-unmapped'],
|
|
90
|
+
};
|
|
91
|
+
if (values.include) {
|
|
92
|
+
options.include = values.include.split(',').map(s => s.trim());
|
|
93
|
+
}
|
|
94
|
+
if (values.exclude) {
|
|
95
|
+
options.exclude = values.exclude.split(',').map(s => s.trim());
|
|
96
|
+
}
|
|
97
|
+
if (values.overrides) {
|
|
98
|
+
const overridesPath = resolve(values.overrides);
|
|
99
|
+
const overridesContent = readFileSync(overridesPath, 'utf-8');
|
|
100
|
+
options.typeOverrides = JSON.parse(overridesContent);
|
|
101
|
+
}
|
|
102
|
+
if (values['custom-scalars']) {
|
|
103
|
+
const scalarsPath = resolve(values['custom-scalars']);
|
|
104
|
+
const scalarsContent = readFileSync(scalarsPath, 'utf-8');
|
|
105
|
+
options.customScalars = JSON.parse(scalarsContent);
|
|
106
|
+
}
|
|
107
|
+
// Resolve source
|
|
108
|
+
let source;
|
|
109
|
+
if (values.endpoint) {
|
|
110
|
+
console.log(`Fetching introspection from ${values.endpoint}...`);
|
|
111
|
+
source = await fetchIntrospection(values.endpoint);
|
|
112
|
+
}
|
|
113
|
+
else if (values.introspection) {
|
|
114
|
+
const filePath = resolve(values.introspection);
|
|
115
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
116
|
+
const parsed = JSON.parse(content);
|
|
117
|
+
// Handle both { data: { __schema: ... } } and { __schema: ... } formats
|
|
118
|
+
source = parsed.data ?? parsed;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const filePath = resolve(values.sdl);
|
|
122
|
+
source = readFileSync(filePath, 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
// Convert
|
|
125
|
+
const doctypes = convertGraphQLSchema(source, options);
|
|
126
|
+
if (doctypes.length === 0) {
|
|
127
|
+
console.warn('No entity types found in the schema. Check your include/exclude filters.');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
// Write output
|
|
131
|
+
if (!existsSync(outputDir)) {
|
|
132
|
+
mkdirSync(outputDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
let warnings = 0;
|
|
135
|
+
let errors = 0;
|
|
136
|
+
for (const doctype of doctypes) {
|
|
137
|
+
const fileName = `${doctype.slug}.json`;
|
|
138
|
+
const filePath = join(outputDir, fileName);
|
|
139
|
+
const json = JSON.stringify(doctype, null, '\t');
|
|
140
|
+
writeFileSync(filePath, json + '\n', 'utf-8');
|
|
141
|
+
// Validate the output
|
|
142
|
+
const validation = validateDoctype(doctype);
|
|
143
|
+
if (!validation.success) {
|
|
144
|
+
errors++;
|
|
145
|
+
console.error(` ERROR: ${fileName} failed validation:`);
|
|
146
|
+
for (const err of validation.errors) {
|
|
147
|
+
console.error(` ${err.path.join('.')}: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Check for unmapped fields
|
|
152
|
+
const unmappedFields = doctype.fields.filter((f) => f._unmapped);
|
|
153
|
+
if (unmappedFields.length > 0) {
|
|
154
|
+
warnings++;
|
|
155
|
+
console.warn(` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields
|
|
156
|
+
.map((f) => f.fieldname)
|
|
157
|
+
.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log(`\nGenerated ${doctypes.length} doctype(s) in ${outputDir}` +
|
|
162
|
+
(warnings ? ` (${warnings} with warnings)` : '') +
|
|
163
|
+
(errors ? ` (${errors} with errors)` : ''));
|
|
164
|
+
if (errors > 0) {
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function printHelp() {
|
|
169
|
+
console.log(`
|
|
170
|
+
stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
|
|
171
|
+
|
|
172
|
+
USAGE:
|
|
173
|
+
stonecrop-schema generate [options]
|
|
174
|
+
|
|
175
|
+
SOURCE (exactly one required):
|
|
176
|
+
--endpoint, -e <url> Fetch introspection from a live GraphQL endpoint
|
|
177
|
+
--introspection, -i <file> Read from a saved introspection JSON file
|
|
178
|
+
--sdl, -s <file> Read from a GraphQL SDL (.graphql) file
|
|
179
|
+
|
|
180
|
+
OUTPUT:
|
|
181
|
+
--output, -o <dir> Directory to write doctype JSON files (required)
|
|
182
|
+
|
|
183
|
+
OPTIONS:
|
|
184
|
+
--include <types> Comma-separated list of type names to include
|
|
185
|
+
--exclude <types> Comma-separated list of type names to exclude
|
|
186
|
+
--overrides <file> JSON file with per-type field overrides
|
|
187
|
+
--custom-scalars <file> JSON file mapping custom scalar names to field templates
|
|
188
|
+
--include-unmapped Include _graphqlType metadata on unmapped fields
|
|
189
|
+
--help, -h Show this help message
|
|
190
|
+
|
|
191
|
+
EXAMPLES:
|
|
192
|
+
# From a live PostGraphile server
|
|
193
|
+
stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas
|
|
194
|
+
|
|
195
|
+
# From a saved introspection result
|
|
196
|
+
stonecrop-schema generate -i introspection.json -o ./schemas
|
|
197
|
+
|
|
198
|
+
# From an SDL file with custom scalars
|
|
199
|
+
stonecrop-schema generate -s schema.graphql -o ./schemas \\
|
|
200
|
+
--custom-scalars custom-scalars.json
|
|
201
|
+
|
|
202
|
+
# Only convert specific types
|
|
203
|
+
stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\
|
|
204
|
+
--include "User,Post,Comment"
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
207
|
+
main().catch(err => {
|
|
208
|
+
console.error('Error:', err.message);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Field types
|
|
2
|
+
export { StonecropFieldType, BUILTIN_FIELD_TYPES, TYPE_MAP, getDefaultComponent, resolveComponent, isBuiltinFieldType, } from './fieldtype';
|
|
3
|
+
// Validation helpers
|
|
4
|
+
export { parseDoctype, parseField, validateDoctype, validateField, } from './validation';
|
|
5
|
+
// GraphQL to Doctype conversion
|
|
6
|
+
export { buildScalarMap, classifyFieldType, convertGraphQLSchema, defaultIsEntityField, defaultIsEntityType, GQL_SCALAR_MAP, INTERNAL_SCALARS, WELL_KNOWN_SCALARS, } from './converter';
|
|
7
|
+
// Naming utilities
|
|
8
|
+
export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from './naming';
|
package/dist/tsdoc-metadata.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/schema",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/schema.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
"description": "Stonecrop schema definitions and validation tooling",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"graphql": "^16.
|
|
21
|
+
"graphql": "^16.13.2",
|
|
22
22
|
"zod": "^4.3.6"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@microsoft/api-documenter": "^7.
|
|
26
|
-
"@rushstack/heft": "^1.2.
|
|
27
|
-
"@types/node": "^22.19.
|
|
28
|
-
"@vitest/coverage-istanbul": "^4.
|
|
29
|
-
"jsdom": "^
|
|
30
|
-
"typescript": "^
|
|
31
|
-
"vite": "^7.3.
|
|
32
|
-
"vitest": "^4.
|
|
25
|
+
"@microsoft/api-documenter": "^7.30.5",
|
|
26
|
+
"@rushstack/heft": "^1.2.17",
|
|
27
|
+
"@types/node": "^22.19.17",
|
|
28
|
+
"@vitest/coverage-istanbul": "^4.1.5",
|
|
29
|
+
"jsdom": "^29.1.1",
|
|
30
|
+
"typescript": "^6.0.3",
|
|
31
|
+
"vite": "^7.3.2",
|
|
32
|
+
"vitest": "^4.1.5",
|
|
33
33
|
"stonecrop-rig": "0.7.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|