@stonecrop/schema 0.10.16 → 0.11.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 +87 -26
- package/dist/cli.cjs +41 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/converter/heuristics.js +8 -6
- package/dist/converter/index.js +20 -3
- package/dist/converter-DNwpowpq.js +459 -0
- package/dist/converter-DNwpowpq.js.map +1 -0
- package/dist/doctype.js +90 -5
- package/dist/field.js +5 -3
- package/dist/fieldtype.js +0 -2
- package/dist/index-0qWNlDQ5.js +450 -0
- package/dist/index-0qWNlDQ5.js.map +1 -0
- package/dist/index-2UVTfbcY.js +448 -0
- package/dist/index-2UVTfbcY.js.map +1 -0
- package/dist/index-9QjWZ_PC.js +449 -0
- package/dist/index-9QjWZ_PC.js.map +1 -0
- package/dist/index-BCADNO5M.js +449 -0
- package/dist/index-BCADNO5M.js.map +1 -0
- package/dist/index-BRQJGVFR.js +453 -0
- package/dist/index-BRQJGVFR.js.map +1 -0
- package/dist/index-BatnoC-J.js +429 -0
- package/dist/index-BatnoC-J.js.map +1 -0
- package/dist/index-BhMd2_xl.js +454 -0
- package/dist/index-BhMd2_xl.js.map +1 -0
- package/dist/index-CLc5mUMQ.js +478 -0
- package/dist/index-CLc5mUMQ.js.map +1 -0
- package/dist/index-COrltkHl.js +401 -0
- package/dist/index-COrltkHl.js.map +1 -0
- package/dist/index-CvN9xK1B.js +453 -0
- package/dist/index-CvN9xK1B.js.map +1 -0
- package/dist/index-CzoRIy1-.js +408 -0
- package/dist/index-CzoRIy1-.js.map +1 -0
- package/dist/index-DIb_Z0wI.js +476 -0
- package/dist/index-DIb_Z0wI.js.map +1 -0
- package/dist/index-DNROIEMe.js +449 -0
- package/dist/index-DNROIEMe.js.map +1 -0
- package/dist/index-DUFcQC-H.js +449 -0
- package/dist/index-DUFcQC-H.js.map +1 -0
- package/dist/index-DmD8vmuB.js +449 -0
- package/dist/index-DmD8vmuB.js.map +1 -0
- package/dist/index-XZZbfFxT.js +434 -0
- package/dist/index-XZZbfFxT.js.map +1 -0
- package/dist/index-aeXXzPET.cjs +2 -0
- package/dist/index-aeXXzPET.cjs.map +1 -0
- package/dist/index-neVkjlgB.js +446 -0
- package/dist/index-neVkjlgB.js.map +1 -0
- package/dist/index-xdlVWldg.js +408 -0
- package/dist/index-xdlVWldg.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +23 -29
- package/dist/jsonschema.js +225 -0
- package/dist/schema.d.ts +206 -35
- package/dist/src/cli.js +210 -0
- package/dist/src/converter/heuristics.d.ts.map +1 -1
- package/dist/src/converter/heuristics.js +256 -0
- package/dist/src/converter/index.d.ts.map +1 -1
- package/dist/src/converter/index.js +164 -0
- package/dist/src/converter/scalars.js +86 -0
- package/dist/src/converter/types.d.ts +2 -0
- package/dist/src/converter/types.d.ts.map +1 -1
- package/dist/src/converter/types.js +5 -0
- package/dist/src/doctype.d.ts +176 -12
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +67 -0
- package/dist/src/field.d.ts +3 -2
- package/dist/src/field.d.ts.map +1 -1
- package/dist/src/field.js +108 -0
- package/dist/src/fieldtype.d.ts +0 -1
- package/dist/src/fieldtype.d.ts.map +1 -1
- package/dist/src/fieldtype.js +75 -0
- package/dist/src/index.d.ts +5 -9
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +12 -0
- package/dist/src/jsonschema.d.ts +95 -0
- package/dist/src/jsonschema.d.ts.map +1 -0
- package/dist/src/naming.js +106 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/validation.js +60 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ const decimalField: FieldMeta = {
|
|
|
86
86
|
|
|
87
87
|
### Doctype Metadata
|
|
88
88
|
|
|
89
|
-
`DoctypeMeta` defines a complete doctype with fields, workflow, and inheritance:
|
|
89
|
+
`DoctypeMeta` defines a complete doctype with fields, links, workflow, and inheritance:
|
|
90
90
|
|
|
91
91
|
```typescript
|
|
92
92
|
import { DoctypeMeta } from '@stonecrop/schema'
|
|
@@ -105,11 +105,19 @@ const doctype: DoctypeMeta = {
|
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
fieldname: 'items',
|
|
108
|
-
fieldtype: '
|
|
109
|
-
label: '
|
|
110
|
-
options: 'sales-order-item',
|
|
108
|
+
fieldtype: 'Link',
|
|
109
|
+
label: 'Items',
|
|
110
|
+
options: 'sales-order-item',
|
|
111
111
|
},
|
|
112
112
|
],
|
|
113
|
+
links: {
|
|
114
|
+
items: {
|
|
115
|
+
target: 'sales-order-item',
|
|
116
|
+
cardinality: 'noneOrMany',
|
|
117
|
+
backlink: 'sales_order',
|
|
118
|
+
fieldname: 'items',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
113
121
|
workflow: {
|
|
114
122
|
states: ['Draft', 'Submitted', 'Cancelled'],
|
|
115
123
|
actions: {
|
|
@@ -124,6 +132,35 @@ const doctype: DoctypeMeta = {
|
|
|
124
132
|
}
|
|
125
133
|
```
|
|
126
134
|
|
|
135
|
+
### Link Declarations
|
|
136
|
+
|
|
137
|
+
`links` on `DoctypeMeta` declares relationships to other doctypes. Each link has a `target`, `cardinality`, and optional `backlink`:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { LinkDeclaration, Cardinality } from '@stonecrop/schema'
|
|
141
|
+
|
|
142
|
+
// Cardinality values:
|
|
143
|
+
// 'one' — exactly 1 (required pointer)
|
|
144
|
+
// 'atMostOne' — 0 or 1 (optional pointer)
|
|
145
|
+
// 'noneOrMany' — 0 or more (optional collection)
|
|
146
|
+
// 'atLeastOne' — 1 or more (required collection)
|
|
147
|
+
|
|
148
|
+
const links: Record<string, LinkDeclaration> = {
|
|
149
|
+
// 1:many — ancestor has descendants
|
|
150
|
+
tasks: {
|
|
151
|
+
target: 'recipe-task',
|
|
152
|
+
cardinality: 'noneOrMany',
|
|
153
|
+
backlink: 'recipe', // fieldname on recipe-task that points back
|
|
154
|
+
},
|
|
155
|
+
// Self-referential — version lineage
|
|
156
|
+
supersededBy: {
|
|
157
|
+
target: 'recipe',
|
|
158
|
+
cardinality: 'atMostOne',
|
|
159
|
+
backlink: 'supersededBy',
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
127
164
|
### Workflow and Actions
|
|
128
165
|
|
|
129
166
|
Define state machines and actions for doctypes:
|
|
@@ -151,6 +188,36 @@ const workflow: WorkflowMeta = {
|
|
|
151
188
|
}
|
|
152
189
|
```
|
|
153
190
|
|
|
191
|
+
## Client Interfaces
|
|
192
|
+
|
|
193
|
+
`DataClient` is the interface that any data transport must implement. `GetRecordOptions` and `GetRecordsOptions` are the option types:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import type { DataClient, GetRecordOptions, GetRecordsOptions } from '@stonecrop/schema'
|
|
197
|
+
|
|
198
|
+
// Fetch a record — with optional nested link sub-selections
|
|
199
|
+
const record = await client.getRecord({ name: 'Recipe' }, 'r1', {
|
|
200
|
+
includeNested: true, // fetch all descendant links
|
|
201
|
+
maxDepth: 2, // limit recursion depth
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Fetch only specific links
|
|
205
|
+
const record = await client.getRecord({ name: 'Recipe' }, 'r1', {
|
|
206
|
+
includeNested: ['tasks'], // fetch only the tasks link
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Fetch multiple records
|
|
210
|
+
const records = await client.getRecords(
|
|
211
|
+
{ name: 'Recipe' },
|
|
212
|
+
{
|
|
213
|
+
filters: { status: 'Active' },
|
|
214
|
+
orderBy: 'name',
|
|
215
|
+
limit: 20,
|
|
216
|
+
offset: 0,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
154
221
|
## Validation
|
|
155
222
|
|
|
156
223
|
Runtime validation with detailed error reporting:
|
|
@@ -237,18 +304,18 @@ then `--exclude` removes any remaining unwanted names.
|
|
|
237
304
|
|
|
238
305
|
### All options
|
|
239
306
|
|
|
240
|
-
| Flag
|
|
241
|
-
|
|
242
|
-
| `--endpoint <url>`
|
|
243
|
-
| `--introspection <file>`
|
|
244
|
-
| `--sdl <file>`
|
|
245
|
-
| `--output <dir>`
|
|
246
|
-
| `--include <types>`
|
|
247
|
-
| `--exclude <types>`
|
|
248
|
-
| `--overrides <file>`
|
|
249
|
-
| `--custom-scalars <file>` |
|
|
250
|
-
| `--include-unmapped`
|
|
251
|
-
| `--help`
|
|
307
|
+
| Flag | Short | Description |
|
|
308
|
+
| ------------------------- | ----- | -------------------------------------------------------- |
|
|
309
|
+
| `--endpoint <url>` | `-e` | Fetch introspection from a live GraphQL endpoint |
|
|
310
|
+
| `--introspection <file>` | `-i` | Read from a saved introspection JSON file |
|
|
311
|
+
| `--sdl <file>` | `-s` | Read from a GraphQL SDL (`.graphql`) file |
|
|
312
|
+
| `--output <dir>` | `-o` | Directory to write doctype JSON files (required) |
|
|
313
|
+
| `--include <types>` | | Comma-separated allowlist of type names to generate |
|
|
314
|
+
| `--exclude <types>` | | Comma-separated list of type names to skip |
|
|
315
|
+
| `--overrides <file>` | | JSON file with per-type, per-field overrides |
|
|
316
|
+
| `--custom-scalars <file>` | | JSON file mapping custom scalar names to field templates |
|
|
317
|
+
| `--include-unmapped` | | Retain `_graphqlType` metadata on fields with no mapping |
|
|
318
|
+
| `--help` | `-h` | Show help |
|
|
252
319
|
|
|
253
320
|
### Custom scalars
|
|
254
321
|
|
|
@@ -258,7 +325,7 @@ a JSON mapping file:
|
|
|
258
325
|
```json
|
|
259
326
|
{
|
|
260
327
|
"BigFloat": { "component": "ADecimalInput", "fieldtype": "Decimal" },
|
|
261
|
-
"Datetime":
|
|
328
|
+
"Datetime": { "component": "ADatetimeInput", "fieldtype": "Datetime" }
|
|
262
329
|
}
|
|
263
330
|
```
|
|
264
331
|
|
|
@@ -332,14 +399,7 @@ doctypes.forEach(doctype => {
|
|
|
332
399
|
Convert between different naming conventions:
|
|
333
400
|
|
|
334
401
|
```typescript
|
|
335
|
-
import {
|
|
336
|
-
snakeToCamel,
|
|
337
|
-
camelToSnake,
|
|
338
|
-
snakeToLabel,
|
|
339
|
-
camelToLabel,
|
|
340
|
-
toPascalCase,
|
|
341
|
-
toSlug,
|
|
342
|
-
} from '@stonecrop/schema'
|
|
402
|
+
import { snakeToCamel, camelToSnake, snakeToLabel, camelToLabel, toPascalCase, toSlug } from '@stonecrop/schema'
|
|
343
403
|
|
|
344
404
|
snakeToCamel('customer_name') // 'customerName'
|
|
345
405
|
camelToSnake('customerName') // 'customer_name'
|
|
@@ -367,7 +427,8 @@ console.log(TYPE_MAP['Link']) // { component: 'ALink', fieldtype: 'Link' }
|
|
|
367
427
|
|
|
368
428
|
This package provides the type system used throughout Stonecrop:
|
|
369
429
|
|
|
370
|
-
- **`@stonecrop/stonecrop`** - Registry uses `DoctypeMeta` for schema storage
|
|
430
|
+
- **`@stonecrop/stonecrop`** - Registry uses `DoctypeMeta` for schema storage; `getDescendantLinks()` / `getAncestorLinks()` for relationship traversal
|
|
431
|
+
- **`@stonecrop/graphql-client`** - `StonecropClient` implements `DataClient`; uses `GetRecordOptions` / `GetRecordsOptions` for fetch parameters
|
|
371
432
|
- **`@stonecrop/aform`** - Renders fields based on `FieldMeta` definitions
|
|
372
433
|
- **`@stonecrop/atable`** - Uses `FieldMeta` for column configuration
|
|
373
434
|
- **Backend APIs** - Validates and stores doctypes using these schemas
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";const r=require("node:fs"),c=require("node:path"),w=require("node:util"),x=require("graphql"),v=require("./index-aeXXzPET.cjs");async function O(e,h){const t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",...h},body:JSON.stringify({query:x.getIntrospectionQuery()})});if(!t.ok)throw new Error(`Failed to fetch introspection: ${t.status} ${t.statusText}`);const a=await t.json();if(a.errors?.length)throw new Error(`GraphQL errors: ${a.errors.map(n=>n.message).join(", ")}`);if(!a.data)throw new Error("No data in introspection response");return a.data}async function $(){const{values:e,positionals:h}=w.parseArgs({allowPositionals:!0,options:{endpoint:{type:"string",short:"e"},introspection:{type:"string",short:"i"},sdl:{type:"string",short:"s"},output:{type:"string",short:"o"},include:{type:"string"},exclude:{type:"string"},overrides:{type:"string"},"custom-scalars":{type:"string"},"include-unmapped":{type:"boolean",default:!1},help:{type:"boolean",short:"h"}}}),t=h[0];(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));const n=c.resolve(e.output),l={includeUnmappedMeta:e["include-unmapped"]};if(e.include&&(l.include=e.include.split(",").map(o=>o.trim())),e.exclude&&(l.exclude=e.exclude.split(",").map(o=>o.trim())),e.overrides){const o=c.resolve(e.overrides),s=r.readFileSync(o,"utf-8");l.typeOverrides=JSON.parse(s)}if(e["custom-scalars"]){const o=c.resolve(e["custom-scalars"]),s=r.readFileSync(o,"utf-8");l.customScalars=JSON.parse(s)}let p;if(e.endpoint)console.log(`Fetching introspection from ${e.endpoint}...`),p=await O(e.endpoint);else if(e.introspection){const o=c.resolve(e.introspection),s=r.readFileSync(o,"utf-8"),u=JSON.parse(s);p=u.data??u}else{const o=c.resolve(e.sdl);p=r.readFileSync(o,"utf-8")}const m=v.convertGraphQLSchema(p,l);m.length===0&&(console.warn("No entity types found in the schema. Check your include/exclude filters."),process.exit(0)),r.existsSync(n)||r.mkdirSync(n,{recursive:!0});let f=0,d=0;for(const o of m){const s=`${o.slug}.json`,u=c.join(n,s),S=JSON.stringify(o,null," ");r.writeFileSync(u,S+`
|
|
3
|
+
`,"utf-8");const g=v.validateDoctype(o);if(g.success){const i=o.fields.filter(y=>y._unmapped);i.length>0&&(f++,console.warn(` WARN: ${s} has ${i.length} unmapped field(s): ${i.map(y=>y.fieldname).join(", ")}`))}else{d++,console.error(` ERROR: ${s} failed validation:`);for(const i of g.errors)console.error(` ${i.path.join(".")}: ${i.message}`)}}console.log(`
|
|
4
|
+
Generated ${m.length} doctype(s) in ${n}`+(f?` (${f} with warnings)`:"")+(d?` (${d} with errors)`:"")),d>0&&process.exit(1)}function q(){console.log(`
|
|
5
|
+
stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
|
|
6
|
+
|
|
7
|
+
USAGE:
|
|
8
|
+
stonecrop-schema generate [options]
|
|
9
|
+
|
|
10
|
+
SOURCE (exactly one required):
|
|
11
|
+
--endpoint, -e <url> Fetch introspection from a live GraphQL endpoint
|
|
12
|
+
--introspection, -i <file> Read from a saved introspection JSON file
|
|
13
|
+
--sdl, -s <file> Read from a GraphQL SDL (.graphql) file
|
|
14
|
+
|
|
15
|
+
OUTPUT:
|
|
16
|
+
--output, -o <dir> Directory to write doctype JSON files (required)
|
|
17
|
+
|
|
18
|
+
OPTIONS:
|
|
19
|
+
--include <types> Comma-separated list of type names to include
|
|
20
|
+
--exclude <types> Comma-separated list of type names to exclude
|
|
21
|
+
--overrides <file> JSON file with per-type field overrides
|
|
22
|
+
--custom-scalars <file> JSON file mapping custom scalar names to field templates
|
|
23
|
+
--include-unmapped Include _graphqlType metadata on unmapped fields
|
|
24
|
+
--help, -h Show this help message
|
|
25
|
+
|
|
26
|
+
EXAMPLES:
|
|
27
|
+
# From a live PostGraphile server
|
|
28
|
+
stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas
|
|
29
|
+
|
|
30
|
+
# From a saved introspection result
|
|
31
|
+
stonecrop-schema generate -i introspection.json -o ./schemas
|
|
32
|
+
|
|
33
|
+
# From an SDL file with custom scalars
|
|
34
|
+
stonecrop-schema generate -s schema.graphql -o ./schemas \\
|
|
35
|
+
--custom-scalars custom-scalars.json
|
|
36
|
+
|
|
37
|
+
# Only convert specific types
|
|
38
|
+
stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\
|
|
39
|
+
--include "User,Post,Comment"
|
|
40
|
+
`)}$().catch(e=>{console.error("Error:",e.message),process.exit(1)});
|
|
41
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.cjs","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\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 = (await response.json()) as {\n\t\tdata?: IntrospectionQuery\n\t\terrors?: Array<{ message: string }>\n\t}\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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"}
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync as u, existsSync as S, mkdirSync as w, writeFileSync as x
|
|
|
3
3
|
import { resolve as c, join as O } from "node:path";
|
|
4
4
|
import { parseArgs as $ } from "node:util";
|
|
5
5
|
import { getIntrospectionQuery as N } from "graphql";
|
|
6
|
-
import {
|
|
6
|
+
import { e as P, v as j } from "./index-CLc5mUMQ.js";
|
|
7
7
|
async function C(e, m) {
|
|
8
8
|
const t = await fetch(e, {
|
|
9
9
|
method: "POST",
|
|
@@ -223,21 +223,23 @@ export function classifyFieldType(fieldName, field, entityTypes, options = {}) {
|
|
|
223
223
|
base.options = toSlug(namedType.name);
|
|
224
224
|
return base;
|
|
225
225
|
}
|
|
226
|
-
// 4. Connection type →
|
|
226
|
+
// 4. Connection type → link (child table)
|
|
227
227
|
const connectionNodeTypeName = getConnectionNodeType(namedType);
|
|
228
228
|
if (connectionNodeTypeName && entityTypes.has(connectionNodeTypeName)) {
|
|
229
229
|
base.component = 'ATable';
|
|
230
|
-
base.
|
|
230
|
+
base._isLink = true;
|
|
231
231
|
base.options = toSlug(connectionNodeTypeName);
|
|
232
|
-
base.cardinality = '
|
|
232
|
+
base.cardinality = 'noneOrMany';
|
|
233
|
+
delete base.fieldtype;
|
|
233
234
|
return base;
|
|
234
235
|
}
|
|
235
|
-
// 5. List of entity type →
|
|
236
|
+
// 5. List of entity type → link
|
|
236
237
|
if (isList && entityTypes.has(namedType.name)) {
|
|
237
238
|
base.component = 'ATable';
|
|
238
|
-
base.
|
|
239
|
+
base._isLink = true;
|
|
239
240
|
base.options = toSlug(namedType.name);
|
|
240
|
-
base.cardinality = '
|
|
241
|
+
base.cardinality = 'noneOrMany';
|
|
242
|
+
delete base.fieldtype;
|
|
241
243
|
return base;
|
|
242
244
|
}
|
|
243
245
|
// Unknown object type — mark as unmapped
|
package/dist/converter/index.js
CHANGED
|
@@ -88,7 +88,7 @@ export function convertGraphQLSchema(source, options = {}) {
|
|
|
88
88
|
continue;
|
|
89
89
|
const fields = type.getFields();
|
|
90
90
|
const typeOverrides = options.typeOverrides?.[typeName];
|
|
91
|
-
const
|
|
91
|
+
const allClassifiedFields = Object.entries(fields)
|
|
92
92
|
.filter(([fieldName, field]) => isEntityField(fieldName, field, type))
|
|
93
93
|
.map(([fieldName, field]) => {
|
|
94
94
|
// Check for full custom classification first
|
|
@@ -111,20 +111,37 @@ export function convertGraphQLSchema(source, options = {}) {
|
|
|
111
111
|
return { ...classified, ...typeOverrides[fieldName] };
|
|
112
112
|
}
|
|
113
113
|
return classified;
|
|
114
|
+
});
|
|
115
|
+
// Separate scalar fields from link fields
|
|
116
|
+
const links = {};
|
|
117
|
+
const convertedFields = allClassifiedFields
|
|
118
|
+
.filter(field => {
|
|
119
|
+
if (field._isLink && typeof field.options === 'string' && field.cardinality) {
|
|
120
|
+
links[field.fieldname] = {
|
|
121
|
+
target: field.options,
|
|
122
|
+
cardinality: field.cardinality,
|
|
123
|
+
};
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
114
127
|
})
|
|
115
128
|
// Clean up internal metadata unless requested
|
|
116
129
|
.map(field => {
|
|
117
130
|
if (!options.includeUnmappedMeta) {
|
|
118
|
-
const { _graphqlType, _unmapped, ...clean } = field;
|
|
131
|
+
const { _graphqlType, _unmapped, _isLink, ...clean } = field;
|
|
119
132
|
return clean;
|
|
120
133
|
}
|
|
121
|
-
|
|
134
|
+
const { _isLink, ...rest } = field;
|
|
135
|
+
return rest;
|
|
122
136
|
});
|
|
123
137
|
const doctype = {
|
|
124
138
|
name: typeName,
|
|
125
139
|
slug: toSlug(typeName),
|
|
126
140
|
fields: convertedFields,
|
|
127
141
|
};
|
|
142
|
+
if (Object.keys(links).length > 0) {
|
|
143
|
+
doctype.links = links;
|
|
144
|
+
}
|
|
128
145
|
const tableName = deriveTableName(typeName);
|
|
129
146
|
if (tableName) {
|
|
130
147
|
doctype.tableName = tableName;
|