@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.
- 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
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ This package is schema-only and has no UI dependencies - it can be used in both
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# From the monorepo root
|
|
21
|
-
|
|
20
|
+
# From the monorepo root, which links the workspace copy
|
|
21
|
+
pnpm install
|
|
22
22
|
|
|
23
|
-
# Or
|
|
23
|
+
# Or, in a consuming project
|
|
24
24
|
pnpm add @stonecrop/schema
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -437,20 +437,20 @@ This package provides the type system used throughout Stonecrop:
|
|
|
437
437
|
## Development
|
|
438
438
|
|
|
439
439
|
```bash
|
|
440
|
-
# Install dependencies
|
|
441
|
-
|
|
440
|
+
# Install dependencies (from the repo root)
|
|
441
|
+
pnpm install
|
|
442
442
|
|
|
443
|
-
# Build
|
|
444
|
-
|
|
443
|
+
# Build (a Vite+ task, so `pnpm run build` does not resolve it)
|
|
444
|
+
pnpm exec vp run build
|
|
445
445
|
|
|
446
446
|
# Run tests
|
|
447
|
-
|
|
447
|
+
pnpm run test
|
|
448
448
|
|
|
449
449
|
# Watch mode
|
|
450
|
-
|
|
450
|
+
pnpm run test:watch
|
|
451
451
|
|
|
452
452
|
# Generate API documentation
|
|
453
|
-
|
|
453
|
+
pnpm run docs
|
|
454
454
|
```
|
|
455
455
|
|
|
456
456
|
## TypeScript Support
|
package/dist/cli.js
CHANGED
|
@@ -1,124 +1,194 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
import { G as stripFieldKind, _ as validateDoctype, i as authoredPrimaryKey, n as formatDoctypeDrift, r as mergeIntrospectedDoctype, s as planGeneration, t as convertGraphQLSchema } from "./converter-CLwduvT_.js";
|
|
3
|
+
import { getIntrospectionQuery } from "graphql";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
/**
|
|
9
|
+
* Stonecrop Schema CLI
|
|
10
|
+
*
|
|
11
|
+
* Converts GraphQL introspection results to Stonecrop doctype JSON schemas.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* stonecrop-schema generate --endpoint <url> --output <dir>
|
|
15
|
+
* stonecrop-schema generate --introspection <file.json> --output <dir>
|
|
16
|
+
* stonecrop-schema generate --sdl <file.graphql> --output <dir>
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
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({ query: getIntrospectionQuery() })
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) throw new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`);
|
|
37
|
+
const json = await response.json();
|
|
38
|
+
if (json.errors?.length) throw new Error(`GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
39
|
+
if (!json.data) throw new Error("No data in introspection response");
|
|
40
|
+
return json.data;
|
|
26
41
|
}
|
|
27
|
-
async function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
42
|
+
async function main() {
|
|
43
|
+
const { values, positionals } = parseArgs({
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
options: {
|
|
46
|
+
endpoint: {
|
|
47
|
+
type: "string",
|
|
48
|
+
short: "e"
|
|
49
|
+
},
|
|
50
|
+
introspection: {
|
|
51
|
+
type: "string",
|
|
52
|
+
short: "i"
|
|
53
|
+
},
|
|
54
|
+
sdl: {
|
|
55
|
+
type: "string",
|
|
56
|
+
short: "s"
|
|
57
|
+
},
|
|
58
|
+
output: {
|
|
59
|
+
type: "string",
|
|
60
|
+
short: "o"
|
|
61
|
+
},
|
|
62
|
+
include: { type: "string" },
|
|
63
|
+
exclude: { type: "string" },
|
|
64
|
+
names: { type: "string" },
|
|
65
|
+
"custom-scalars": { type: "string" },
|
|
66
|
+
"include-unmapped": {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
default: false
|
|
69
|
+
},
|
|
70
|
+
"no-aggregates": {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
default: false
|
|
73
|
+
},
|
|
74
|
+
check: {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
default: false
|
|
77
|
+
},
|
|
78
|
+
help: {
|
|
79
|
+
type: "boolean",
|
|
80
|
+
short: "h"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const command = positionals[0];
|
|
85
|
+
if (values.help || !command) {
|
|
86
|
+
printHelp();
|
|
87
|
+
process.exit(command ? 0 : 1);
|
|
88
|
+
}
|
|
89
|
+
if (command !== "generate") {
|
|
90
|
+
console.error(`Unknown command: ${command}`);
|
|
91
|
+
console.error("Available commands: generate");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if ([
|
|
95
|
+
values.endpoint,
|
|
96
|
+
values.introspection,
|
|
97
|
+
values.sdl
|
|
98
|
+
].filter(Boolean).length !== 1) {
|
|
99
|
+
console.error("Exactly one of --endpoint, --introspection, or --sdl must be provided");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
if (!values.output) {
|
|
103
|
+
console.error("--output <dir> is required");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const outputDir = resolve(values.output);
|
|
107
|
+
const options = { includeUnmappedMeta: values["include-unmapped"] };
|
|
108
|
+
if (values.include) options.include = values.include.split(",").map((s) => s.trim());
|
|
109
|
+
if (values.exclude) options.exclude = values.exclude.split(",").map((s) => s.trim());
|
|
110
|
+
if (values.names) {
|
|
111
|
+
const namesPath = resolve(values.names);
|
|
112
|
+
options.doctypeNames = JSON.parse(readFileSync(namesPath, "utf-8"));
|
|
113
|
+
}
|
|
114
|
+
options.onWarning = (message) => console.warn(` WARN: ${message}`);
|
|
115
|
+
if (values["custom-scalars"]) {
|
|
116
|
+
const scalarsPath = resolve(values["custom-scalars"]);
|
|
117
|
+
const scalarsContent = readFileSync(scalarsPath, "utf-8");
|
|
118
|
+
options.customScalars = JSON.parse(scalarsContent);
|
|
119
|
+
}
|
|
120
|
+
let source;
|
|
121
|
+
if (values.endpoint) {
|
|
122
|
+
console.log(`Fetching introspection from ${values.endpoint}...`);
|
|
123
|
+
source = await fetchIntrospection(values.endpoint);
|
|
124
|
+
} else if (values.introspection) {
|
|
125
|
+
const filePath = resolve(values.introspection);
|
|
126
|
+
const content = readFileSync(filePath, "utf-8");
|
|
127
|
+
const parsed = JSON.parse(content);
|
|
128
|
+
source = parsed.data ?? parsed;
|
|
129
|
+
} else {
|
|
130
|
+
const filePath = resolve(values.sdl);
|
|
131
|
+
source = readFileSync(filePath, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
const entities = convertGraphQLSchema(source, options);
|
|
134
|
+
if (entities.length === 0) {
|
|
135
|
+
console.warn("No entity types found in the schema. Check your include/exclude filters.");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
const identity = {};
|
|
139
|
+
for (const entity of entities) {
|
|
140
|
+
const entityPath = join(outputDir, `${entity.slug}.json`);
|
|
141
|
+
if (!existsSync(entityPath)) continue;
|
|
142
|
+
const declared = authoredPrimaryKey(JSON.parse(readFileSync(entityPath, "utf-8")));
|
|
143
|
+
if (declared !== void 0) identity[entity.name] = declared;
|
|
144
|
+
}
|
|
145
|
+
const doctypes = planGeneration(entities, {
|
|
146
|
+
noAggregates: values["no-aggregates"],
|
|
147
|
+
identity,
|
|
148
|
+
onWarning: (message) => console.warn(` WARN: ${message}`)
|
|
149
|
+
});
|
|
150
|
+
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
151
|
+
let warnings = 0;
|
|
152
|
+
let errors = 0;
|
|
153
|
+
let changed = 0;
|
|
154
|
+
const driftLines = [];
|
|
155
|
+
for (const { generated, basis, subset } of doctypes) {
|
|
156
|
+
const fileName = `${generated.slug}.json`;
|
|
157
|
+
const filePath = join(outputDir, fileName);
|
|
158
|
+
let output = generated;
|
|
159
|
+
if (existsSync(filePath)) {
|
|
160
|
+
const { doctype: merged, drift } = mergeIntrospectedDoctype(JSON.parse(readFileSync(filePath, "utf-8")), basis, { subset });
|
|
161
|
+
output = merged;
|
|
162
|
+
driftLines.push(...formatDoctypeDrift(drift));
|
|
163
|
+
}
|
|
164
|
+
const serializable = { ...output };
|
|
165
|
+
if (Array.isArray(serializable.fields)) serializable.fields = serializable.fields.map(stripFieldKind);
|
|
166
|
+
const json = JSON.stringify(serializable, null, " ") + "\n";
|
|
167
|
+
const unchanged = existsSync(filePath) && readFileSync(filePath, "utf-8") === json;
|
|
168
|
+
if (!unchanged) changed++;
|
|
169
|
+
if (!values.check && !unchanged) writeFileSync(filePath, json, "utf-8");
|
|
170
|
+
const validation = validateDoctype(output);
|
|
171
|
+
if (!validation.success) {
|
|
172
|
+
errors++;
|
|
173
|
+
console.error(` ERROR: ${fileName} failed validation:`);
|
|
174
|
+
for (const err of validation.errors) console.error(` ${err.path.join(".")}: ${err.message}`);
|
|
175
|
+
} else {
|
|
176
|
+
const unmappedFields = generated.fields.filter((f) => f._unmapped);
|
|
177
|
+
if (unmappedFields.length > 0) {
|
|
178
|
+
warnings++;
|
|
179
|
+
console.warn(` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields.map((f) => f.fieldname).join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (driftLines.length > 0) {
|
|
184
|
+
console.log("\nDrift between the authored doctypes and the schema (reported, not applied):");
|
|
185
|
+
for (const line of driftLines) console.log(line);
|
|
186
|
+
}
|
|
187
|
+
console.log(`\n${values.check ? "Checked" : "Generated"} ${doctypes.length} doctype(s) in ${outputDir}` + (changed ? ` (${changed} ${values.check ? "would change" : "written"})` : " (all up to date)") + (warnings ? ` (${warnings} with warnings)` : "") + (errors ? ` (${errors} with errors)` : ""));
|
|
188
|
+
if (errors > 0 || values.check && changed > 0) process.exit(1);
|
|
119
189
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
190
|
+
function printHelp() {
|
|
191
|
+
console.log(`
|
|
122
192
|
stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
|
|
123
193
|
|
|
124
194
|
USAGE:
|
|
@@ -172,7 +242,10 @@ EXAMPLES:
|
|
|
172
242
|
--include "User,Post,Comment"
|
|
173
243
|
`);
|
|
174
244
|
}
|
|
175
|
-
|
|
176
|
-
|
|
245
|
+
main().catch((err) => {
|
|
246
|
+
console.error("Error:", err.message);
|
|
247
|
+
process.exit(1);
|
|
177
248
|
});
|
|
178
|
-
//#
|
|
249
|
+
//#endregion
|
|
250
|
+
|
|
251
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/* oxlint-disable no-console */\n\n/**\n * Stonecrop Schema CLI\n *\n * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.\n *\n * Usage:\n * stonecrop-schema generate --endpoint <url> --output <dir>\n * stonecrop-schema generate --introspection <file.json> --output <dir>\n * stonecrop-schema generate --sdl <file.graphql> --output <dir>\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { getIntrospectionQuery, type IntrospectionQuery } from 'graphql'\n\nimport { authoredPrimaryKey } from './converter/authored'\nimport { convertGraphQLSchema, formatDoctypeDrift, mergeIntrospectedDoctype, planGeneration } from './converter/index'\nimport { stripFieldKind } from './field'\nimport { validateDoctype } from './validation'\nimport type { GraphQLConversionOptions } from './converter/types'\n\n/**\n * Fetch an introspection result from a live GraphQL endpoint.\n *\n * @param endpoint - The GraphQL endpoint URL\n * @param headers - Optional HTTP headers\n * @returns The introspection query result\n */\nasync function fetchIntrospection(endpoint: string, headers?: Record<string, string>): Promise<IntrospectionQuery> {\n\tconst response = await fetch(endpoint, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...headers,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tquery: getIntrospectionQuery(),\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`)\n\t}\n\n\tconst json: { data?: IntrospectionQuery; errors?: Array<{ message: string }> } = await response.json()\n\n\tif (json.errors?.length) {\n\t\tthrow new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`)\n\t}\n\n\tif (!json.data) {\n\t\tthrow new Error('No data in introspection response')\n\t}\n\n\treturn json.data\n}\n\nasync function main(): Promise<void> {\n\tconst { values, positionals } = parseArgs({\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tendpoint: { type: 'string', short: 'e' },\n\t\t\tintrospection: { type: 'string', short: 'i' },\n\t\t\tsdl: { type: 'string', short: 's' },\n\t\t\toutput: { type: 'string', short: 'o' },\n\t\t\tinclude: { type: 'string' },\n\t\t\texclude: { type: 'string' },\n\t\t\tnames: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { type: 'boolean', default: false },\n\t\t\t'no-aggregates': { type: 'boolean', default: false },\n\t\t\tcheck: { type: 'boolean', default: false },\n\t\t\thelp: { type: 'boolean', short: 'h' },\n\t\t},\n\t})\n\n\tconst command = positionals[0]\n\n\tif (values.help || !command) {\n\t\tprintHelp()\n\t\tprocess.exit(command ? 0 : 1)\n\t}\n\n\tif (command !== 'generate') {\n\t\tconsole.error(`Unknown command: ${command}`)\n\t\tconsole.error('Available commands: generate')\n\t\tprocess.exit(1)\n\t}\n\n\t// Determine source\n\tconst sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length\n\tif (sourceCount !== 1) {\n\t\tconsole.error('Exactly one of --endpoint, --introspection, or --sdl must be provided')\n\t\tprocess.exit(1)\n\t}\n\n\tif (!values.output) {\n\t\tconsole.error('--output <dir> is required')\n\t\tprocess.exit(1)\n\t}\n\n\tconst outputDir = resolve(values.output)\n\n\t// Build conversion options\n\tconst options: GraphQLConversionOptions = {\n\t\tincludeUnmappedMeta: values['include-unmapped'],\n\t}\n\n\tif (values.include) {\n\t\toptions.include = values.include.split(',').map(s => s.trim())\n\t}\n\n\tif (values.exclude) {\n\t\toptions.exclude = values.exclude.split(',').map(s => s.trim())\n\t}\n\n\tif (values.names) {\n\t\tconst namesPath = resolve(values.names)\n\t\toptions.doctypeNames = JSON.parse(readFileSync(namesPath, 'utf-8'))\n\t}\n\n\toptions.onWarning = message => console.warn(` WARN: ${message}`)\n\n\tif (values['custom-scalars']) {\n\t\tconst scalarsPath = resolve(values['custom-scalars'])\n\t\tconst scalarsContent = readFileSync(scalarsPath, 'utf-8')\n\t\toptions.customScalars = JSON.parse(scalarsContent)\n\t}\n\n\t// Resolve source\n\tlet source: IntrospectionQuery | string\n\n\tif (values.endpoint) {\n\t\tconsole.log(`Fetching introspection from ${values.endpoint}...`)\n\t\tsource = await fetchIntrospection(values.endpoint)\n\t} else if (values.introspection) {\n\t\tconst filePath = resolve(values.introspection)\n\t\tconst content = readFileSync(filePath, 'utf-8')\n\t\tconst parsed = JSON.parse(content)\n\t\t// Handle both { data: { __schema: ... } } and { __schema: ... } formats\n\t\tsource = parsed.data ?? parsed\n\t} else {\n\t\tconst filePath = resolve(values.sdl!)\n\t\tsource = readFileSync(filePath, 'utf-8')\n\t}\n\n\t// Convert\n\tconst entities = convertGraphQLSchema(source, options)\n\n\tif (entities.length === 0) {\n\t\tconsole.warn('No entity types found in the schema. Check your include/exclude filters.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Read the identity each authored doctype declares, before planning rather than during the write\n\t// loop below. SDL cannot express which UNIQUE column is a table's key, so for a natural-key\n\t// doctype the declaration in the file is the only answer that exists — and planning is what\n\t// decides whether the aggregate can be built at all. Reading it 20 lines later, as the loop\n\t// does, made the \"declare a primaryKey and re-run\" warning a dead end: re-running after\n\t// declaring one changed nothing.\n\tconst identity: Record<string, string> = {}\n\tfor (const entity of entities) {\n\t\tconst entityPath = join(outputDir, `${entity.slug}.json`)\n\t\tif (!existsSync(entityPath)) continue\n\t\tconst declared = authoredPrimaryKey(JSON.parse(readFileSync(entityPath, 'utf-8')))\n\t\tif (declared !== undefined) identity[entity.name] = declared\n\t}\n\n\t// Each table yields two doctypes — the entity and its aggregate — written as peers, one file\n\t// each, along with what each is verified against. See `planGeneration`.\n\tconst doctypes = planGeneration(entities, {\n\t\tnoAggregates: values['no-aggregates'],\n\t\tidentity,\n\t\tonWarning: message => console.warn(` WARN: ${message}`),\n\t})\n\n\t// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\tlet changed = 0\n\tconst driftLines: string[] = []\n\n\tfor (const { generated, basis, subset } of doctypes) {\n\t\tconst fileName = `${generated.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\n\t\t// When a doctype already exists it is the source of truth: generation confirms it and adds\n\t\t// provenance markers, and reports anything it disagrees with rather than applying it. A\n\t\t// doctype legitimately declares identity the schema cannot express — most often a natural\n\t\t// key that is a UNIQUE constraint, not the table's PRIMARY KEY — and overwriting that would\n\t\t// silently re-key the doctype on every run. A first generation has nothing to merge into,\n\t\t// so converter output is written verbatim.\n\t\tlet output: object = generated\n\t\tif (existsSync(filePath)) {\n\t\t\tconst { doctype: merged, drift } = mergeIntrospectedDoctype(JSON.parse(readFileSync(filePath, 'utf-8')), basis, {\n\t\t\t\tsubset,\n\t\t\t})\n\t\t\toutput = merged\n\t\t\tdriftLines.push(...formatDoctypeDrift(drift))\n\t\t}\n\n\t\t// Serialize the merged object directly. Never round-trip it through the Zod parser first:\n\t\t// that runs in strip mode and would silently drop every key this package does not model,\n\t\t// `handler` on an action being the one consumers actually rely on.\n\t\t//\n\t\t// `kind` is dropped on the way out because it is the discriminated-union tag the parser\n\t\t// synthesizes, not authored data — `injectKind` puts it back on every read. Writing it made\n\t\t// the generator and the docbuilder disagree with hand-authored files about what a doctype\n\t\t// looks like, and re-added the key on every regeneration.\n\t\tconst serializable: Record<string, unknown> = { ...output }\n\t\tif (Array.isArray(serializable.fields)) {\n\t\t\tserializable.fields = serializable.fields.map(stripFieldKind)\n\t\t}\n\t\tconst json = JSON.stringify(serializable, null, '\\t') + '\\n'\n\t\tconst unchanged = existsSync(filePath) && readFileSync(filePath, 'utf-8') === json\n\t\tif (!unchanged) changed++\n\n\t\tif (!values.check && !unchanged) {\n\t\t\twriteFileSync(filePath, json, 'utf-8')\n\t\t}\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(output)\n\t\tif (!validation.success) {\n\t\t\terrors++\n\t\t\tconsole.error(` ERROR: ${fileName} failed validation:`)\n\t\t\tfor (const err of validation.errors) {\n\t\t\t\tconsole.error(` ${err.path.join('.')}: ${err.message}`)\n\t\t\t}\n\t\t} else {\n\t\t\t// Check for unmapped fields\n\t\t\tconst unmappedFields = generated.fields.filter((f: any) => f._unmapped)\n\t\t\tif (unmappedFields.length > 0) {\n\t\t\t\twarnings++\n\t\t\t\tconsole.warn(\n\t\t\t\t\t` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields\n\t\t\t\t\t\t.map((f: any) => f.fieldname)\n\t\t\t\t\t\t.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (driftLines.length > 0) {\n\t\tconsole.log('\\nDrift between the authored doctypes and the schema (reported, not applied):')\n\t\tfor (const line of driftLines) console.log(line)\n\t}\n\n\tconsole.log(\n\t\t`\\n${values.check ? 'Checked' : 'Generated'} ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(changed ? ` (${changed} ${values.check ? 'would change' : 'written'})` : ' (all up to date)') +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 0 || (values.check && changed > 0)) {\n\t\tprocess.exit(1)\n\t}\n}\n\nfunction printHelp(): void {\n\tconsole.log(`\nstonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes\n\nUSAGE:\n stonecrop-schema generate [options]\n\nSOURCE (exactly one required):\n --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint\n --introspection, -i <file> Read from a saved introspection JSON file\n --sdl, -s <file> Read from a GraphQL SDL (.graphql) file\n\nOUTPUT:\n --output, -o <dir> Directory to write doctype JSON files (required)\n\nOPTIONS:\n --include <types> Comma-separated list of type names to include\n --exclude <types> Comma-separated list of type names to exclude\n --names <file> JSON file mapping GraphQL type name to doctype name\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --no-aggregates Emit only the entity doctype, not its aggregate\n --check Report drift and exit non-zero if anything would change; write nothing\n --help, -h Show this help message\n\nEach table generates TWO doctypes: the entity (every column, backs the record form) and its\naggregate (the collection view, identity column only by default), named as simple plurals —\n'task.json' and 'tasks.json'. Widen an aggregate by adding fields to it — curation survives\nregeneration. A doctype whose name is already plural gets no aggregate, because it would claim\nits own name and its own file; the run says so and still writes the entity.\n\nAn aggregate needs an identity column. A table keyed on 'id' gives one up; for a natural key,\ndeclare 'primaryKey' on the field in the entity's own file and re-run — the aggregate is keyed\non whatever that file declares.\n\nNOTE: an existing doctype file is the source of truth. Regeneration verifies it against the\nschema and adds 'source: introspected' markers; it reports disagreements rather than\noverwriting them, so hand-curation survives. Use --check in CI.\n\nEXAMPLES:\n # From a live PostGraphile server\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas\n\n # From a saved introspection result\n stonecrop-schema generate -i introspection.json -o ./schemas\n\n # From an SDL file with custom scalars\n stonecrop-schema generate -s schema.graphql -o ./schemas \\\\\n --custom-scalars custom-scalars.json\n\n # Only convert specific types\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\\\\n --include \"User,Post,Comment\"\n`)\n}\n\nmain().catch(err => {\n\tconsole.error('Error:', err.message)\n\tprocess.exit(1)\n})\n"],"names":["fetchIntrospection","endpoint","headers","response","getIntrospectionQuery","json","e","main","values","positionals","parseArgs","command","printHelp","outputDir","resolve","options","s","namesPath","readFileSync","message","scalarsPath","scalarsContent","source","filePath","content","parsed","entities","convertGraphQLSchema","identity","entity","entityPath","join","existsSync","declared","authoredPrimaryKey","doctypes","planGeneration","mkdirSync","warnings","errors","changed","driftLines","generated","basis","subset","fileName","output","merged","drift","mergeIntrospectedDoctype","formatDoctypeDrift","serializable","stripFieldKind","unchanged","writeFileSync","validation","validateDoctype","unmappedFields","f","err","line"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/* oxlint-disable no-console */\n\n/**\n * Stonecrop Schema CLI\n *\n * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.\n *\n * Usage:\n * stonecrop-schema generate --endpoint <url> --output <dir>\n * stonecrop-schema generate --introspection <file.json> --output <dir>\n * stonecrop-schema generate --sdl <file.graphql> --output <dir>\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { getIntrospectionQuery, type IntrospectionQuery } from 'graphql'\n\nimport { authoredPrimaryKey } from './converter/authored'\nimport { convertGraphQLSchema, formatDoctypeDrift, mergeIntrospectedDoctype, planGeneration } from './converter/index'\nimport { stripFieldKind } from './field'\nimport { validateDoctype } from './validation'\nimport type { GraphQLConversionOptions } from './converter/types'\n\n/**\n * Fetch an introspection result from a live GraphQL endpoint.\n *\n * @param endpoint - The GraphQL endpoint URL\n * @param headers - Optional HTTP headers\n * @returns The introspection query result\n */\nasync function fetchIntrospection(endpoint: string, headers?: Record<string, string>): Promise<IntrospectionQuery> {\n\tconst response = await fetch(endpoint, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...headers,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tquery: getIntrospectionQuery(),\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`)\n\t}\n\n\tconst json: { data?: IntrospectionQuery; errors?: Array<{ message: string }> } = await response.json()\n\n\tif (json.errors?.length) {\n\t\tthrow new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`)\n\t}\n\n\tif (!json.data) {\n\t\tthrow new Error('No data in introspection response')\n\t}\n\n\treturn json.data\n}\n\nasync function main(): Promise<void> {\n\tconst { values, positionals } = parseArgs({\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tendpoint: { type: 'string', short: 'e' },\n\t\t\tintrospection: { type: 'string', short: 'i' },\n\t\t\tsdl: { type: 'string', short: 's' },\n\t\t\toutput: { type: 'string', short: 'o' },\n\t\t\tinclude: { type: 'string' },\n\t\t\texclude: { type: 'string' },\n\t\t\tnames: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { type: 'boolean', default: false },\n\t\t\t'no-aggregates': { type: 'boolean', default: false },\n\t\t\tcheck: { type: 'boolean', default: false },\n\t\t\thelp: { type: 'boolean', short: 'h' },\n\t\t},\n\t})\n\n\tconst command = positionals[0]\n\n\tif (values.help || !command) {\n\t\tprintHelp()\n\t\tprocess.exit(command ? 0 : 1)\n\t}\n\n\tif (command !== 'generate') {\n\t\tconsole.error(`Unknown command: ${command}`)\n\t\tconsole.error('Available commands: generate')\n\t\tprocess.exit(1)\n\t}\n\n\t// Determine source\n\tconst sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length\n\tif (sourceCount !== 1) {\n\t\tconsole.error('Exactly one of --endpoint, --introspection, or --sdl must be provided')\n\t\tprocess.exit(1)\n\t}\n\n\tif (!values.output) {\n\t\tconsole.error('--output <dir> is required')\n\t\tprocess.exit(1)\n\t}\n\n\tconst outputDir = resolve(values.output)\n\n\t// Build conversion options\n\tconst options: GraphQLConversionOptions = {\n\t\tincludeUnmappedMeta: values['include-unmapped'],\n\t}\n\n\tif (values.include) {\n\t\toptions.include = values.include.split(',').map(s => s.trim())\n\t}\n\n\tif (values.exclude) {\n\t\toptions.exclude = values.exclude.split(',').map(s => s.trim())\n\t}\n\n\tif (values.names) {\n\t\tconst namesPath = resolve(values.names)\n\t\toptions.doctypeNames = JSON.parse(readFileSync(namesPath, 'utf-8'))\n\t}\n\n\toptions.onWarning = message => console.warn(` WARN: ${message}`)\n\n\tif (values['custom-scalars']) {\n\t\tconst scalarsPath = resolve(values['custom-scalars'])\n\t\tconst scalarsContent = readFileSync(scalarsPath, 'utf-8')\n\t\toptions.customScalars = JSON.parse(scalarsContent)\n\t}\n\n\t// Resolve source\n\tlet source: IntrospectionQuery | string\n\n\tif (values.endpoint) {\n\t\tconsole.log(`Fetching introspection from ${values.endpoint}...`)\n\t\tsource = await fetchIntrospection(values.endpoint)\n\t} else if (values.introspection) {\n\t\tconst filePath = resolve(values.introspection)\n\t\tconst content = readFileSync(filePath, 'utf-8')\n\t\tconst parsed = JSON.parse(content)\n\t\t// Handle both { data: { __schema: ... } } and { __schema: ... } formats\n\t\tsource = parsed.data ?? parsed\n\t} else {\n\t\tconst filePath = resolve(values.sdl!)\n\t\tsource = readFileSync(filePath, 'utf-8')\n\t}\n\n\t// Convert\n\tconst entities = convertGraphQLSchema(source, options)\n\n\tif (entities.length === 0) {\n\t\tconsole.warn('No entity types found in the schema. Check your include/exclude filters.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Read the identity each authored doctype declares, before planning rather than during the write\n\t// loop below. SDL cannot express which UNIQUE column is a table's key, so for a natural-key\n\t// doctype the declaration in the file is the only answer that exists — and planning is what\n\t// decides whether the aggregate can be built at all. Reading it 20 lines later, as the loop\n\t// does, made the \"declare a primaryKey and re-run\" warning a dead end: re-running after\n\t// declaring one changed nothing.\n\tconst identity: Record<string, string> = {}\n\tfor (const entity of entities) {\n\t\tconst entityPath = join(outputDir, `${entity.slug}.json`)\n\t\tif (!existsSync(entityPath)) continue\n\t\tconst declared = authoredPrimaryKey(JSON.parse(readFileSync(entityPath, 'utf-8')))\n\t\tif (declared !== undefined) identity[entity.name] = declared\n\t}\n\n\t// Each table yields two doctypes — the entity and its aggregate — written as peers, one file\n\t// each, along with what each is verified against. See `planGeneration`.\n\tconst doctypes = planGeneration(entities, {\n\t\tnoAggregates: values['no-aggregates'],\n\t\tidentity,\n\t\tonWarning: message => console.warn(` WARN: ${message}`),\n\t})\n\n\t// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\tlet changed = 0\n\tconst driftLines: string[] = []\n\n\tfor (const { generated, basis, subset } of doctypes) {\n\t\tconst fileName = `${generated.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\n\t\t// When a doctype already exists it is the source of truth: generation confirms it and adds\n\t\t// provenance markers, and reports anything it disagrees with rather than applying it. A\n\t\t// doctype legitimately declares identity the schema cannot express — most often a natural\n\t\t// key that is a UNIQUE constraint, not the table's PRIMARY KEY — and overwriting that would\n\t\t// silently re-key the doctype on every run. A first generation has nothing to merge into,\n\t\t// so converter output is written verbatim.\n\t\tlet output: object = generated\n\t\tif (existsSync(filePath)) {\n\t\t\tconst { doctype: merged, drift } = mergeIntrospectedDoctype(JSON.parse(readFileSync(filePath, 'utf-8')), basis, {\n\t\t\t\tsubset,\n\t\t\t})\n\t\t\toutput = merged\n\t\t\tdriftLines.push(...formatDoctypeDrift(drift))\n\t\t}\n\n\t\t// Serialize the merged object directly. Never round-trip it through the Zod parser first:\n\t\t// that runs in strip mode and would silently drop every key this package does not model,\n\t\t// `handler` on an action being the one consumers actually rely on.\n\t\t//\n\t\t// `kind` is dropped on the way out because it is the discriminated-union tag the parser\n\t\t// synthesizes, not authored data — `injectKind` puts it back on every read. Writing it made\n\t\t// the generator and the docbuilder disagree with hand-authored files about what a doctype\n\t\t// looks like, and re-added the key on every regeneration.\n\t\tconst serializable: Record<string, unknown> = { ...output }\n\t\tif (Array.isArray(serializable.fields)) {\n\t\t\tserializable.fields = serializable.fields.map(stripFieldKind)\n\t\t}\n\t\tconst json = JSON.stringify(serializable, null, '\\t') + '\\n'\n\t\tconst unchanged = existsSync(filePath) && readFileSync(filePath, 'utf-8') === json\n\t\tif (!unchanged) changed++\n\n\t\tif (!values.check && !unchanged) {\n\t\t\twriteFileSync(filePath, json, 'utf-8')\n\t\t}\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(output)\n\t\tif (!validation.success) {\n\t\t\terrors++\n\t\t\tconsole.error(` ERROR: ${fileName} failed validation:`)\n\t\t\tfor (const err of validation.errors) {\n\t\t\t\tconsole.error(` ${err.path.join('.')}: ${err.message}`)\n\t\t\t}\n\t\t} else {\n\t\t\t// Check for unmapped fields\n\t\t\tconst unmappedFields = generated.fields.filter((f: any) => f._unmapped)\n\t\t\tif (unmappedFields.length > 0) {\n\t\t\t\twarnings++\n\t\t\t\tconsole.warn(\n\t\t\t\t\t` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields\n\t\t\t\t\t\t.map((f: any) => f.fieldname)\n\t\t\t\t\t\t.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (driftLines.length > 0) {\n\t\tconsole.log('\\nDrift between the authored doctypes and the schema (reported, not applied):')\n\t\tfor (const line of driftLines) console.log(line)\n\t}\n\n\tconsole.log(\n\t\t`\\n${values.check ? 'Checked' : 'Generated'} ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(changed ? ` (${changed} ${values.check ? 'would change' : 'written'})` : ' (all up to date)') +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 0 || (values.check && changed > 0)) {\n\t\tprocess.exit(1)\n\t}\n}\n\nfunction printHelp(): void {\n\tconsole.log(`\nstonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes\n\nUSAGE:\n stonecrop-schema generate [options]\n\nSOURCE (exactly one required):\n --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint\n --introspection, -i <file> Read from a saved introspection JSON file\n --sdl, -s <file> Read from a GraphQL SDL (.graphql) file\n\nOUTPUT:\n --output, -o <dir> Directory to write doctype JSON files (required)\n\nOPTIONS:\n --include <types> Comma-separated list of type names to include\n --exclude <types> Comma-separated list of type names to exclude\n --names <file> JSON file mapping GraphQL type name to doctype name\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --no-aggregates Emit only the entity doctype, not its aggregate\n --check Report drift and exit non-zero if anything would change; write nothing\n --help, -h Show this help message\n\nEach table generates TWO doctypes: the entity (every column, backs the record form) and its\naggregate (the collection view, identity column only by default), named as simple plurals —\n'task.json' and 'tasks.json'. Widen an aggregate by adding fields to it — curation survives\nregeneration. A doctype whose name is already plural gets no aggregate, because it would claim\nits own name and its own file; the run says so and still writes the entity.\n\nAn aggregate needs an identity column. A table keyed on 'id' gives one up; for a natural key,\ndeclare 'primaryKey' on the field in the entity's own file and re-run — the aggregate is keyed\non whatever that file declares.\n\nNOTE: an existing doctype file is the source of truth. Regeneration verifies it against the\nschema and adds 'source: introspected' markers; it reports disagreements rather than\noverwriting them, so hand-curation survives. Use --check in CI.\n\nEXAMPLES:\n # From a live PostGraphile server\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas\n\n # From a saved introspection result\n stonecrop-schema generate -i introspection.json -o ./schemas\n\n # From an SDL file with custom scalars\n stonecrop-schema generate -s schema.graphql -o ./schemas \\\\\n --custom-scalars custom-scalars.json\n\n # Only convert specific types\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\\\\n --include \"User,Post,Comment\"\n`)\n}\n\nmain().catch(err => {\n\tconsole.error('Error:', err.message)\n\tprocess.exit(1)\n})\n"],"mappings":"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"}
|