@stonecrop/schema 0.25.0 → 0.27.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 +23 -0
- package/dist/cli.js +85 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.js +68 -61
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +256 -13
- package/dist/src/cli.js +53 -6
- package/dist/src/component-meta.d.ts.map +1 -1
- package/dist/src/component-meta.js +1 -0
- package/dist/src/converter/aggregate.d.ts +127 -0
- package/dist/src/converter/aggregate.d.ts.map +1 -0
- package/dist/src/converter/aggregate.js +235 -0
- package/dist/src/converter/authored.d.ts +43 -0
- package/dist/src/converter/authored.d.ts.map +1 -0
- package/dist/src/converter/authored.js +52 -0
- package/dist/src/converter/heuristics.d.ts +2 -2
- package/dist/src/converter/heuristics.d.ts.map +1 -1
- package/dist/src/converter/heuristics.js +43 -4
- package/dist/src/converter/index.d.ts +3 -1
- package/dist/src/converter/index.d.ts.map +1 -1
- package/dist/src/converter/index.js +2 -0
- package/dist/src/converter/merge.d.ts +25 -10
- package/dist/src/converter/merge.d.ts.map +1 -1
- package/dist/src/converter/merge.js +17 -26
- package/dist/src/doctype.d.ts +38 -0
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +55 -1
- package/dist/src/field.d.ts +59 -8
- package/dist/src/field.d.ts.map +1 -1
- package/dist/src/field.js +84 -14
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/validation-BjRDR6sh.js +1000 -0
- package/dist/validation-BjRDR6sh.js.map +1 -0
- package/package.json +3 -1
- package/dist/validation-CQtfIFHQ.js +0 -583
- package/dist/validation-CQtfIFHQ.js.map +0 -1
package/README.md
CHANGED
|
@@ -305,6 +305,29 @@ stonecrop-schema generate -i introspection.json -o ./app/doctypes
|
|
|
305
305
|
stonecrop-schema generate -s schema.graphql -o ./app/doctypes
|
|
306
306
|
```
|
|
307
307
|
|
|
308
|
+
### What it writes
|
|
309
|
+
|
|
310
|
+
Each table yields two peer doctypes — the entity, carrying every column and backing the record
|
|
311
|
+
form, and its aggregate, the collection view — plus the `route` each one registers at:
|
|
312
|
+
|
|
313
|
+
```jsonc
|
|
314
|
+
// order.json
|
|
315
|
+
{ "name": "Order", "slug": "order", "route": "/order/:id" }
|
|
316
|
+
// orders.json
|
|
317
|
+
{ "name": "Orders", "slug": "orders", "route": "/order" }
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The pair shares one URL segment, taken from the entity, so no URL ever carries a plural.
|
|
321
|
+
|
|
322
|
+
A doctype the schema shows as rows owned by another gets **no** `route`: it has no page, because
|
|
323
|
+
its records are edited inside their parent. Ownership is read from `ON DELETE CASCADE`, the only
|
|
324
|
+
place a database states it — a foreign key that cascades marks rows belonging to the parent, while
|
|
325
|
+
one that does not marks a reference to something outliving it. A doctype that anything links to
|
|
326
|
+
singly keeps its route regardless, so a link always has somewhere to navigate.
|
|
327
|
+
|
|
328
|
+
Every key is yours once written. Regeneration verifies an authored file and never overwrites it, so
|
|
329
|
+
a `route` you add, change or delete stays that way.
|
|
330
|
+
|
|
308
331
|
### Filtering types
|
|
309
332
|
|
|
310
333
|
GraphQL schemas (especially PostGraphile) expose many internal types. Use `--include` to
|
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
|
|
2
|
+
import { readFileSync as i, existsSync as y, mkdirSync as E, writeFileSync as C } from "node:fs";
|
|
3
|
+
import { resolve as u, join as A } from "node:path";
|
|
4
|
+
import { parseArgs as J } from "node:util";
|
|
5
|
+
import { getIntrospectionQuery as R } from "graphql";
|
|
6
|
+
import { c as F, a as G, p as L, m as q, f as D, s as T, v as U } from "./validation-BjRDR6sh.js";
|
|
7
|
+
async function Q(e, w) {
|
|
8
8
|
const o = await fetch(e, {
|
|
9
9
|
method: "POST",
|
|
10
10
|
headers: {
|
|
11
11
|
"Content-Type": "application/json",
|
|
12
|
-
...
|
|
12
|
+
...w
|
|
13
13
|
},
|
|
14
14
|
body: JSON.stringify({
|
|
15
|
-
query:
|
|
15
|
+
query: R()
|
|
16
16
|
})
|
|
17
17
|
});
|
|
18
18
|
if (!o.ok)
|
|
19
19
|
throw new Error(`Failed to fetch introspection: ${o.status} ${o.statusText}`);
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
throw new Error(`GraphQL errors: ${
|
|
23
|
-
if (!
|
|
20
|
+
const l = await o.json();
|
|
21
|
+
if (l.errors?.length)
|
|
22
|
+
throw new Error(`GraphQL errors: ${l.errors.map((r) => r.message).join(", ")}`);
|
|
23
|
+
if (!l.data)
|
|
24
24
|
throw new Error("No data in introspection response");
|
|
25
|
-
return
|
|
25
|
+
return l.data;
|
|
26
26
|
}
|
|
27
|
-
async function
|
|
28
|
-
const { values: e, positionals:
|
|
27
|
+
async function W() {
|
|
28
|
+
const { values: e, positionals: w } = J({
|
|
29
29
|
allowPositionals: !0,
|
|
30
30
|
options: {
|
|
31
31
|
endpoint: { type: "string", short: "e" },
|
|
@@ -37,72 +37,87 @@ async function D() {
|
|
|
37
37
|
names: { type: "string" },
|
|
38
38
|
"custom-scalars": { type: "string" },
|
|
39
39
|
"include-unmapped": { type: "boolean", default: !1 },
|
|
40
|
+
"no-aggregates": { type: "boolean", default: !1 },
|
|
40
41
|
check: { type: "boolean", default: !1 },
|
|
41
42
|
help: { type: "boolean", short: "h" }
|
|
42
43
|
}
|
|
43
|
-
}), o =
|
|
44
|
-
(e.help || !o) && (
|
|
45
|
-
const
|
|
44
|
+
}), o = w[0];
|
|
45
|
+
(e.help || !o) && (I(), 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));
|
|
46
|
+
const r = u(e.output), c = {
|
|
46
47
|
includeUnmappedMeta: e["include-unmapped"]
|
|
47
48
|
};
|
|
48
|
-
if (e.include && (
|
|
49
|
-
const t =
|
|
50
|
-
|
|
49
|
+
if (e.include && (c.include = e.include.split(",").map((t) => t.trim())), e.exclude && (c.exclude = e.exclude.split(",").map((t) => t.trim())), e.names) {
|
|
50
|
+
const t = u(e.names);
|
|
51
|
+
c.doctypeNames = JSON.parse(i(t, "utf-8"));
|
|
51
52
|
}
|
|
52
|
-
if (
|
|
53
|
-
const t =
|
|
54
|
-
|
|
53
|
+
if (c.onWarning = (t) => console.warn(` WARN: ${t}`), e["custom-scalars"]) {
|
|
54
|
+
const t = u(e["custom-scalars"]), n = i(t, "utf-8");
|
|
55
|
+
c.customScalars = JSON.parse(n);
|
|
55
56
|
}
|
|
56
57
|
let h;
|
|
57
58
|
if (e.endpoint)
|
|
58
|
-
console.log(`Fetching introspection from ${e.endpoint}...`), h = await
|
|
59
|
+
console.log(`Fetching introspection from ${e.endpoint}...`), h = await Q(e.endpoint);
|
|
59
60
|
else if (e.introspection) {
|
|
60
|
-
const t =
|
|
61
|
-
h =
|
|
61
|
+
const t = u(e.introspection), n = i(t, "utf-8"), a = JSON.parse(n);
|
|
62
|
+
h = a.data ?? a;
|
|
62
63
|
} else {
|
|
63
|
-
const t =
|
|
64
|
-
h =
|
|
64
|
+
const t = u(e.sdl);
|
|
65
|
+
h = i(t, "utf-8");
|
|
65
66
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
const v = F(h, c);
|
|
68
|
+
v.length === 0 && (console.warn("No entity types found in the schema. Check your include/exclude filters."), process.exit(0));
|
|
69
|
+
const O = {};
|
|
70
|
+
for (const t of v) {
|
|
71
|
+
const n = A(r, `${t.slug}.json`);
|
|
72
|
+
if (!y(n)) continue;
|
|
73
|
+
const a = G(JSON.parse(i(n, "utf-8")));
|
|
74
|
+
a !== void 0 && (O[t.name] = a);
|
|
75
|
+
}
|
|
76
|
+
const k = L(v, {
|
|
77
|
+
noAggregates: e["no-aggregates"],
|
|
78
|
+
identity: O,
|
|
79
|
+
onWarning: (t) => console.warn(` WARN: ${t}`)
|
|
80
|
+
});
|
|
81
|
+
y(r) || E(r, { recursive: !0 });
|
|
82
|
+
let S = 0, f = 0, m = 0;
|
|
83
|
+
const $ = [];
|
|
84
|
+
for (const { generated: t, basis: n, subset: a } of k) {
|
|
85
|
+
const x = `${t.slug}.json`, p = A(r, x);
|
|
86
|
+
let N = t;
|
|
87
|
+
if (y(p)) {
|
|
88
|
+
const { doctype: s, drift: d } = q(JSON.parse(i(p, "utf-8")), n, {
|
|
89
|
+
subset: a
|
|
90
|
+
});
|
|
91
|
+
N = s, $.push(...D(d));
|
|
79
92
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const g = { ...N };
|
|
94
|
+
Array.isArray(g.fields) && (g.fields = g.fields.map(T));
|
|
95
|
+
const b = JSON.stringify(g, null, " ") + `
|
|
96
|
+
`, j = y(p) && i(p, "utf-8") === b;
|
|
97
|
+
j || m++, !e.check && !j && C(p, b, "utf-8");
|
|
98
|
+
const P = U(N);
|
|
99
|
+
if (P.success) {
|
|
100
|
+
const s = t.fields.filter((d) => d._unmapped);
|
|
101
|
+
s.length > 0 && (S++, console.warn(
|
|
102
|
+
` WARN: ${x} has ${s.length} unmapped field(s): ${s.map((d) => d.fieldname).join(", ")}`
|
|
88
103
|
));
|
|
89
104
|
} else {
|
|
90
|
-
|
|
91
|
-
for (const
|
|
92
|
-
console.error(` ${
|
|
105
|
+
f++, console.error(` ERROR: ${x} failed validation:`);
|
|
106
|
+
for (const s of P.errors)
|
|
107
|
+
console.error(` ${s.path.join(".")}: ${s.message}`);
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
|
-
if (
|
|
110
|
+
if ($.length > 0) {
|
|
96
111
|
console.log(`
|
|
97
112
|
Drift between the authored doctypes and the schema (reported, not applied):`);
|
|
98
|
-
for (const t of
|
|
113
|
+
for (const t of $) console.log(t);
|
|
99
114
|
}
|
|
100
115
|
console.log(
|
|
101
116
|
`
|
|
102
|
-
${e.check ? "Checked" : "Generated"} ${
|
|
103
|
-
), (
|
|
117
|
+
${e.check ? "Checked" : "Generated"} ${k.length} doctype(s) in ${r}` + (m ? ` (${m} ${e.check ? "would change" : "written"})` : " (all up to date)") + (S ? ` (${S} with warnings)` : "") + (f ? ` (${f} with errors)` : "")
|
|
118
|
+
), (f > 0 || e.check && m > 0) && process.exit(1);
|
|
104
119
|
}
|
|
105
|
-
function
|
|
120
|
+
function I() {
|
|
106
121
|
console.log(`
|
|
107
122
|
stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
|
|
108
123
|
|
|
@@ -123,9 +138,20 @@ OPTIONS:
|
|
|
123
138
|
--names <file> JSON file mapping GraphQL type name to doctype name
|
|
124
139
|
--custom-scalars <file> JSON file mapping custom scalar names to field templates
|
|
125
140
|
--include-unmapped Include _graphqlType metadata on unmapped fields
|
|
141
|
+
--no-aggregates Emit only the entity doctype, not its aggregate
|
|
126
142
|
--check Report drift and exit non-zero if anything would change; write nothing
|
|
127
143
|
--help, -h Show this help message
|
|
128
144
|
|
|
145
|
+
Each table generates TWO doctypes: the entity (every column, backs the record form) and its
|
|
146
|
+
aggregate (the collection view, identity column only by default), named as simple plurals —
|
|
147
|
+
'task.json' and 'tasks.json'. Widen an aggregate by adding fields to it — curation survives
|
|
148
|
+
regeneration. A doctype whose name is already plural gets no aggregate, because it would claim
|
|
149
|
+
its own name and its own file; the run says so and still writes the entity.
|
|
150
|
+
|
|
151
|
+
An aggregate needs an identity column. A table keyed on 'id' gives one up; for a natural key,
|
|
152
|
+
declare 'primaryKey' on the field in the entity's own file and re-run — the aggregate is keyed
|
|
153
|
+
on whatever that file declares.
|
|
154
|
+
|
|
129
155
|
NOTE: an existing doctype file is the source of truth. Regeneration verifies it against the
|
|
130
156
|
schema and adds 'source: introspected' markers; it reports disagreements rather than
|
|
131
157
|
overwriting them, so hand-curation survives. Use --check in CI.
|
|
@@ -146,7 +172,7 @@ EXAMPLES:
|
|
|
146
172
|
--include "User,Post,Comment"
|
|
147
173
|
`);
|
|
148
174
|
}
|
|
149
|
-
|
|
175
|
+
W().catch((e) => {
|
|
150
176
|
console.error("Error:", e.message), process.exit(1);
|
|
151
177
|
});
|
|
152
178
|
//# 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, 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":";;;;;;AAgCA,eAAeA,EAAmBC,GAAkBC,GAA+D;AAClH,QAAMC,IAAW,MAAM,MAAMF,GAAU;AAAA,IACtC,QAAQ;AAAA,IACR,SAAS;AAAA,MACR,gBAAgB;AAAA,MAChB,GAAGC;AAAA,IAAA;AAAA,IAEJ,MAAM,KAAK,UAAU;AAAA,MACpB,OAAOE,EAAA;AAAA,IAAsB,CAC7B;AAAA,EAAA,CACD;AAED,MAAI,CAACD,EAAS;AACb,UAAM,IAAI,MAAM,kCAAkCA,EAAS,MAAM,IAAIA,EAAS,UAAU,EAAE;AAG3F,QAAME,IAA2E,MAAMF,EAAS,KAAA;AAEhG,MAAIE,EAAK,QAAQ;AAChB,UAAM,IAAI,MAAM,mBAAmBA,EAAK,OAAO,IAAI,CAAAC,MAAKA,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,EAAE;AAGhF,MAAI,CAACD,EAAK;AACT,UAAM,IAAI,MAAM,mCAAmC;AAGpD,SAAOA,EAAK;AACb;AAEA,eAAeE,IAAsB;AACpC,QAAM,EAAE,QAAAC,GAAQ,aAAAC,EAAA,IAAgBC,EAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACR,UAAU,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MACnC,eAAe,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MACxC,KAAK,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MAC9B,QAAQ,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MACjC,SAAS,EAAE,MAAM,SAAA;AAAA,MACjB,SAAS,EAAE,MAAM,SAAA;AAAA,MACjB,OAAO,EAAE,MAAM,SAAA;AAAA,MACf,kBAAkB,EAAE,MAAM,SAAA;AAAA,MAC1B,oBAAoB,EAAE,MAAM,WAAW,SAAS,GAAA;AAAA,MAChD,OAAO,EAAE,MAAM,WAAW,SAAS,GAAA;AAAA,MACnC,MAAM,EAAE,MAAM,WAAW,OAAO,IAAA;AAAA,IAAI;AAAA,EACrC,CACA,GAEKC,IAAUF,EAAY,CAAC;AAE7B,GAAID,EAAO,QAAQ,CAACG,OACnBC,EAAA,GACA,QAAQ,KAAKD,IAAU,IAAI,CAAC,IAGzBA,MAAY,eACf,QAAQ,MAAM,oBAAoBA,CAAO,EAAE,GAC3C,QAAQ,MAAM,8BAA8B,GAC5C,QAAQ,KAAK,CAAC,IAIK,CAACH,EAAO,UAAUA,EAAO,eAAeA,EAAO,GAAG,EAAE,OAAO,OAAO,EAAE,WACpE,MACnB,QAAQ,MAAM,uEAAuE,GACrF,QAAQ,KAAK,CAAC,IAGVA,EAAO,WACX,QAAQ,MAAM,4BAA4B,GAC1C,QAAQ,KAAK,CAAC;AAGf,QAAMK,IAAYC,EAAQN,EAAO,MAAM,GAGjCO,IAAoC;AAAA,IACzC,qBAAqBP,EAAO,kBAAkB;AAAA,EAAA;AAW/C,MARIA,EAAO,YACVO,EAAQ,UAAUP,EAAO,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAAQ,MAAKA,EAAE,KAAA,CAAM,IAG1DR,EAAO,YACVO,EAAQ,UAAUP,EAAO,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAAQ,MAAKA,EAAE,KAAA,CAAM,IAG1DR,EAAO,OAAO;AACjB,UAAMS,IAAYH,EAAQN,EAAO,KAAK;AACtC,IAAAO,EAAQ,eAAe,KAAK,MAAMG,EAAaD,GAAW,OAAO,CAAC;AAAA,EACnE;AAIA,MAFAF,EAAQ,YAAY,CAAAI,MAAW,QAAQ,KAAK,WAAWA,CAAO,EAAE,GAE5DX,EAAO,gBAAgB,GAAG;AAC7B,UAAMY,IAAcN,EAAQN,EAAO,gBAAgB,CAAC,GAC9Ca,IAAiBH,EAAaE,GAAa,OAAO;AACxD,IAAAL,EAAQ,gBAAgB,KAAK,MAAMM,CAAc;AAAA,EAClD;AAGA,MAAIC;AAEJ,MAAId,EAAO;AACV,YAAQ,IAAI,+BAA+BA,EAAO,QAAQ,KAAK,GAC/Dc,IAAS,MAAMtB,EAAmBQ,EAAO,QAAQ;AAAA,WACvCA,EAAO,eAAe;AAChC,UAAMe,IAAWT,EAAQN,EAAO,aAAa,GACvCgB,IAAUN,EAAaK,GAAU,OAAO,GACxCE,IAAS,KAAK,MAAMD,CAAO;AAEjC,IAAAF,IAASG,EAAO,QAAQA;AAAA,EACzB,OAAO;AACN,UAAMF,IAAWT,EAAQN,EAAO,GAAI;AACpC,IAAAc,IAASJ,EAAaK,GAAU,OAAO;AAAA,EACxC;AAGA,QAAMG,IAAWC,EAAqBL,GAAQP,CAAO;AAErD,EAAIW,EAAS,WAAW,MACvB,QAAQ,KAAK,0EAA0E,GACvF,QAAQ,KAAK,CAAC,IAIVE,EAAWf,CAAS,KACxBgB,EAAUhB,GAAW,EAAE,WAAW,GAAA,CAAM;AAGzC,MAAIiB,IAAW,GACXC,IAAS,GACTC,IAAU;AACd,QAAMC,IAAuB,CAAA;AAE7B,aAAWC,KAAaR,GAAU;AACjC,UAAMS,IAAW,GAAGD,EAAU,IAAI,SAC5BX,IAAWa,EAAKvB,GAAWsB,CAAQ;AAQzC,QAAIE,IAAiBH;AACrB,QAAIN,EAAWL,CAAQ,GAAG;AACzB,YAAM,EAAE,SAASe,GAAQ,OAAAC,EAAA,IAAUC;AAAA,QAClC,KAAK,MAAMtB,EAAaK,GAAU,OAAO,CAAC;AAAA,QAC1CW;AAAA,MAAA;AAED,MAAAG,IAASC,GACTL,EAAW,KAAK,GAAGQ,EAAmBF,CAAK,CAAC;AAAA,IAC7C;AAKA,UAAMlC,IAAO,KAAK,UAAUgC,GAAQ,MAAM,GAAI,IAAI;AAAA,GAC5CK,IAAYd,EAAWL,CAAQ,KAAKL,EAAaK,GAAU,OAAO,MAAMlB;AAC9E,IAAKqC,KAAWV,KAEZ,CAACxB,EAAO,SAAS,CAACkC,KACrBC,EAAcpB,GAAUlB,GAAM,OAAO;AAItC,UAAMuC,IAAaC,EAAgBR,CAAM;AACzC,QAAKO,EAAW,SAMT;AAEN,YAAME,IAAiBZ,EAAU,OAAO,OAAO,CAACa,MAAWA,EAAE,SAAS;AACtE,MAAID,EAAe,SAAS,MAC3BhB,KACA,QAAQ;AAAA,QACP,WAAWK,CAAQ,QAAQW,EAAe,MAAM,uBAAuBA,EACrE,IAAI,CAACC,MAAWA,EAAE,SAAS,EAC3B,KAAK,IAAI,CAAC;AAAA,MAAA;AAAA,IAGf,OAjByB;AACxB,MAAAhB,KACA,QAAQ,MAAM,YAAYI,CAAQ,qBAAqB;AACvD,iBAAWa,KAAOJ,EAAW;AAC5B,gBAAQ,MAAM,OAAOI,EAAI,KAAK,KAAK,GAAG,CAAC,KAAKA,EAAI,OAAO,EAAE;AAAA,IAE3D;AAAA,EAYD;AAEA,MAAIf,EAAW,SAAS,GAAG;AAC1B,YAAQ,IAAI;AAAA,4EAA+E;AAC3F,eAAWgB,KAAQhB,EAAY,SAAQ,IAAIgB,CAAI;AAAA,EAChD;AAEA,UAAQ;AAAA,IACP;AAAA,EAAKzC,EAAO,QAAQ,YAAY,WAAW,IAAIkB,EAAS,MAAM,kBAAkBb,CAAS,MACvFmB,IAAU,KAAKA,CAAO,IAAIxB,EAAO,QAAQ,iBAAiB,SAAS,MAAM,wBACzEsB,IAAW,KAAKA,CAAQ,oBAAoB,OAC5CC,IAAS,KAAKA,CAAM,kBAAkB;AAAA,EAAA,IAGrCA,IAAS,KAAMvB,EAAO,SAASwB,IAAU,MAC5C,QAAQ,KAAK,CAAC;AAEhB;AAEA,SAASpB,IAAkB;AAC1B,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAyCZ;AACD;AAEAL,IAAO,MAAM,CAAAyC,MAAO;AACnB,UAAQ,MAAM,UAAUA,EAAI,OAAO,GACnC,QAAQ,KAAK,CAAC;AACf,CAAC;"}
|
|
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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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as L, D as p, F as E, G as h, I as _, b as P, L as R, T as j, d as x, e as B, V as M, W as w, g as k, h as K, i as G, j as V, k as W, l as Q, n as Y, o as z, c as q, q as J, r as X, t as H, f as U, u as Z, w as $, x as ee, y as re, z as ae, B as te, C as ne, E as ie, m as se, H as oe, J as le, K as ce, M as de, p as ue, N as fe, O as Ae, s as ye, P as ge, Q as be, v as Se, R as ve } from "./validation-BjRDR6sh.js";
|
|
2
2
|
const l = {
|
|
3
3
|
ATextInput: "text",
|
|
4
4
|
ATextboxInput: "text",
|
|
@@ -11,13 +11,14 @@ const l = {
|
|
|
11
11
|
ADuration: "text",
|
|
12
12
|
ADateRange: "date",
|
|
13
13
|
ADropdown: "select",
|
|
14
|
+
ASegmentedControl: "select",
|
|
14
15
|
ACodeEditor: "code",
|
|
15
16
|
AFormLink: "link",
|
|
16
17
|
AFileAttach: "attach",
|
|
17
18
|
AQuantityInput: "quantity",
|
|
18
19
|
ACurrencyInput: "currency"
|
|
19
20
|
};
|
|
20
|
-
function
|
|
21
|
+
function S(e) {
|
|
21
22
|
return e ? l[e] : void 0;
|
|
22
23
|
}
|
|
23
24
|
const c = {
|
|
@@ -40,7 +41,7 @@ function t(e) {
|
|
|
40
41
|
function i(e) {
|
|
41
42
|
return typeof e == "string" && A.has(e);
|
|
42
43
|
}
|
|
43
|
-
function
|
|
44
|
+
function d(e) {
|
|
44
45
|
if (e === null || typeof e != "object" || Array.isArray(e)) return !1;
|
|
45
46
|
for (const a of Object.keys(e))
|
|
46
47
|
if (!y.has(a)) return !1;
|
|
@@ -48,12 +49,12 @@ function u(e) {
|
|
|
48
49
|
return !(r.variant !== void 0 && !i(r.variant) || r.color !== void 0 && typeof r.color != "string" || r.label !== void 0 && typeof r.label != "string");
|
|
49
50
|
}
|
|
50
51
|
function o(e) {
|
|
51
|
-
return i(e) ||
|
|
52
|
+
return i(e) || d(e);
|
|
52
53
|
}
|
|
53
54
|
function n(e) {
|
|
54
55
|
return Array.isArray(e.choices);
|
|
55
56
|
}
|
|
56
|
-
function
|
|
57
|
+
function m(e) {
|
|
57
58
|
if (e === null || typeof e != "object" || Array.isArray(e)) return !1;
|
|
58
59
|
const r = e;
|
|
59
60
|
return !(typeof r.label != "string" || r.variant !== void 0 && !i(r.variant) || r.color !== void 0 && typeof r.color != "string");
|
|
@@ -67,90 +68,96 @@ function s(e) {
|
|
|
67
68
|
const r = Object.entries(e);
|
|
68
69
|
return r.length === 0 ? !1 : r.every(([, a]) => o(a));
|
|
69
70
|
}
|
|
70
|
-
function
|
|
71
|
+
function D(e) {
|
|
71
72
|
return e === void 0 || Array.isArray(e) || !t(e) ? !1 : n(e);
|
|
72
73
|
}
|
|
73
|
-
function
|
|
74
|
+
function F(e) {
|
|
74
75
|
return e === void 0 ? [] : Array.isArray(e) ? e : t(e) ? n(e) ? e.choices : s(e) ? Object.keys(e) : [] : [];
|
|
75
76
|
}
|
|
76
|
-
function
|
|
77
|
-
return i(e) ? { label: r, variant: e } :
|
|
77
|
+
function u(e, r) {
|
|
78
|
+
return i(e) ? { label: r, variant: e } : d(e) ? {
|
|
78
79
|
label: e.label ?? r,
|
|
79
80
|
variant: e.variant,
|
|
80
81
|
color: e.color
|
|
81
82
|
} : { label: r };
|
|
82
83
|
}
|
|
83
|
-
function
|
|
84
|
+
function g(e, r) {
|
|
84
85
|
if (!(r === void 0 || r === "") && r in e)
|
|
85
|
-
return
|
|
86
|
+
return u(e[r], r);
|
|
86
87
|
}
|
|
87
|
-
function
|
|
88
|
+
function b(e, r) {
|
|
88
89
|
if (r === void 0 || r === "") return;
|
|
89
90
|
const a = e.badges;
|
|
90
91
|
if (!(!a || !(r in a)))
|
|
91
|
-
return
|
|
92
|
+
return u(a[r], r);
|
|
92
93
|
}
|
|
93
|
-
function
|
|
94
|
+
function T(e, r) {
|
|
94
95
|
if (!(e === void 0 || r === void 0 || r === "") && !Array.isArray(e) && t(e)) {
|
|
95
|
-
if (n(e)) return
|
|
96
|
-
if (s(e)) return
|
|
96
|
+
if (n(e)) return b(e, r);
|
|
97
|
+
if (s(e)) return g(e, r);
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
function
|
|
100
|
+
function C(e) {
|
|
100
101
|
return e === void 0 || Array.isArray(e) || !t(e) ? !1 : n(e) ? e.badges !== void 0 && Object.keys(e.badges).length > 0 : s(e);
|
|
101
102
|
}
|
|
102
103
|
export {
|
|
103
|
-
|
|
104
|
+
L as ActionDefinition,
|
|
104
105
|
O as CANONICAL_COMPONENTS,
|
|
105
106
|
l as COMPONENT_CATEGORY,
|
|
106
107
|
c as COMPONENT_LINK_EXPANSION,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
p as DoctypeFieldSchema,
|
|
109
|
+
E as FieldsetFieldSchema,
|
|
110
|
+
h as GQL_SCALAR_MAP,
|
|
111
|
+
_ as INTERNAL_SCALARS,
|
|
112
|
+
P as INTROSPECTED_IDENTITY_PROPS,
|
|
113
|
+
R as LINK_DISPLAY_SUFFIX,
|
|
114
|
+
j as TableFieldSchema,
|
|
115
|
+
x as TableViewConfig,
|
|
115
116
|
B as TriggerDefinition,
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
M as ValueFieldSchema,
|
|
118
|
+
w as WELL_KNOWN_SCALARS,
|
|
118
119
|
k as WorkflowLayout,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
V as
|
|
122
|
-
W as
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
K as WorkflowMeta,
|
|
121
|
+
G as aggregateDoctypeName,
|
|
122
|
+
V as buildAggregateDoctype,
|
|
123
|
+
W as buildScalarMap,
|
|
124
|
+
Q as camelToLabel,
|
|
125
|
+
Y as camelToSnake,
|
|
126
|
+
z as classifyFieldType,
|
|
127
|
+
S as componentCategory,
|
|
125
128
|
f as componentLinkExpansion,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
q as convertGraphQLSchema,
|
|
130
|
+
J as defaultIsEntityField,
|
|
131
|
+
X as defaultIsEntityType,
|
|
132
|
+
H as flattenFields,
|
|
133
|
+
U as formatDoctypeDrift,
|
|
134
|
+
Z as getDisplayField,
|
|
135
|
+
$ as getDoctypeSlug,
|
|
136
|
+
ee as getPrimaryKeyField,
|
|
137
|
+
re as getRecordIdField,
|
|
138
|
+
ae as getRecordIdentity,
|
|
139
|
+
C as hasBadgeOptions,
|
|
140
|
+
te as inferFieldKind,
|
|
141
|
+
ne as isActionAllowedInState,
|
|
142
|
+
m as isBadgeDescriptor,
|
|
138
143
|
s as isSelectChoiceMap,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
D as isSelectOptions,
|
|
145
|
+
ie as linkDisplayFieldname,
|
|
146
|
+
T as lookupBadge,
|
|
147
|
+
se as mergeIntrospectedDoctype,
|
|
148
|
+
oe as normalizeFieldKind,
|
|
149
|
+
le as parseDoctype,
|
|
150
|
+
ce as parseField,
|
|
151
|
+
de as pascalToSnake,
|
|
152
|
+
ue as planGeneration,
|
|
147
153
|
v as resolveLinkRenderMode,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
F as selectChoices,
|
|
155
|
+
fe as snakeToCamel,
|
|
156
|
+
Ae as snakeToLabel,
|
|
157
|
+
ye as stripFieldKind,
|
|
158
|
+
ge as toPascalCase,
|
|
159
|
+
be as toSlug,
|
|
160
|
+
Se as validateDoctype,
|
|
161
|
+
ve as validateField
|
|
155
162
|
};
|
|
156
163
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/component-meta.ts","../src/badge.ts"],"sourcesContent":["/**\n * Semantic category for a rendering component.\n *\n * `component` is the primary field axis, so the runtime consumers that need to know what a field\n * *means* (atable cell formatting / filter widgets, record-default init) derive it from here. This\n * is the single source of \"what kind of value does this component render\", keyed by the canonical\n * registered component names — each consumer maps the category to its own concern (filter widget,\n * default value, …).\n *\n * @public\n */\nexport type ComponentCategory =\n\t'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | 'code' | 'link' | 'attach' | 'quantity' | 'currency'\n\n/**\n * Canonical component → semantic category. Only the components Stonecrop ships with appear here;\n * custom/unknown component names have no category and consumers fall back to their default.\n * @public\n */\nexport const COMPONENT_CATEGORY: Record<string, ComponentCategory> = {\n\tATextInput: 'text',\n\tATextboxInput: 'text',\n\tANumericInput: 'number',\n\tACheckbox: 'boolean',\n\tADate: 'date',\n\tADatePicker: 'date',\n\tADateSelection: 'date',\n\tADateTime: 'datetime',\n\tADuration: 'text',\n\tADateRange: 'date',\n\tADropdown: 'select',\n\tACodeEditor: 'code',\n\tAFormLink: 'link',\n\tAFileAttach: 'attach',\n\tAQuantityInput: 'quantity',\n\tACurrencyInput: 'currency',\n}\n\n/**\n * Resolve a component's semantic category, or `undefined` for an unknown (custom) component —\n * callers treat that as \"no opinion\" and use their own default.\n * @public\n */\nexport function componentCategory(component?: string): ComponentCategory | undefined {\n\treturn component ? COMPONENT_CATEGORY[component] : undefined\n}\n\n/**\n * Whether a link component expands its target doctype, or renders the link inline.\n *\n * This is the *only* axis the component decides. It deliberately does not choose between an\n * embedded record and an embedded table: `cardinality` states whether the value is a scalar or\n * an array, which is a fact about the data rather than a rendering preference, so a component\n * must not be able to override it (an `AForm` over a `noneOrMany` link would be handed an array\n * it cannot render). Component names encode both axes — `AFormLink`/`ATableLink` are the inline\n * pair, `AForm`/`ATable` the expanding pair — but only the inline/expand half is authoritative.\n *\n * @public\n */\nexport type LinkExpansion = 'inline' | 'expand'\n\n/**\n * Canonical link component → expansion. Only components Stonecrop ships with appear here; an\n * unmapped (custom) component has none, and callers treat that as `expand` — the behaviour that\n * predates this map, so a custom component can never silently collapse a link to a picker.\n * @public\n */\nexport const COMPONENT_LINK_EXPANSION: Record<string, LinkExpansion> = {\n\tAFormLink: 'inline',\n\tAForm: 'expand',\n\tATable: 'expand',\n}\n\n/**\n * Resolve a component's link expansion, or `undefined` for an absent/unmapped component.\n * @public\n */\nexport function componentLinkExpansion(component?: string): LinkExpansion | undefined {\n\treturn component ? COMPONENT_LINK_EXPANSION[component] : undefined\n}\n\n/**\n * How a link field renders.\n *\n * - `inline` — a scalar id-picker; the target is *not* expanded (the field keeps its own value\n * and carries a `doctype` prop for async display-text resolution and navigation).\n * - `record` — the target doctype is resolved and embedded as a nested form.\n * - `table` — the target doctype is resolved and embedded as a child table.\n *\n * @public\n */\nexport type LinkRenderMode = 'inline' | 'record' | 'table'\n\n/**\n * Decide how a *declared* link (one with a `LinkDeclaration`) renders.\n *\n * Two independent axes: the **component** picks inline vs expand, and when expanding the\n * **cardinality** picks record vs table (many → table). The declaration's component wins over the\n * field's, matching the precedence the resolver already uses for the rendered component.\n *\n * This is the single definition of \"does this link expand\" — it is consumed by both the client\n * resolver (which builds the nested schema) and the server column builder (which must still\n * SELECT an `inline` link's FK column). Call it; never re-derive the rule at the call site, or\n * the two will drift and the client will render a table for a column the server never selected.\n *\n * @param link - the link declaration (only `component` and `cardinality` are consulted)\n * @param fieldComponent - the linked field's own `component`, used when the declaration names none\n * @public\n */\nexport function resolveLinkRenderMode(\n\tlink: { component?: string; cardinality?: string },\n\tfieldComponent?: string\n): LinkRenderMode {\n\tif (componentLinkExpansion(link.component ?? fieldComponent) === 'inline') return 'inline'\n\treturn link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne' ? 'table' : 'record'\n}\n\n/**\n * Every component Stonecrop ships with that can render a value field, sorted by name.\n *\n * The union of the two maps above is the definition, not a copy of it: a shipped component either\n * categorises a value ({@link COMPONENT_CATEGORY}) or is one of the link containers that has no\n * value of its own ({@link COMPONENT_LINK_EXPANSION}'s `AForm`/`ATable`). `AFieldset` is absent by\n * the same rule — it is a `kind: 'fieldset'` container, so it is never a value field's component.\n *\n * `component` is an **open** axis: any string is valid, and naming a custom component is how an app\n * renders a field Stonecrop ships no widget for. This list is therefore the set to *suggest* to an\n * author, and to check first-party data against — never a set to validate arbitrary input against.\n *\n * @public\n */\nexport const CANONICAL_COMPONENTS: readonly string[] = [\n\t...new Set([...Object.keys(COMPONENT_CATEGORY), ...Object.keys(COMPONENT_LINK_EXPANSION)]),\n].toSorted()\n","import type { FieldOptions } from './field'\n\n/**\n * Semantic badge variant. Maps to theme tokens `--sc-badge-{variant}-*`.\n * @public\n */\nexport type BadgeVariant = 'neutral' | 'success' | 'warning' | 'danger' | 'brand'\n\n/**\n * Where ABadge paints the same descriptor.\n * @public\n */\nexport type BadgePresentation = 'cell-fill' | 'input-accent'\n\n/**\n * Per-choice badge configuration in a Select options map.\n * @public\n */\nexport interface BadgeSpecObject {\n\tvariant?: BadgeVariant\n\tcolor?: string\n\tlabel?: string\n}\n\n/**\n * Value in a choice→badge map: shorthand variant or object form.\n * @public\n */\nexport type BadgeSpec = BadgeVariant | BadgeSpecObject\n\n/**\n * Select field options when choices carry badge colors.\n * @public\n */\nexport interface SelectOptions extends Record<string, unknown> {\n\tchoices: string[]\n\tbadges?: Record<string, BadgeSpec>\n}\n\n/**\n * Resolved badge for rendering. Returned by `format` or built from an options map.\n * @public\n */\nexport interface BadgeDescriptor {\n\tlabel: string\n\tvariant?: BadgeVariant\n\tcolor?: string\n}\n\nconst BADGE_VARIANTS = new Set<string>(['neutral', 'success', 'warning', 'danger', 'brand'])\n\nconst BADGE_SPEC_OBJECT_KEYS = new Set(['variant', 'color', 'label'])\n\nfunction isOptionsRecord(options: FieldOptions): options is Record<string, unknown> {\n\treturn !Array.isArray(options)\n}\n\nfunction isBadgeVariant(value: unknown): value is BadgeVariant {\n\treturn typeof value === 'string' && BADGE_VARIANTS.has(value)\n}\n\nfunction isBadgeSpecObject(value: unknown): value is BadgeSpecObject {\n\tif (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n\tfor (const key of Object.keys(value)) {\n\t\tif (!BADGE_SPEC_OBJECT_KEYS.has(key)) return false\n\t}\n\tconst obj = value as BadgeSpecObject\n\tif (obj.variant !== undefined && !isBadgeVariant(obj.variant)) return false\n\tif (obj.color !== undefined && typeof obj.color !== 'string') return false\n\tif (obj.label !== undefined && typeof obj.label !== 'string') return false\n\treturn true\n}\n\nfunction isBadgeSpec(value: unknown): value is BadgeSpec {\n\treturn isBadgeVariant(value) || isBadgeSpecObject(value)\n}\n\n/**\n * Narrows an options record to the structured `{ choices, badges }` form. A type predicate rather\n * than a cast: `SelectOptions` requires `choices`, so asserting into it trips `no-unsafe-type-assertion`.\n */\nfunction isStructuredSelectOptions(options: Record<string, unknown>): options is SelectOptions {\n\treturn Array.isArray(options.choices)\n}\n\n/**\n * True when `value` is a resolved badge descriptor for ACell / ADropdown.\n * @public\n */\nexport function isBadgeDescriptor(value: unknown): value is BadgeDescriptor {\n\tif (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n\t// Partial<> keeps this a widening assertion; asserting into BadgeDescriptor itself (required\n\t// `label`) narrows, which `no-unsafe-type-assertion` rejects.\n\tconst obj = value as Partial<BadgeDescriptor>\n\tif (typeof obj.label !== 'string') return false\n\tif (obj.variant !== undefined && !isBadgeVariant(obj.variant)) return false\n\tif (obj.color !== undefined && typeof obj.color !== 'string') return false\n\treturn true\n}\n\n/**\n * True when `options` is a Select choice map (`{ Open: \"warning\", ... }` or\n * `{ choices: [...], badges: {...} }`), not a quantity/currency/code config bag.\n * @public\n */\nexport function isSelectChoiceMap(options: FieldOptions | undefined): options is Record<string, BadgeSpec> {\n\tif (options === undefined || Array.isArray(options) || !isOptionsRecord(options)) return false\n\tif (isStructuredSelectOptions(options)) {\n\t\tconst badges = options.badges\n\t\tif (badges === undefined) return false\n\t\treturn Object.values(badges).every(isBadgeSpec)\n\t}\n\tconst entries = Object.entries(options)\n\tif (entries.length === 0) return false\n\treturn entries.every(([, value]) => isBadgeSpec(value))\n}\n\n/**\n * True when `options` uses the structured SelectOptions shape.\n * @public\n */\nexport function isSelectOptions(options: FieldOptions | undefined): options is SelectOptions {\n\tif (options === undefined || Array.isArray(options) || !isOptionsRecord(options)) return false\n\treturn isStructuredSelectOptions(options)\n}\n\n/**\n * Dropdown / filter choice strings.\n * @public\n */\nexport function selectChoices(options: FieldOptions | undefined): string[] {\n\tif (options === undefined) return []\n\tif (Array.isArray(options)) return options\n\tif (!isOptionsRecord(options)) return []\n\tif (isStructuredSelectOptions(options)) return options.choices\n\tif (isSelectChoiceMap(options)) return Object.keys(options)\n\treturn []\n}\n\nfunction normalizeBadgeSpec(spec: unknown, key: string): BadgeDescriptor {\n\tif (isBadgeVariant(spec)) {\n\t\treturn { label: key, variant: spec }\n\t}\n\t// A malformed spec still names a real choice, so keep the label and drop only the styling.\n\t// Reading `.variant`/`.color` off an unvalidated value is what let a misspelt variant reach the\n\t// DOM as an unmatched class, and threw outright on null.\n\tif (!isBadgeSpecObject(spec)) {\n\t\treturn { label: key }\n\t}\n\treturn {\n\t\tlabel: spec.label ?? key,\n\t\tvariant: spec.variant,\n\t\tcolor: spec.color,\n\t}\n}\n\nfunction lookupFromBareMap(map: Record<string, BadgeSpec>, key: string | undefined): BadgeDescriptor | undefined {\n\tif (key === undefined || key === '') return undefined\n\tif (!(key in map)) return undefined\n\treturn normalizeBadgeSpec(map[key], key)\n}\n\nfunction lookupFromStructured(options: SelectOptions, key: string | undefined): BadgeDescriptor | undefined {\n\tif (key === undefined || key === '') return undefined\n\tconst badges = options.badges\n\tif (!badges || !(key in badges)) return undefined\n\treturn normalizeBadgeSpec(badges[key], key)\n}\n\n/**\n * Resolve a stored choice value to a badge descriptor using field options.\n * @public\n */\nexport function lookupBadge(options: FieldOptions | undefined, key: string | undefined): BadgeDescriptor | undefined {\n\tif (options === undefined || key === undefined || key === '') return undefined\n\tif (Array.isArray(options)) return undefined\n\tif (!isOptionsRecord(options)) return undefined\n\tif (isStructuredSelectOptions(options)) return lookupFromStructured(options, key)\n\tif (isSelectChoiceMap(options)) return lookupFromBareMap(options, key)\n\treturn undefined\n}\n\n/**\n * Whether field options carry any badge mapping.\n * @public\n */\nexport function hasBadgeOptions(options: FieldOptions | undefined): boolean {\n\tif (options === undefined || Array.isArray(options)) return false\n\tif (!isOptionsRecord(options)) return false\n\tif (isStructuredSelectOptions(options)) return options.badges !== undefined && Object.keys(options.badges).length > 0\n\treturn isSelectChoiceMap(options)\n}\n"],"names":["COMPONENT_CATEGORY","componentCategory","component","COMPONENT_LINK_EXPANSION","componentLinkExpansion","resolveLinkRenderMode","link","fieldComponent","CANONICAL_COMPONENTS","BADGE_VARIANTS","BADGE_SPEC_OBJECT_KEYS","isOptionsRecord","options","isBadgeVariant","value","isBadgeSpecObject","key","obj","isBadgeSpec","isStructuredSelectOptions","isBadgeDescriptor","isSelectChoiceMap","badges","entries","isSelectOptions","selectChoices","normalizeBadgeSpec","spec","lookupFromBareMap","map","lookupFromStructured","lookupBadge","hasBadgeOptions"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/component-meta.ts","../src/badge.ts"],"sourcesContent":["/**\n * Semantic category for a rendering component.\n *\n * `component` is the primary field axis, so the runtime consumers that need to know what a field\n * *means* (atable cell formatting / filter widgets, record-default init) derive it from here. This\n * is the single source of \"what kind of value does this component render\", keyed by the canonical\n * registered component names — each consumer maps the category to its own concern (filter widget,\n * default value, …).\n *\n * @public\n */\nexport type ComponentCategory =\n\t'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | 'code' | 'link' | 'attach' | 'quantity' | 'currency'\n\n/**\n * Canonical component → semantic category. Only the components Stonecrop ships with appear here;\n * custom/unknown component names have no category and consumers fall back to their default.\n * @public\n */\nexport const COMPONENT_CATEGORY: Record<string, ComponentCategory> = {\n\tATextInput: 'text',\n\tATextboxInput: 'text',\n\tANumericInput: 'number',\n\tACheckbox: 'boolean',\n\tADate: 'date',\n\tADatePicker: 'date',\n\tADateSelection: 'date',\n\tADateTime: 'datetime',\n\tADuration: 'text',\n\tADateRange: 'date',\n\tADropdown: 'select',\n\tASegmentedControl: 'select',\n\tACodeEditor: 'code',\n\tAFormLink: 'link',\n\tAFileAttach: 'attach',\n\tAQuantityInput: 'quantity',\n\tACurrencyInput: 'currency',\n}\n\n/**\n * Resolve a component's semantic category, or `undefined` for an unknown (custom) component —\n * callers treat that as \"no opinion\" and use their own default.\n * @public\n */\nexport function componentCategory(component?: string): ComponentCategory | undefined {\n\treturn component ? COMPONENT_CATEGORY[component] : undefined\n}\n\n/**\n * Whether a link component expands its target doctype, or renders the link inline.\n *\n * This is the *only* axis the component decides. It deliberately does not choose between an\n * embedded record and an embedded table: `cardinality` states whether the value is a scalar or\n * an array, which is a fact about the data rather than a rendering preference, so a component\n * must not be able to override it (an `AForm` over a `noneOrMany` link would be handed an array\n * it cannot render). Component names encode both axes — `AFormLink`/`ATableLink` are the inline\n * pair, `AForm`/`ATable` the expanding pair — but only the inline/expand half is authoritative.\n *\n * @public\n */\nexport type LinkExpansion = 'inline' | 'expand'\n\n/**\n * Canonical link component → expansion. Only components Stonecrop ships with appear here; an\n * unmapped (custom) component has none, and callers treat that as `expand` — the behaviour that\n * predates this map, so a custom component can never silently collapse a link to a picker.\n * @public\n */\nexport const COMPONENT_LINK_EXPANSION: Record<string, LinkExpansion> = {\n\tAFormLink: 'inline',\n\tAForm: 'expand',\n\tATable: 'expand',\n}\n\n/**\n * Resolve a component's link expansion, or `undefined` for an absent/unmapped component.\n * @public\n */\nexport function componentLinkExpansion(component?: string): LinkExpansion | undefined {\n\treturn component ? COMPONENT_LINK_EXPANSION[component] : undefined\n}\n\n/**\n * How a link field renders.\n *\n * - `inline` — a scalar id-picker; the target is *not* expanded (the field keeps its own value\n * and carries a `doctype` prop for async display-text resolution and navigation).\n * - `record` — the target doctype is resolved and embedded as a nested form.\n * - `table` — the target doctype is resolved and embedded as a child table.\n *\n * @public\n */\nexport type LinkRenderMode = 'inline' | 'record' | 'table'\n\n/**\n * Decide how a *declared* link (one with a `LinkDeclaration`) renders.\n *\n * Two independent axes: the **component** picks inline vs expand, and when expanding the\n * **cardinality** picks record vs table (many → table). The declaration's component wins over the\n * field's, matching the precedence the resolver already uses for the rendered component.\n *\n * This is the single definition of \"does this link expand\" — it is consumed by both the client\n * resolver (which builds the nested schema) and the server column builder (which must still\n * SELECT an `inline` link's FK column). Call it; never re-derive the rule at the call site, or\n * the two will drift and the client will render a table for a column the server never selected.\n *\n * @param link - the link declaration (only `component` and `cardinality` are consulted)\n * @param fieldComponent - the linked field's own `component`, used when the declaration names none\n * @public\n */\nexport function resolveLinkRenderMode(\n\tlink: { component?: string; cardinality?: string },\n\tfieldComponent?: string\n): LinkRenderMode {\n\tif (componentLinkExpansion(link.component ?? fieldComponent) === 'inline') return 'inline'\n\treturn link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne' ? 'table' : 'record'\n}\n\n/**\n * Every component Stonecrop ships with that can render a value field, sorted by name.\n *\n * The union of the two maps above is the definition, not a copy of it: a shipped component either\n * categorises a value ({@link COMPONENT_CATEGORY}) or is one of the link containers that has no\n * value of its own ({@link COMPONENT_LINK_EXPANSION}'s `AForm`/`ATable`). `AFieldset` is absent by\n * the same rule — it is a `kind: 'fieldset'` container, so it is never a value field's component.\n *\n * `component` is an **open** axis: any string is valid, and naming a custom component is how an app\n * renders a field Stonecrop ships no widget for. This list is therefore the set to *suggest* to an\n * author, and to check first-party data against — never a set to validate arbitrary input against.\n *\n * @public\n */\nexport const CANONICAL_COMPONENTS: readonly string[] = [\n\t...new Set([...Object.keys(COMPONENT_CATEGORY), ...Object.keys(COMPONENT_LINK_EXPANSION)]),\n].toSorted()\n","import type { FieldOptions } from './field'\n\n/**\n * Semantic badge variant. Maps to theme tokens `--sc-badge-{variant}-*`.\n * @public\n */\nexport type BadgeVariant = 'neutral' | 'success' | 'warning' | 'danger' | 'brand'\n\n/**\n * Where ABadge paints the same descriptor.\n * @public\n */\nexport type BadgePresentation = 'cell-fill' | 'input-accent'\n\n/**\n * Per-choice badge configuration in a Select options map.\n * @public\n */\nexport interface BadgeSpecObject {\n\tvariant?: BadgeVariant\n\tcolor?: string\n\tlabel?: string\n}\n\n/**\n * Value in a choice→badge map: shorthand variant or object form.\n * @public\n */\nexport type BadgeSpec = BadgeVariant | BadgeSpecObject\n\n/**\n * Select field options when choices carry badge colors.\n * @public\n */\nexport interface SelectOptions extends Record<string, unknown> {\n\tchoices: string[]\n\tbadges?: Record<string, BadgeSpec>\n}\n\n/**\n * Resolved badge for rendering. Returned by `format` or built from an options map.\n * @public\n */\nexport interface BadgeDescriptor {\n\tlabel: string\n\tvariant?: BadgeVariant\n\tcolor?: string\n}\n\nconst BADGE_VARIANTS = new Set<string>(['neutral', 'success', 'warning', 'danger', 'brand'])\n\nconst BADGE_SPEC_OBJECT_KEYS = new Set(['variant', 'color', 'label'])\n\nfunction isOptionsRecord(options: FieldOptions): options is Record<string, unknown> {\n\treturn !Array.isArray(options)\n}\n\nfunction isBadgeVariant(value: unknown): value is BadgeVariant {\n\treturn typeof value === 'string' && BADGE_VARIANTS.has(value)\n}\n\nfunction isBadgeSpecObject(value: unknown): value is BadgeSpecObject {\n\tif (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n\tfor (const key of Object.keys(value)) {\n\t\tif (!BADGE_SPEC_OBJECT_KEYS.has(key)) return false\n\t}\n\tconst obj = value as BadgeSpecObject\n\tif (obj.variant !== undefined && !isBadgeVariant(obj.variant)) return false\n\tif (obj.color !== undefined && typeof obj.color !== 'string') return false\n\tif (obj.label !== undefined && typeof obj.label !== 'string') return false\n\treturn true\n}\n\nfunction isBadgeSpec(value: unknown): value is BadgeSpec {\n\treturn isBadgeVariant(value) || isBadgeSpecObject(value)\n}\n\n/**\n * Narrows an options record to the structured `{ choices, badges }` form. A type predicate rather\n * than a cast: `SelectOptions` requires `choices`, so asserting into it trips `no-unsafe-type-assertion`.\n */\nfunction isStructuredSelectOptions(options: Record<string, unknown>): options is SelectOptions {\n\treturn Array.isArray(options.choices)\n}\n\n/**\n * True when `value` is a resolved badge descriptor for ACell / ADropdown.\n * @public\n */\nexport function isBadgeDescriptor(value: unknown): value is BadgeDescriptor {\n\tif (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n\t// Partial<> keeps this a widening assertion; asserting into BadgeDescriptor itself (required\n\t// `label`) narrows, which `no-unsafe-type-assertion` rejects.\n\tconst obj = value as Partial<BadgeDescriptor>\n\tif (typeof obj.label !== 'string') return false\n\tif (obj.variant !== undefined && !isBadgeVariant(obj.variant)) return false\n\tif (obj.color !== undefined && typeof obj.color !== 'string') return false\n\treturn true\n}\n\n/**\n * True when `options` is a Select choice map (`{ Open: \"warning\", ... }` or\n * `{ choices: [...], badges: {...} }`), not a quantity/currency/code config bag.\n * @public\n */\nexport function isSelectChoiceMap(options: FieldOptions | undefined): options is Record<string, BadgeSpec> {\n\tif (options === undefined || Array.isArray(options) || !isOptionsRecord(options)) return false\n\tif (isStructuredSelectOptions(options)) {\n\t\tconst badges = options.badges\n\t\tif (badges === undefined) return false\n\t\treturn Object.values(badges).every(isBadgeSpec)\n\t}\n\tconst entries = Object.entries(options)\n\tif (entries.length === 0) return false\n\treturn entries.every(([, value]) => isBadgeSpec(value))\n}\n\n/**\n * True when `options` uses the structured SelectOptions shape.\n * @public\n */\nexport function isSelectOptions(options: FieldOptions | undefined): options is SelectOptions {\n\tif (options === undefined || Array.isArray(options) || !isOptionsRecord(options)) return false\n\treturn isStructuredSelectOptions(options)\n}\n\n/**\n * Dropdown / filter choice strings.\n * @public\n */\nexport function selectChoices(options: FieldOptions | undefined): string[] {\n\tif (options === undefined) return []\n\tif (Array.isArray(options)) return options\n\tif (!isOptionsRecord(options)) return []\n\tif (isStructuredSelectOptions(options)) return options.choices\n\tif (isSelectChoiceMap(options)) return Object.keys(options)\n\treturn []\n}\n\nfunction normalizeBadgeSpec(spec: unknown, key: string): BadgeDescriptor {\n\tif (isBadgeVariant(spec)) {\n\t\treturn { label: key, variant: spec }\n\t}\n\t// A malformed spec still names a real choice, so keep the label and drop only the styling.\n\t// Reading `.variant`/`.color` off an unvalidated value is what let a misspelt variant reach the\n\t// DOM as an unmatched class, and threw outright on null.\n\tif (!isBadgeSpecObject(spec)) {\n\t\treturn { label: key }\n\t}\n\treturn {\n\t\tlabel: spec.label ?? key,\n\t\tvariant: spec.variant,\n\t\tcolor: spec.color,\n\t}\n}\n\nfunction lookupFromBareMap(map: Record<string, BadgeSpec>, key: string | undefined): BadgeDescriptor | undefined {\n\tif (key === undefined || key === '') return undefined\n\tif (!(key in map)) return undefined\n\treturn normalizeBadgeSpec(map[key], key)\n}\n\nfunction lookupFromStructured(options: SelectOptions, key: string | undefined): BadgeDescriptor | undefined {\n\tif (key === undefined || key === '') return undefined\n\tconst badges = options.badges\n\tif (!badges || !(key in badges)) return undefined\n\treturn normalizeBadgeSpec(badges[key], key)\n}\n\n/**\n * Resolve a stored choice value to a badge descriptor using field options.\n * @public\n */\nexport function lookupBadge(options: FieldOptions | undefined, key: string | undefined): BadgeDescriptor | undefined {\n\tif (options === undefined || key === undefined || key === '') return undefined\n\tif (Array.isArray(options)) return undefined\n\tif (!isOptionsRecord(options)) return undefined\n\tif (isStructuredSelectOptions(options)) return lookupFromStructured(options, key)\n\tif (isSelectChoiceMap(options)) return lookupFromBareMap(options, key)\n\treturn undefined\n}\n\n/**\n * Whether field options carry any badge mapping.\n * @public\n */\nexport function hasBadgeOptions(options: FieldOptions | undefined): boolean {\n\tif (options === undefined || Array.isArray(options)) return false\n\tif (!isOptionsRecord(options)) return false\n\tif (isStructuredSelectOptions(options)) return options.badges !== undefined && Object.keys(options.badges).length > 0\n\treturn isSelectChoiceMap(options)\n}\n"],"names":["COMPONENT_CATEGORY","componentCategory","component","COMPONENT_LINK_EXPANSION","componentLinkExpansion","resolveLinkRenderMode","link","fieldComponent","CANONICAL_COMPONENTS","BADGE_VARIANTS","BADGE_SPEC_OBJECT_KEYS","isOptionsRecord","options","isBadgeVariant","value","isBadgeSpecObject","key","obj","isBadgeSpec","isStructuredSelectOptions","isBadgeDescriptor","isSelectChoiceMap","badges","entries","isSelectOptions","selectChoices","normalizeBadgeSpec","spec","lookupFromBareMap","map","lookupFromStructured","lookupBadge","hasBadgeOptions"],"mappings":"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"}
|