@stonecrop/schema 0.10.16 → 0.11.1
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.js +1 -1
- package/dist/converter/heuristics.js +8 -6
- package/dist/converter/index.js +20 -3
- package/dist/doctype.js +90 -5
- package/dist/field.js +5 -3
- package/dist/fieldtype.js +0 -2
- package/dist/{index-D6Up-BP5.js → index-CLc5mUMQ.js} +201 -158
- package/dist/index-CLc5mUMQ.js.map +1 -0
- package/dist/index.js +23 -29
- package/dist/schema.d.ts +206 -35
- package/dist/src/converter/heuristics.d.ts.map +1 -1
- package/dist/src/converter/index.d.ts.map +1 -1
- package/dist/src/converter/types.d.ts +2 -0
- package/dist/src/converter/types.d.ts.map +1 -1
- package/dist/src/doctype.d.ts +176 -12
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/field.d.ts +3 -2
- package/dist/src/field.d.ts.map +1 -1
- package/dist/src/fieldtype.d.ts +0 -1
- package/dist/src/fieldtype.d.ts.map +1 -1
- package/dist/src/index.d.ts +5 -9
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/index-D6Up-BP5.js.map +0 -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.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;
|
package/dist/doctype.js
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { FieldMeta } from './field';
|
|
3
|
+
/**
|
|
4
|
+
* Cardinality for relationship links.
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export const Cardinality = z.enum(['atMostOne', 'one', 'noneOrMany', 'atLeastOne']).meta({
|
|
8
|
+
title: 'Cardinality',
|
|
9
|
+
description: 'Cardinality for relationship links between doctypes',
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Sync fetch strategy - data is fetched in the initial query.
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export const SyncFetch = z
|
|
16
|
+
.object({
|
|
17
|
+
/** Fetch method type */
|
|
18
|
+
method: z.literal('sync'),
|
|
19
|
+
/** Optional limit on number of records to fetch */
|
|
20
|
+
limit: z.number().int().positive().optional(),
|
|
21
|
+
})
|
|
22
|
+
.meta({
|
|
23
|
+
title: 'SyncFetch',
|
|
24
|
+
description: 'Sync fetch strategy - data is fetched in the initial query',
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Lazy fetch strategy - data is fetched on demand in a separate query.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export const LazyFetch = z
|
|
31
|
+
.object({
|
|
32
|
+
/** Fetch method type */
|
|
33
|
+
method: z.literal('lazy'),
|
|
34
|
+
})
|
|
35
|
+
.meta({
|
|
36
|
+
title: 'LazyFetch',
|
|
37
|
+
description: 'Lazy fetch strategy - data is fetched on demand in a separate query',
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Custom fetch strategy - uses a custom handler function.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export const CustomFetch = z
|
|
44
|
+
.object({
|
|
45
|
+
/** Fetch method type */
|
|
46
|
+
method: z.literal('custom'),
|
|
47
|
+
/** Serialized handler function to invoke */
|
|
48
|
+
handler: z.string(),
|
|
49
|
+
})
|
|
50
|
+
.meta({
|
|
51
|
+
title: 'CustomFetch',
|
|
52
|
+
description: 'Custom fetch strategy - uses a custom handler function',
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Fetch strategy for link data loading.
|
|
56
|
+
* - sync: fetched in the initial query
|
|
57
|
+
* - lazy: fetched on demand in a separate query
|
|
58
|
+
* - custom: uses a custom handler function
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export const FetchStrategy = z.discriminatedUnion('method', [SyncFetch, LazyFetch, CustomFetch]).meta({
|
|
62
|
+
title: 'FetchStrategy',
|
|
63
|
+
description: 'Fetch strategy for link data loading',
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Link declaration - describes a relationship from one doctype to another.
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
export const LinkDeclaration = z
|
|
70
|
+
.object({
|
|
71
|
+
/** Target doctype slug */
|
|
72
|
+
target: z.string().min(1),
|
|
73
|
+
/** Cardinality of the relationship */
|
|
74
|
+
cardinality: Cardinality,
|
|
75
|
+
/** Backlink fieldname on the target doctype that points back to this link */
|
|
76
|
+
backlink: z.string().optional(),
|
|
77
|
+
/** Override default rendering component (AForm for 1:1, ATable for 1:many) */
|
|
78
|
+
component: z.string().optional(),
|
|
79
|
+
/** Fieldname of the corresponding Link field in the fields array */
|
|
80
|
+
fieldname: z.string().min(1).optional(),
|
|
81
|
+
/** Fetch strategy for loading nested data */
|
|
82
|
+
fetch: FetchStrategy.optional(),
|
|
83
|
+
/** Whether to block workflow actions until nested data is loaded (default: true) */
|
|
84
|
+
blockWorkflows: z.boolean().optional(),
|
|
85
|
+
})
|
|
86
|
+
.meta({
|
|
87
|
+
title: 'LinkDeclaration',
|
|
88
|
+
description: 'Declares a relationship from one doctype to another',
|
|
89
|
+
});
|
|
3
90
|
/**
|
|
4
91
|
* Action definition within a workflow
|
|
5
92
|
* @public
|
|
@@ -50,16 +137,14 @@ export const DoctypeMeta = z
|
|
|
50
137
|
slug: z.string().min(1).optional(),
|
|
51
138
|
/** Database table name */
|
|
52
139
|
tableName: z.string().optional(),
|
|
53
|
-
/** Field definitions */
|
|
140
|
+
/** Field definitions (including link fields with fieldtype: 'Link') */
|
|
54
141
|
fields: z.array(FieldMeta),
|
|
142
|
+
/** Relationship links to other doctypes */
|
|
143
|
+
links: z.record(z.string(), LinkDeclaration).optional(),
|
|
55
144
|
/** Workflow configuration */
|
|
56
145
|
workflow: WorkflowMeta.optional(),
|
|
57
146
|
/** Parent doctype for inheritance */
|
|
58
147
|
inherits: z.string().optional(),
|
|
59
|
-
/** Doctype to use for list views */
|
|
60
|
-
listDoctype: z.string().optional(),
|
|
61
|
-
/** Parent doctype for child tables */
|
|
62
|
-
parentDoctype: z.string().optional(),
|
|
63
148
|
})
|
|
64
149
|
.meta({
|
|
65
150
|
title: 'DoctypeMeta',
|
package/dist/field.js
CHANGED
|
@@ -85,10 +85,12 @@ export const FieldMeta = z
|
|
|
85
85
|
options: FieldOptions.optional(),
|
|
86
86
|
/**
|
|
87
87
|
* Cardinality for Doctype fields:
|
|
88
|
-
* - 'one': 1
|
|
89
|
-
* - '
|
|
88
|
+
* - 'one': exactly 1 (default)
|
|
89
|
+
* - 'atMostOne': 0 or 1
|
|
90
|
+
* - 'noneOrMany': 0 or more
|
|
91
|
+
* - 'atLeastOne': 1 or more
|
|
90
92
|
*/
|
|
91
|
-
cardinality: z.enum(['one', '
|
|
93
|
+
cardinality: z.enum(['one', 'atMostOne', 'noneOrMany', 'atLeastOne']).optional(),
|
|
92
94
|
/**
|
|
93
95
|
* Input mask pattern. Accepts either a plain mask string or a stringified
|
|
94
96
|
* arrow function that receives `locale` and returns a mask string.
|
package/dist/fieldtype.js
CHANGED
|
@@ -20,7 +20,6 @@ export const StonecropFieldType = z
|
|
|
20
20
|
'JSON', // JSON data
|
|
21
21
|
'Code', // Code/source (with syntax highlighting)
|
|
22
22
|
'Link', // Reference to another doctype
|
|
23
|
-
'Doctype', // Child doctype (1:1 nested form or 1:many table via cardinality)
|
|
24
23
|
'Attach', // File attachment
|
|
25
24
|
'Currency', // Currency value
|
|
26
25
|
'Quantity', // Quantity with unit
|
|
@@ -56,7 +55,6 @@ export const TYPE_MAP = {
|
|
|
56
55
|
Code: { component: 'ACodeEditor', fieldtype: 'Code' },
|
|
57
56
|
// Relational
|
|
58
57
|
Link: { component: 'ALink', fieldtype: 'Link' },
|
|
59
|
-
Doctype: { component: 'AForm', fieldtype: 'Doctype' },
|
|
60
58
|
// Files
|
|
61
59
|
Attach: { component: 'AFileAttach', fieldtype: 'Attach' },
|
|
62
60
|
// Specialized
|