@stonecrop/schema 0.16.4 → 0.16.5
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 +23 -9
- package/dist/cli.js +74 -53
- package/dist/cli.js.map +1 -1
- package/dist/index.js +34 -29
- package/dist/schema.d.ts +142 -8
- package/dist/src/cli.js +49 -15
- package/dist/src/converter/index.d.ts +2 -0
- package/dist/src/converter/index.d.ts.map +1 -1
- package/dist/src/converter/index.js +30 -16
- package/dist/src/converter/merge.d.ts +87 -0
- package/dist/src/converter/merge.d.ts.map +1 -0
- package/dist/src/converter/merge.js +145 -0
- package/dist/src/converter/types.d.ts +17 -8
- package/dist/src/converter/types.d.ts.map +1 -1
- package/dist/src/field.d.ts +47 -0
- package/dist/src/field.d.ts.map +1 -1
- package/dist/src/field.js +67 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/validation-CToUOy0a.js +538 -0
- package/dist/validation-CToUOy0a.js.map +1 -0
- package/package.json +1 -1
- package/dist/validation-Dyxjvaio.js +0 -458
- package/dist/validation-Dyxjvaio.js.map +0 -1
package/README.md
CHANGED
|
@@ -333,11 +333,26 @@ then `--exclude` removes any remaining unwanted names.
|
|
|
333
333
|
| `--output <dir>` | `-o` | Directory to write doctype JSON files (required) |
|
|
334
334
|
| `--include <types>` | | Comma-separated allowlist of type names to generate |
|
|
335
335
|
| `--exclude <types>` | | Comma-separated list of type names to skip |
|
|
336
|
-
| `--
|
|
336
|
+
| `--names <file>` | | JSON file mapping GraphQL type name to doctype name |
|
|
337
337
|
| `--custom-scalars <file>` | | JSON file mapping custom scalar names to field templates |
|
|
338
338
|
| `--include-unmapped` | | Retain `_graphqlType` metadata on fields with no mapping |
|
|
339
|
+
| `--check` | | Report drift and exit non-zero if anything would change |
|
|
339
340
|
| `--help` | `-h` | Show help |
|
|
340
341
|
|
|
342
|
+
### Regeneration is non-destructive
|
|
343
|
+
|
|
344
|
+
An existing doctype file is the source of truth. Regeneration verifies each field against the
|
|
345
|
+
schema, stamps `"source": "introspected"` on the ones it confirms, and **reports** anything it
|
|
346
|
+
disagrees with rather than overwriting it — so labels, component choices and a hand-declared
|
|
347
|
+
primary key all survive.
|
|
348
|
+
|
|
349
|
+
That polarity is deliberate. A doctype legitimately declares identity the schema cannot express:
|
|
350
|
+
a natural business key is usually a `UNIQUE` constraint rather than the table's `PRIMARY KEY`, and
|
|
351
|
+
where a table carries several no rule can pick between them. Overwriting identity from the schema
|
|
352
|
+
would silently re-key such a doctype on every run.
|
|
353
|
+
|
|
354
|
+
Use `--check` in CI to fail the build when a doctype and the schema have diverged.
|
|
355
|
+
|
|
341
356
|
### Custom scalars
|
|
342
357
|
|
|
343
358
|
For servers that use non-standard scalars (e.g. PostGraphile's `BigFloat`, `Datetime`), provide
|
|
@@ -355,21 +370,20 @@ stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes \
|
|
|
355
370
|
--custom-scalars custom-scalars.json
|
|
356
371
|
```
|
|
357
372
|
|
|
358
|
-
###
|
|
373
|
+
### Doctype name remap
|
|
359
374
|
|
|
360
|
-
|
|
375
|
+
Emit a doctype under a name other than its GraphQL type — for a second view over an existing type,
|
|
376
|
+
say, distinguished only by presentation. The `slug` follows the doctype name, so it addresses both
|
|
377
|
+
the output file and the route. Keep this consistent with the middleware's `tables` option, which
|
|
378
|
+
maps the resulting doctype name to its SQL target.
|
|
361
379
|
|
|
362
380
|
```json
|
|
363
|
-
{
|
|
364
|
-
"SalesOrder": {
|
|
365
|
-
"totalAmount": { "component": "ANumericInput" }
|
|
366
|
-
}
|
|
367
|
-
}
|
|
381
|
+
{ "Plan": "Planner" }
|
|
368
382
|
```
|
|
369
383
|
|
|
370
384
|
```bash
|
|
371
385
|
stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes \
|
|
372
|
-
--
|
|
386
|
+
--names names.json
|
|
373
387
|
```
|
|
374
388
|
|
|
375
389
|
## Naming Utilities
|
package/dist/cli.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync as
|
|
3
|
-
import { resolve as
|
|
4
|
-
import { parseArgs as
|
|
5
|
-
import { getIntrospectionQuery as
|
|
6
|
-
import { c as
|
|
7
|
-
async function
|
|
8
|
-
const
|
|
2
|
+
import { readFileSync as c, existsSync as v, mkdirSync as O, writeFileSync as k } from "node:fs";
|
|
3
|
+
import { resolve as d, join as C } from "node:path";
|
|
4
|
+
import { parseArgs as P } from "node:util";
|
|
5
|
+
import { getIntrospectionQuery as j } from "graphql";
|
|
6
|
+
import { c as E, m as J, f as L, v as R } from "./validation-CToUOy0a.js";
|
|
7
|
+
async function q(e, f) {
|
|
8
|
+
const o = await fetch(e, {
|
|
9
9
|
method: "POST",
|
|
10
10
|
headers: {
|
|
11
11
|
"Content-Type": "application/json",
|
|
12
|
-
...
|
|
12
|
+
...f
|
|
13
13
|
},
|
|
14
14
|
body: JSON.stringify({
|
|
15
|
-
query:
|
|
15
|
+
query: j()
|
|
16
16
|
})
|
|
17
17
|
});
|
|
18
|
-
if (!
|
|
19
|
-
throw new Error(`Failed to fetch introspection: ${
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
throw new Error(`GraphQL errors: ${
|
|
23
|
-
if (!
|
|
18
|
+
if (!o.ok)
|
|
19
|
+
throw new Error(`Failed to fetch introspection: ${o.status} ${o.statusText}`);
|
|
20
|
+
const p = await o.json();
|
|
21
|
+
if (p.errors?.length)
|
|
22
|
+
throw new Error(`GraphQL errors: ${p.errors.map((i) => i.message).join(", ")}`);
|
|
23
|
+
if (!p.data)
|
|
24
24
|
throw new Error("No data in introspection response");
|
|
25
|
-
return
|
|
25
|
+
return p.data;
|
|
26
26
|
}
|
|
27
|
-
async function
|
|
28
|
-
const { values: e, positionals:
|
|
27
|
+
async function D() {
|
|
28
|
+
const { values: e, positionals: f } = P({
|
|
29
29
|
allowPositionals: !0,
|
|
30
30
|
options: {
|
|
31
31
|
endpoint: { type: "string", short: "e" },
|
|
@@ -34,59 +34,75 @@ async function E() {
|
|
|
34
34
|
output: { type: "string", short: "o" },
|
|
35
35
|
include: { type: "string" },
|
|
36
36
|
exclude: { type: "string" },
|
|
37
|
-
|
|
37
|
+
names: { type: "string" },
|
|
38
38
|
"custom-scalars": { type: "string" },
|
|
39
39
|
"include-unmapped": { type: "boolean", default: !1 },
|
|
40
|
+
check: { type: "boolean", default: !1 },
|
|
40
41
|
help: { type: "boolean", short: "h" }
|
|
41
42
|
}
|
|
42
|
-
}),
|
|
43
|
-
(e.help || !
|
|
44
|
-
const
|
|
43
|
+
}), o = f[0];
|
|
44
|
+
(e.help || !o) && (F(), process.exit(o ? 0 : 1)), o !== "generate" && (console.error(`Unknown command: ${o}`), console.error("Available commands: generate"), process.exit(1)), [e.endpoint, e.introspection, e.sdl].filter(Boolean).length !== 1 && (console.error("Exactly one of --endpoint, --introspection, or --sdl must be provided"), process.exit(1)), e.output || (console.error("--output <dir> is required"), process.exit(1));
|
|
45
|
+
const i = d(e.output), a = {
|
|
45
46
|
includeUnmappedMeta: e["include-unmapped"]
|
|
46
47
|
};
|
|
47
|
-
if (e.include && (a.include = e.include.split(",").map((
|
|
48
|
-
const
|
|
49
|
-
a.
|
|
48
|
+
if (e.include && (a.include = e.include.split(",").map((t) => t.trim())), e.exclude && (a.exclude = e.exclude.split(",").map((t) => t.trim())), e.names) {
|
|
49
|
+
const t = d(e.names);
|
|
50
|
+
a.doctypeNames = JSON.parse(c(t, "utf-8"));
|
|
50
51
|
}
|
|
51
|
-
if (e["custom-scalars"]) {
|
|
52
|
-
const
|
|
53
|
-
a.customScalars = JSON.parse(
|
|
52
|
+
if (a.onWarning = (t) => console.warn(` WARN: ${t}`), e["custom-scalars"]) {
|
|
53
|
+
const t = d(e["custom-scalars"]), r = c(t, "utf-8");
|
|
54
|
+
a.customScalars = JSON.parse(r);
|
|
54
55
|
}
|
|
55
|
-
let
|
|
56
|
+
let h;
|
|
56
57
|
if (e.endpoint)
|
|
57
|
-
console.log(`Fetching introspection from ${e.endpoint}...`),
|
|
58
|
+
console.log(`Fetching introspection from ${e.endpoint}...`), h = await q(e.endpoint);
|
|
58
59
|
else if (e.introspection) {
|
|
59
|
-
const
|
|
60
|
-
|
|
60
|
+
const t = d(e.introspection), r = c(t, "utf-8"), s = JSON.parse(r);
|
|
61
|
+
h = s.data ?? s;
|
|
61
62
|
} else {
|
|
62
|
-
const
|
|
63
|
-
|
|
63
|
+
const t = d(e.sdl);
|
|
64
|
+
h = c(t, "utf-8");
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
const g = E(h, a);
|
|
67
|
+
g.length === 0 && (console.warn("No entity types found in the schema. Check your include/exclude filters."), process.exit(0)), v(i) || O(i, { recursive: !0 });
|
|
68
|
+
let y = 0, m = 0, u = 0;
|
|
69
|
+
const w = [];
|
|
70
|
+
for (const t of g) {
|
|
71
|
+
const r = `${t.slug}.json`, s = C(i, r);
|
|
72
|
+
let S = t;
|
|
73
|
+
if (v(s)) {
|
|
74
|
+
const { doctype: n, drift: l } = J(
|
|
75
|
+
JSON.parse(c(s, "utf-8")),
|
|
76
|
+
t
|
|
77
|
+
);
|
|
78
|
+
S = n, w.push(...L(l));
|
|
79
|
+
}
|
|
80
|
+
const x = JSON.stringify(S, null, " ") + `
|
|
81
|
+
`, $ = v(s) && c(s, "utf-8") === x;
|
|
82
|
+
$ || u++, !e.check && !$ && k(s, x, "utf-8");
|
|
83
|
+
const N = R(S);
|
|
84
|
+
if (N.success) {
|
|
85
|
+
const n = t.fields.filter((l) => l._unmapped);
|
|
86
|
+
n.length > 0 && (y++, console.warn(
|
|
87
|
+
` WARN: ${r} has ${n.length} unmapped field(s): ${n.map((l) => l.fieldname).join(", ")}`
|
|
77
88
|
));
|
|
78
89
|
} else {
|
|
79
|
-
|
|
80
|
-
for (const n of
|
|
90
|
+
m++, console.error(` ERROR: ${r} failed validation:`);
|
|
91
|
+
for (const n of N.errors)
|
|
81
92
|
console.error(` ${n.path.join(".")}: ${n.message}`);
|
|
82
93
|
}
|
|
83
94
|
}
|
|
95
|
+
if (w.length > 0) {
|
|
96
|
+
console.log(`
|
|
97
|
+
Drift between the authored doctypes and the schema (reported, not applied):`);
|
|
98
|
+
for (const t of w) console.log(t);
|
|
99
|
+
}
|
|
84
100
|
console.log(
|
|
85
101
|
`
|
|
86
|
-
Generated ${
|
|
87
|
-
),
|
|
102
|
+
${e.check ? "Checked" : "Generated"} ${g.length} doctype(s) in ${i}` + (u ? ` (${u} ${e.check ? "would change" : "written"})` : " (all up to date)") + (y ? ` (${y} with warnings)` : "") + (m ? ` (${m} with errors)` : "")
|
|
103
|
+
), (m > 0 || e.check && u > 0) && process.exit(1);
|
|
88
104
|
}
|
|
89
|
-
function
|
|
105
|
+
function F() {
|
|
90
106
|
console.log(`
|
|
91
107
|
stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
|
|
92
108
|
|
|
@@ -104,11 +120,16 @@ OUTPUT:
|
|
|
104
120
|
OPTIONS:
|
|
105
121
|
--include <types> Comma-separated list of type names to include
|
|
106
122
|
--exclude <types> Comma-separated list of type names to exclude
|
|
107
|
-
--
|
|
123
|
+
--names <file> JSON file mapping GraphQL type name to doctype name
|
|
108
124
|
--custom-scalars <file> JSON file mapping custom scalar names to field templates
|
|
109
125
|
--include-unmapped Include _graphqlType metadata on unmapped fields
|
|
126
|
+
--check Report drift and exit non-zero if anything would change; write nothing
|
|
110
127
|
--help, -h Show this help message
|
|
111
128
|
|
|
129
|
+
NOTE: an existing doctype file is the source of truth. Regeneration verifies it against the
|
|
130
|
+
schema and adds 'source: introspected' markers; it reports disagreements rather than
|
|
131
|
+
overwriting them, so hand-curation survives. Use --check in CI.
|
|
132
|
+
|
|
112
133
|
EXAMPLES:
|
|
113
134
|
# From a live PostGraphile server
|
|
114
135
|
stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas
|
|
@@ -125,7 +146,7 @@ EXAMPLES:
|
|
|
125
146
|
--include "User,Post,Comment"
|
|
126
147
|
`);
|
|
127
148
|
}
|
|
128
|
-
|
|
149
|
+
D().catch((e) => {
|
|
129
150
|
console.error("Error:", e.message), process.exit(1);
|
|
130
151
|
});
|
|
131
152
|
//# 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 { convertGraphQLSchema } from './converter/index'\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\toverrides: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { 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.overrides) {\n\t\tconst overridesPath = resolve(values.overrides)\n\t\tconst overridesContent = readFileSync(overridesPath, 'utf-8')\n\t\toptions.typeOverrides = JSON.parse(overridesContent)\n\t}\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 doctypes = convertGraphQLSchema(source, options)\n\n\tif (doctypes.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// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\n\tfor (const doctype of doctypes) {\n\t\tconst fileName = `${doctype.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\t\tconst json = JSON.stringify(doctype, null, '\\t')\n\n\t\twriteFileSync(filePath, json + '\\n', 'utf-8')\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(doctype)\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 = doctype.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\tconsole.log(\n\t\t`\\nGenerated ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 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 --overrides <file> JSON file with per-type field overrides\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --help, -h Show this help message\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","overridesPath","overridesContent","readFileSync","scalarsPath","scalarsContent","source","filePath","content","parsed","doctypes","convertGraphQLSchema","existsSync","mkdirSync","warnings","errors","doctype","fileName","join","writeFileSync","validation","validateDoctype","unmappedFields","f","err"],"mappings":"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"}
|
|
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 { convertGraphQLSchema, formatDoctypeDrift, mergeIntrospectedDoctype } from './converter/index'\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\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 doctypes = convertGraphQLSchema(source, options)\n\n\tif (doctypes.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// 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 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(\n\t\t\t\tJSON.parse(readFileSync(filePath, 'utf-8')),\n\t\t\t\tgenerated\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\tconst json = JSON.stringify(output, 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 --check Report drift and exit non-zero if anything would change; write nothing\n --help, -h Show this help message\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","doctypes","convertGraphQLSchema","existsSync","mkdirSync","warnings","errors","changed","driftLines","generated","fileName","join","output","merged","drift","mergeIntrospectedDoctype","formatDoctypeDrift","unchanged","writeFileSync","validation","validateDoctype","unmappedFields","f","err","line"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as d, D as A, F as
|
|
1
|
+
import { A as d, D as A, F as p, G as u, I as y, a as T, T as m, b as S, d as D, V as I, W as L, e as N, g as C, h as f, i as F, j as O, k as b, c as k, l as E, n as x, f as P, o as _, p as g, q as h, m as R, r as M, s as v, t as w, u as W, w as G, x as K, y as j, z as Q, v as V, B as q } from "./validation-CToUOy0a.js";
|
|
2
2
|
const a = {
|
|
3
3
|
ATextInput: "text",
|
|
4
4
|
ATextboxInput: "text",
|
|
@@ -31,45 +31,50 @@ function i(e) {
|
|
|
31
31
|
function s(e, n) {
|
|
32
32
|
return i(e.component ?? n) === "inline" ? "inline" : e.cardinality === "noneOrMany" || e.cardinality === "atLeastOne" ? "table" : "record";
|
|
33
33
|
}
|
|
34
|
-
const
|
|
34
|
+
const r = [
|
|
35
35
|
.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(t)])
|
|
36
36
|
].toSorted();
|
|
37
37
|
export {
|
|
38
38
|
d as ActionDefinition,
|
|
39
|
-
|
|
39
|
+
r as CANONICAL_COMPONENTS,
|
|
40
40
|
a as COMPONENT_CATEGORY,
|
|
41
41
|
t as COMPONENT_LINK_EXPANSION,
|
|
42
42
|
A as DoctypeFieldSchema,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
T as
|
|
47
|
-
m as
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
p as FieldsetFieldSchema,
|
|
44
|
+
u as GQL_SCALAR_MAP,
|
|
45
|
+
y as INTERNAL_SCALARS,
|
|
46
|
+
T as INTROSPECTED_IDENTITY_PROPS,
|
|
47
|
+
m as TableFieldSchema,
|
|
48
|
+
S as TableViewConfig,
|
|
49
|
+
D as TriggerDefinition,
|
|
50
|
+
I as ValueFieldSchema,
|
|
51
|
+
L as WELL_KNOWN_SCALARS,
|
|
51
52
|
N as WorkflowLayout,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
C as WorkflowMeta,
|
|
54
|
+
f as buildScalarMap,
|
|
55
|
+
F as camelToLabel,
|
|
56
|
+
O as camelToSnake,
|
|
57
|
+
b as classifyFieldType,
|
|
57
58
|
o as componentCategory,
|
|
58
59
|
i as componentLinkExpansion,
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
k as convertGraphQLSchema,
|
|
61
|
+
E as defaultIsEntityField,
|
|
61
62
|
x as defaultIsEntityType,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
P as formatDoctypeDrift,
|
|
64
|
+
_ as getPrimaryKeyField,
|
|
65
|
+
g as getRecordIdentity,
|
|
66
|
+
h as isActionAllowedInState,
|
|
67
|
+
R as mergeIntrospectedDoctype,
|
|
68
|
+
M as normalizeFieldKind,
|
|
69
|
+
v as parseDoctype,
|
|
70
|
+
w as parseField,
|
|
71
|
+
W as pascalToSnake,
|
|
67
72
|
s as resolveLinkRenderMode,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
G as snakeToCamel,
|
|
74
|
+
K as snakeToLabel,
|
|
75
|
+
j as toPascalCase,
|
|
76
|
+
Q as toSlug,
|
|
77
|
+
V as validateDoctype,
|
|
78
|
+
q as validateField
|
|
74
79
|
};
|
|
75
80
|
//# sourceMappingURL=index.js.map
|
package/dist/schema.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export declare const ActionDefinition: z.ZodObject<{
|
|
|
23
23
|
*/
|
|
24
24
|
export declare type ActionDefinition = z.infer<typeof ActionDefinition>;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* A doctype as it exists on disk: a plain object that may carry keys this package does not model
|
|
28
|
+
* (`handler` on an action, `filterFunction` on a field, whatever an app has added). Typing it
|
|
29
|
+
* loosely is what lets the merge round-trip those keys untouched instead of dropping them.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export declare type AuthoredDoctype = Record<string, unknown>;
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Build a merged scalar map from the built-in maps and user-provided custom scalars.
|
|
28
37
|
* Precedence (highest to lowest): customScalars → GQL_SCALAR_MAP → WELL_KNOWN_SCALARS
|
|
@@ -464,6 +473,36 @@ export declare interface DoctypeContext {
|
|
|
464
473
|
[key: string]: unknown;
|
|
465
474
|
}
|
|
466
475
|
|
|
476
|
+
/**
|
|
477
|
+
* What generation found that the authored doctype does not agree with. Every bucket is advisory —
|
|
478
|
+
* nothing here is applied automatically.
|
|
479
|
+
*
|
|
480
|
+
* @public
|
|
481
|
+
*/
|
|
482
|
+
export declare interface DoctypeDrift {
|
|
483
|
+
/** The authored doctype's name. */
|
|
484
|
+
doctype: string;
|
|
485
|
+
/**
|
|
486
|
+
* `clean` — the authored primary key is the one generation would derive.
|
|
487
|
+
* `partial` — the doctype declares an identity generation cannot derive, so identity was left alone.
|
|
488
|
+
*/
|
|
489
|
+
mode: 'clean' | 'partial';
|
|
490
|
+
/** Why the mode is `partial`, when it is. */
|
|
491
|
+
reason?: string;
|
|
492
|
+
/** Fieldnames confirmed against the schema and stamped. */
|
|
493
|
+
tagged: string[];
|
|
494
|
+
/** Authored fields with no matching schema field — app components, fieldsets, or stale entries. */
|
|
495
|
+
orphan: string[];
|
|
496
|
+
/** Schema fields absent from the doctype. Usually deliberate curation, occasionally an oversight. */
|
|
497
|
+
omitted: string[];
|
|
498
|
+
/** `fieldname: authored=… schema=…` where the chosen component differs from the scalar mapping. */
|
|
499
|
+
componentDrift: string[];
|
|
500
|
+
/** `fieldname: authored=… schema=…` where nullability disagrees. */
|
|
501
|
+
requiredDrift: string[];
|
|
502
|
+
/** Identity properties that differ. These are the ones a human must adjudicate. */
|
|
503
|
+
identityDrift: string[];
|
|
504
|
+
}
|
|
505
|
+
|
|
467
506
|
/**
|
|
468
507
|
* Union of all authoring-time field variants.
|
|
469
508
|
* Use `kind` to discriminate: `'field'` | `'fieldset'` | `'table'`.
|
|
@@ -676,6 +715,52 @@ export declare const FieldValidation: z.ZodObject<{
|
|
|
676
715
|
*/
|
|
677
716
|
export declare type FieldValidation = z.infer<typeof FieldValidation>;
|
|
678
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Render a drift report as human-readable lines. Empty when generation agrees with the doctype.
|
|
720
|
+
*
|
|
721
|
+
* @param drift - a report from {@link mergeIntrospectedDoctype}
|
|
722
|
+
* @returns one line per finding, ready to print
|
|
723
|
+
*
|
|
724
|
+
* @public
|
|
725
|
+
*/
|
|
726
|
+
export declare function formatDoctypeDrift(drift: DoctypeDrift): string[];
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Find the field a doctype marks as its primary key, or `undefined` when none is marked.
|
|
730
|
+
*
|
|
731
|
+
* This is the single definition of "which field identifies a record". Both sides depend on it:
|
|
732
|
+
* the middleware builds the SQL identity predicate from it, and the client resolves a record's
|
|
733
|
+
* route/store key from it. Call this; never re-derive the rule at the call site, or the two will
|
|
734
|
+
* drift and the client will key records by a column the server never queried.
|
|
735
|
+
*
|
|
736
|
+
* Two deliberate limits, both matching the shape `primaryKey` actually has:
|
|
737
|
+
* - Only **top-level** fields are scanned. `primaryKey` is a `ValueField` flag and a fieldset's
|
|
738
|
+
* children are not identity columns, so a nested match would be an authoring error, not a PK.
|
|
739
|
+
* - The **first** match wins. Nothing in the schema enforces exactly one `primaryKey: true`, and
|
|
740
|
+
* there is no composite-key representation — a doctype with several is already malformed, and
|
|
741
|
+
* picking the first is what the middleware has always done.
|
|
742
|
+
*
|
|
743
|
+
* @param fields - the doctype's top-level fields
|
|
744
|
+
* @returns the primary-key field, or `undefined` for a PK-less doctype
|
|
745
|
+
* @public
|
|
746
|
+
*/
|
|
747
|
+
export declare function getPrimaryKeyField(fields: readonly DoctypeField[]): ValueField | undefined;
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Resolve a record's identity value using the doctype's declared primary key.
|
|
751
|
+
*
|
|
752
|
+
* Falls back to `record.id` when the doctype declares no `primaryKey`. That fallback is
|
|
753
|
+
* load-bearing, not defensive: surrogate-key doctypes carry an `id` column and never mark a
|
|
754
|
+
* primary key, and PostGraphile renames a single-column `id` PK to `rowId` — so the declared
|
|
755
|
+
* field and `id` are both real sources, in that order.
|
|
756
|
+
*
|
|
757
|
+
* @param fields - the doctype's top-level fields
|
|
758
|
+
* @param record - the record to read the identity from
|
|
759
|
+
* @returns the identity as a string, or `undefined` when neither source yields a usable value
|
|
760
|
+
* @public
|
|
761
|
+
*/
|
|
762
|
+
export declare function getRecordIdentity(fields: readonly DoctypeField[], record: Record<string, unknown>): string | undefined;
|
|
763
|
+
|
|
679
764
|
/**
|
|
680
765
|
* Options for fetching a single record
|
|
681
766
|
* @public
|
|
@@ -760,19 +845,28 @@ export declare interface GraphQLConversionOptions {
|
|
|
760
845
|
*/
|
|
761
846
|
include?: string[];
|
|
762
847
|
/**
|
|
763
|
-
*
|
|
764
|
-
*
|
|
848
|
+
* Emit a doctype under a different name than its GraphQL type. Key is the GraphQL type name,
|
|
849
|
+
* value is the doctype `name`; `slug` is derived from the value.
|
|
850
|
+
*
|
|
851
|
+
* This exists for the case where a doctype is not one-to-one with a table — a second view over
|
|
852
|
+
* an existing type, say, distinguished only by presentation. Without it the converter can only
|
|
853
|
+
* ever name a doctype after its type.
|
|
854
|
+
*
|
|
855
|
+
* Keep it consistent with the middleware's `tables` option, which maps the resulting doctype
|
|
856
|
+
* name to its SQL target.
|
|
765
857
|
*
|
|
766
858
|
* @example
|
|
767
859
|
* ```typescript
|
|
768
|
-
* {
|
|
769
|
-
* SalesOrder: {
|
|
770
|
-
* totalAmount: { component: 'ANumericInput', align: 'right' }
|
|
771
|
-
* }
|
|
772
|
-
* }
|
|
860
|
+
* { Plan: 'Planner' } // emits a doctype named Planner, slug 'planner', from type Plan
|
|
773
861
|
* ```
|
|
774
862
|
*/
|
|
775
|
-
|
|
863
|
+
doctypeNames?: Record<string, string>;
|
|
864
|
+
/**
|
|
865
|
+
* Called with any advisory message raised during conversion — currently only the
|
|
866
|
+
* un-normalized-PostGraphile warning. Left to the caller so the library never writes to the
|
|
867
|
+
* console itself.
|
|
868
|
+
*/
|
|
869
|
+
onWarning?: (message: string) => void;
|
|
776
870
|
/**
|
|
777
871
|
* Map custom or non-standard GraphQL scalar types to the component that renders them.
|
|
778
872
|
* Merged with the built-in scalar maps (GQL_SCALAR_MAP + WELL_KNOWN_SCALARS).
|
|
@@ -853,6 +947,20 @@ export declare type InteractionMode = 'edit' | 'read' | 'display';
|
|
|
853
947
|
*/
|
|
854
948
|
export declare const INTERNAL_SCALARS: Set<string>;
|
|
855
949
|
|
|
950
|
+
/**
|
|
951
|
+
* The field properties a `source: 'introspected'` marker freezes — the ones the database owns.
|
|
952
|
+
*
|
|
953
|
+
* This is the single definition of the identity set. The docbuilder greys these inputs on an
|
|
954
|
+
* introspected field, and the converter's merge refuses to rewrite them. Stating it twice is how
|
|
955
|
+
* the two drift, so both read this constant.
|
|
956
|
+
*
|
|
957
|
+
* Everything absent from this list is author-owned, `component` most importantly: it chooses the
|
|
958
|
+
* widget, which is an authoring decision the database has no opinion about.
|
|
959
|
+
*
|
|
960
|
+
* @public
|
|
961
|
+
*/
|
|
962
|
+
export declare const INTROSPECTED_IDENTITY_PROPS: readonly ["fieldname", "primaryKey", "required", "options", "cardinality", "doctype"];
|
|
963
|
+
|
|
856
964
|
/**
|
|
857
965
|
* Input source for the GraphQL schema converter.
|
|
858
966
|
* Accepts either a standard GraphQL introspection result or an SDL string.
|
|
@@ -956,6 +1064,32 @@ export declare type LinkExpansion = 'inline' | 'expand';
|
|
|
956
1064
|
*/
|
|
957
1065
|
export declare type LinkRenderMode = 'inline' | 'record' | 'table';
|
|
958
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* Verify an authored doctype against freshly generated output and stamp provenance.
|
|
1069
|
+
*
|
|
1070
|
+
* @param authored - the doctype as it exists on disk; every key not named below is preserved verbatim
|
|
1071
|
+
* @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type
|
|
1072
|
+
* @returns the doctype to write, plus a drift report
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* ```ts
|
|
1076
|
+
* const [generated] = convertGraphQLSchema(introspection, { include: ['Uom'] })
|
|
1077
|
+
* const { doctype, drift } = mergeIntrospectedDoctype(JSON.parse(onDisk), generated)
|
|
1078
|
+
* if (drift.identityDrift.length) console.warn(drift.identityDrift.join('\n'))
|
|
1079
|
+
* ```
|
|
1080
|
+
*
|
|
1081
|
+
* @public
|
|
1082
|
+
*/
|
|
1083
|
+
export declare function mergeIntrospectedDoctype(authored: AuthoredDoctype, generated: ConvertedGraphQLDoctype): MergeResult;
|
|
1084
|
+
|
|
1085
|
+
/** Outcome of a merge: the doctype to write, plus what generation disagreed with. @public */
|
|
1086
|
+
export declare interface MergeResult {
|
|
1087
|
+
/** The authored doctype with `source` markers added and nothing else changed. */
|
|
1088
|
+
doctype: AuthoredDoctype;
|
|
1089
|
+
/** Advisory report. Never applied. */
|
|
1090
|
+
drift: DoctypeDrift;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
959
1093
|
/**
|
|
960
1094
|
* Recursively injects the `kind` discriminant into a raw field object and, for fieldsets,
|
|
961
1095
|
* into each of its nested `schema` children — mirroring exactly what Zod's `preprocess`
|