@stonecrop/schema 0.16.3 → 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 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
- | `--overrides <file>` | | JSON file with per-type, per-field overrides |
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
- ### Per-field overrides
373
+ ### Doctype name remap
359
374
 
360
- Override the generated field definition for specific types and fields:
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
- --overrides overrides.json
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 u, existsSync as S, mkdirSync as w, writeFileSync as x } from "node:fs";
3
- import { resolve as c, join as O } from "node:path";
4
- import { parseArgs as $ } from "node:util";
5
- import { getIntrospectionQuery as N } from "graphql";
6
- import { c as P, v as j } from "./validation-Dyxjvaio.js";
7
- async function C(e, m) {
8
- const t = await fetch(e, {
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
- ...m
12
+ ...f
13
13
  },
14
14
  body: JSON.stringify({
15
- query: N()
15
+ query: j()
16
16
  })
17
17
  });
18
- if (!t.ok)
19
- throw new Error(`Failed to fetch introspection: ${t.status} ${t.statusText}`);
20
- const i = await t.json();
21
- if (i.errors?.length)
22
- throw new Error(`GraphQL errors: ${i.errors.map((r) => r.message).join(", ")}`);
23
- if (!i.data)
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 i.data;
25
+ return p.data;
26
26
  }
27
- async function E() {
28
- const { values: e, positionals: m } = $({
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
- overrides: { type: "string" },
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
- }), t = m[0];
43
- (e.help || !t) && (q(), process.exit(t ? 0 : 1)), t !== "generate" && (console.error(`Unknown command: ${t}`), 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));
44
- const r = c(e.output), a = {
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((o) => o.trim())), e.exclude && (a.exclude = e.exclude.split(",").map((o) => o.trim())), e.overrides) {
48
- const o = c(e.overrides), s = u(o, "utf-8");
49
- a.typeOverrides = JSON.parse(s);
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 o = c(e["custom-scalars"]), s = u(o, "utf-8");
53
- a.customScalars = JSON.parse(s);
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 l;
56
+ let h;
56
57
  if (e.endpoint)
57
- console.log(`Fetching introspection from ${e.endpoint}...`), l = await C(e.endpoint);
58
+ console.log(`Fetching introspection from ${e.endpoint}...`), h = await q(e.endpoint);
58
59
  else if (e.introspection) {
59
- const o = c(e.introspection), s = u(o, "utf-8"), d = JSON.parse(s);
60
- l = d.data ?? d;
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 o = c(e.sdl);
63
- l = u(o, "utf-8");
63
+ const t = d(e.sdl);
64
+ h = c(t, "utf-8");
64
65
  }
65
- const f = P(l, a);
66
- f.length === 0 && (console.warn("No entity types found in the schema. Check your include/exclude filters."), process.exit(0)), S(r) || w(r, { recursive: !0 });
67
- let h = 0, p = 0;
68
- for (const o of f) {
69
- const s = `${o.slug}.json`, d = O(r, s), v = JSON.stringify(o, null, " ");
70
- x(d, v + `
71
- `, "utf-8");
72
- const g = j(o);
73
- if (g.success) {
74
- const n = o.fields.filter((y) => y._unmapped);
75
- n.length > 0 && (h++, console.warn(
76
- ` WARN: ${s} has ${n.length} unmapped field(s): ${n.map((y) => y.fieldname).join(", ")}`
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
- p++, console.error(` ERROR: ${s} failed validation:`);
80
- for (const n of g.errors)
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 ${f.length} doctype(s) in ${r}` + (h ? ` (${h} with warnings)` : "") + (p ? ` (${p} with errors)` : "")
87
- ), p > 0 && process.exit(1);
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 q() {
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
- --overrides <file> JSON file with per-type field overrides
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
- E().catch((e) => {
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 u, G as p, I as S, T, a as m, b as L, V as y, W as N, d as C, e as F, f as b, g as f, h as k, i as O, c as D, j as x, k as I, l as E, n as h, p as _, m as M, o as g, s as v, q as P, t as R, r as w, v as W, u as G } from "./validation-Dyxjvaio.js";
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",
@@ -14,7 +14,8 @@ const a = {
14
14
  ACodeEditor: "code",
15
15
  AFormLink: "link",
16
16
  AFileAttach: "attach",
17
- AQuantityInput: "quantity"
17
+ AQuantityInput: "quantity",
18
+ ACurrencyInput: "currency"
18
19
  };
19
20
  function o(e) {
20
21
  return e ? a[e] : void 0;
@@ -30,45 +31,50 @@ function i(e) {
30
31
  function s(e, n) {
31
32
  return i(e.component ?? n) === "inline" ? "inline" : e.cardinality === "noneOrMany" || e.cardinality === "atLeastOne" ? "table" : "record";
32
33
  }
33
- const l = [
34
+ const r = [
34
35
  .../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(t)])
35
36
  ].toSorted();
36
37
  export {
37
38
  d as ActionDefinition,
38
- l as CANONICAL_COMPONENTS,
39
+ r as CANONICAL_COMPONENTS,
39
40
  a as COMPONENT_CATEGORY,
40
41
  t as COMPONENT_LINK_EXPANSION,
41
42
  A as DoctypeFieldSchema,
42
- u as FieldsetFieldSchema,
43
- p as GQL_SCALAR_MAP,
44
- S as INTERNAL_SCALARS,
45
- T as TableFieldSchema,
46
- m as TableViewConfig,
47
- L as TriggerDefinition,
48
- y as ValueFieldSchema,
49
- N as WELL_KNOWN_SCALARS,
50
- C as WorkflowLayout,
51
- F as WorkflowMeta,
52
- b as buildScalarMap,
53
- f as camelToLabel,
54
- k as camelToSnake,
55
- O as classifyFieldType,
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,
52
+ N as WorkflowLayout,
53
+ C as WorkflowMeta,
54
+ f as buildScalarMap,
55
+ F as camelToLabel,
56
+ O as camelToSnake,
57
+ b as classifyFieldType,
56
58
  o as componentCategory,
57
59
  i as componentLinkExpansion,
58
- D as convertGraphQLSchema,
59
- x as defaultIsEntityField,
60
- I as defaultIsEntityType,
61
- E as isActionAllowedInState,
62
- h as normalizeFieldKind,
63
- _ as parseDoctype,
64
- M as parseField,
65
- g as pascalToSnake,
60
+ k as convertGraphQLSchema,
61
+ E as defaultIsEntityField,
62
+ x as defaultIsEntityType,
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,
66
72
  s as resolveLinkRenderMode,
67
- v as snakeToCamel,
68
- P as snakeToLabel,
69
- R as toPascalCase,
70
- w as toSlug,
71
- W as validateDoctype,
72
- G as validateField
73
+ G as snakeToCamel,
74
+ K as snakeToLabel,
75
+ j as toPascalCase,
76
+ Q as toSlug,
77
+ V as validateDoctype,
78
+ q as validateField
73
79
  };
74
80
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/component-meta.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'\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}\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"],"names":["COMPONENT_CATEGORY","componentCategory","component","COMPONENT_LINK_EXPANSION","componentLinkExpansion","resolveLinkRenderMode","link","fieldComponent","CANONICAL_COMPONENTS"],"mappings":";AAmBO,MAAMA,IAAwD;AAAA,EACpE,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AAAA,EACf,WAAW;AAAA,EACX,OAAO;AAAA,EACP,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,WAAW;AAAA,EACX,aAAa;AAAA,EACb,gBAAgB;AACjB;AAOO,SAASC,EAAkBC,GAAmD;AACpF,SAAOA,IAAYF,EAAmBE,CAAS,IAAI;AACpD;AAsBO,MAAMC,IAA0D;AAAA,EACtE,WAAW;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AACT;AAMO,SAASC,EAAuBF,GAA+C;AACrF,SAAOA,IAAYC,EAAyBD,CAAS,IAAI;AAC1D;AA8BO,SAASG,EACfC,GACAC,GACiB;AACjB,SAAIH,EAAuBE,EAAK,aAAaC,CAAc,MAAM,WAAiB,WAC3ED,EAAK,gBAAgB,gBAAgBA,EAAK,gBAAgB,eAAe,UAAU;AAC3F;AAgBO,MAAME,IAA0C;AAAA,EACtD,GAAG,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAKR,CAAkB,GAAG,GAAG,OAAO,KAAKG,CAAwB,CAAC,CAAC;AAC1F,EAAE,SAAA;"}
1
+ {"version":3,"file":"index.js","sources":["../src/component-meta.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"],"names":["COMPONENT_CATEGORY","componentCategory","component","COMPONENT_LINK_EXPANSION","componentLinkExpansion","resolveLinkRenderMode","link","fieldComponent","CANONICAL_COMPONENTS"],"mappings":";AAmBO,MAAMA,IAAwD;AAAA,EACpE,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AAAA,EACf,WAAW;AAAA,EACX,OAAO;AAAA,EACP,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,WAAW;AAAA,EACX,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,gBAAgB;AACjB;AAOO,SAASC,EAAkBC,GAAmD;AACpF,SAAOA,IAAYF,EAAmBE,CAAS,IAAI;AACpD;AAsBO,MAAMC,IAA0D;AAAA,EACtE,WAAW;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AACT;AAMO,SAASC,EAAuBF,GAA+C;AACrF,SAAOA,IAAYC,EAAyBD,CAAS,IAAI;AAC1D;AA8BO,SAASG,EACfC,GACAC,GACiB;AACjB,SAAIH,EAAuBE,EAAK,aAAaC,CAAc,MAAM,WAAiB,WAC3ED,EAAK,gBAAgB,gBAAgBA,EAAK,gBAAgB,eAAe,UAAU;AAC3F;AAgBO,MAAME,IAA0C;AAAA,EACtD,GAAG,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAKR,CAAkB,GAAG,GAAG,OAAO,KAAKG,CAAwB,CAAC,CAAC;AAC1F,EAAE,SAAA;"}