@stonecrop/schema 0.31.0 → 0.32.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.
Files changed (82) hide show
  1. package/README.md +10 -10
  2. package/dist/cli.js +194 -121
  3. package/dist/cli.js.map +1 -1
  4. package/dist/converter-CLwduvT_.js +2102 -0
  5. package/dist/converter-CLwduvT_.js.map +1 -0
  6. package/dist/flatten-Bx2cfvw3.js +37 -0
  7. package/dist/flatten-Bx2cfvw3.js.map +1 -0
  8. package/dist/index.js +123 -117
  9. package/dist/index.js.map +1 -1
  10. package/dist/record-BQOOi83C.js +134 -0
  11. package/dist/record-BQOOi83C.js.map +1 -0
  12. package/dist/record.js +2 -6
  13. package/dist/tsdoc-metadata.json +1 -1
  14. package/package.json +34 -18
  15. package/dist/flatten-C1MjkzFh.js +0 -10
  16. package/dist/flatten-C1MjkzFh.js.map +0 -1
  17. package/dist/record-Bc0lI9Rq.js +0 -61
  18. package/dist/record-Bc0lI9Rq.js.map +0 -1
  19. package/dist/record.js.map +0 -1
  20. package/dist/schema.tsbuildinfo +0 -1
  21. package/dist/src/badge.d.ts +0 -74
  22. package/dist/src/badge.d.ts.map +0 -1
  23. package/dist/src/badge.js +0 -158
  24. package/dist/src/cli.d.ts +0 -3
  25. package/dist/src/cli.d.ts.map +0 -1
  26. package/dist/src/cli.js +0 -292
  27. package/dist/src/column-schema.d.ts +0 -163
  28. package/dist/src/column-schema.d.ts.map +0 -1
  29. package/dist/src/column-schema.js +0 -0
  30. package/dist/src/component-meta.d.ts +0 -96
  31. package/dist/src/component-meta.d.ts.map +0 -1
  32. package/dist/src/component-meta.js +0 -88
  33. package/dist/src/converter/aggregate.d.ts +0 -127
  34. package/dist/src/converter/aggregate.d.ts.map +0 -1
  35. package/dist/src/converter/aggregate.js +0 -235
  36. package/dist/src/converter/authored.d.ts +0 -43
  37. package/dist/src/converter/authored.d.ts.map +0 -1
  38. package/dist/src/converter/authored.js +0 -52
  39. package/dist/src/converter/heuristics.d.ts +0 -60
  40. package/dist/src/converter/heuristics.d.ts.map +0 -1
  41. package/dist/src/converter/heuristics.js +0 -304
  42. package/dist/src/converter/index.d.ts +0 -51
  43. package/dist/src/converter/index.d.ts.map +0 -1
  44. package/dist/src/converter/index.js +0 -195
  45. package/dist/src/converter/merge.d.ts +0 -102
  46. package/dist/src/converter/merge.d.ts.map +0 -1
  47. package/dist/src/converter/merge.js +0 -136
  48. package/dist/src/converter/scalars.d.ts +0 -46
  49. package/dist/src/converter/scalars.d.ts.map +0 -1
  50. package/dist/src/converter/scalars.js +0 -83
  51. package/dist/src/converter/types.d.ts +0 -157
  52. package/dist/src/converter/types.d.ts.map +0 -1
  53. package/dist/src/converter/types.js +0 -5
  54. package/dist/src/doctype.d.ts +0 -516
  55. package/dist/src/doctype.d.ts.map +0 -1
  56. package/dist/src/doctype.js +0 -343
  57. package/dist/src/field.d.ts +0 -423
  58. package/dist/src/field.d.ts.map +0 -1
  59. package/dist/src/field.js +0 -378
  60. package/dist/src/flatten.d.ts +0 -29
  61. package/dist/src/flatten.d.ts.map +0 -1
  62. package/dist/src/flatten.js +0 -38
  63. package/dist/src/index.d.ts +0 -16
  64. package/dist/src/index.d.ts.map +0 -1
  65. package/dist/src/index.js +0 -20
  66. package/dist/src/mode.d.ts +0 -15
  67. package/dist/src/mode.d.ts.map +0 -1
  68. package/dist/src/mode.js +0 -0
  69. package/dist/src/naming.d.ts +0 -80
  70. package/dist/src/naming.d.ts.map +0 -1
  71. package/dist/src/naming.js +0 -106
  72. package/dist/src/record.d.ts +0 -29
  73. package/dist/src/record.d.ts.map +0 -1
  74. package/dist/src/record.js +0 -55
  75. package/dist/src/table.d.ts +0 -33
  76. package/dist/src/table.d.ts.map +0 -1
  77. package/dist/src/table.js +0 -25
  78. package/dist/src/validation.d.ts +0 -54
  79. package/dist/src/validation.d.ts.map +0 -1
  80. package/dist/src/validation.js +0 -60
  81. package/dist/validation-C9P__pRF.js +0 -994
  82. package/dist/validation-C9P__pRF.js.map +0 -1
@@ -1,343 +0,0 @@
1
- import { z } from 'zod';
2
- import { DoctypeFieldSchema, flattenFields, getDisplayField } from './field';
3
- import { toSlug } from './naming';
4
- /**
5
- * Cardinality for relationship links.
6
- * @public
7
- */
8
- export const Cardinality = z.enum(['atMostOne', 'one', 'noneOrMany', 'atLeastOne']).meta({
9
- title: 'Cardinality',
10
- description: 'Cardinality for relationship links between doctypes',
11
- });
12
- /**
13
- * Sync fetch strategy - data is fetched in the initial query.
14
- * @public
15
- */
16
- export const SyncFetch = z
17
- .object({
18
- /** Fetch method type */
19
- method: z.literal('sync'),
20
- /** Optional limit on number of records to fetch */
21
- limit: z.number().int().positive().optional(),
22
- })
23
- .meta({
24
- title: 'SyncFetch',
25
- description: 'Sync fetch strategy - data is fetched in the initial query',
26
- });
27
- /**
28
- * Lazy fetch strategy - data is fetched on demand in a separate query.
29
- * @public
30
- */
31
- export const LazyFetch = z
32
- .object({
33
- /** Fetch method type */
34
- method: z.literal('lazy'),
35
- })
36
- .meta({
37
- title: 'LazyFetch',
38
- description: 'Lazy fetch strategy - data is fetched on demand in a separate query',
39
- });
40
- /**
41
- * Custom fetch strategy - uses a custom handler function.
42
- * @public
43
- */
44
- export const CustomFetch = z
45
- .object({
46
- /** Fetch method type */
47
- method: z.literal('custom'),
48
- /** Serialized handler function to invoke */
49
- handler: z.string(),
50
- })
51
- .meta({
52
- title: 'CustomFetch',
53
- description: 'Custom fetch strategy - uses a custom handler function',
54
- });
55
- /**
56
- * Fetch strategy for link data loading.
57
- * - sync: fetched in the initial query
58
- * - lazy: fetched on demand in a separate query
59
- * - custom: uses a custom handler function
60
- * @public
61
- */
62
- export const FetchStrategy = z.discriminatedUnion('method', [SyncFetch, LazyFetch, CustomFetch]).meta({
63
- title: 'FetchStrategy',
64
- description: 'Fetch strategy for link data loading',
65
- });
66
- /**
67
- * Link declaration - describes a relationship from one doctype to another.
68
- * @public
69
- */
70
- export const LinkDeclaration = z
71
- .object({
72
- /** Target doctype slug */
73
- target: z.string().min(1),
74
- /** Cardinality of the relationship */
75
- cardinality: Cardinality,
76
- /** Backlink fieldname on the target doctype that points back to this link */
77
- backlink: z.string().optional(),
78
- /** Override default rendering component (AForm for 1:1, ATable for 1:many) */
79
- component: z.string().optional(),
80
- /** Fieldname of the corresponding Link field in the fields array */
81
- fieldname: z.string().min(1).optional(),
82
- /** Fetch strategy for loading nested data */
83
- fetch: FetchStrategy.optional(),
84
- /** Whether to block workflow actions until nested data is loaded (default: true) */
85
- blockWorkflows: z.boolean().optional(),
86
- })
87
- .meta({
88
- title: 'LinkDeclaration',
89
- description: 'Declares a relationship from one doctype to another',
90
- });
91
- /**
92
- * Action definition within a workflow
93
- * @public
94
- */
95
- export const ActionDefinition = z
96
- .object({
97
- /** Display label for the action */
98
- label: z.string().min(1),
99
- /** Fields that must have values before action can execute */
100
- requiredFields: z.array(z.string()).optional(),
101
- /** Workflow states where this action is available */
102
- allowedStates: z.array(z.string()).optional(),
103
- /** The state the record transitions to after this action executes */
104
- nextState: z.string().optional(),
105
- /** True for stateless command actions with no workflow effect at all (print, email, etc.) */
106
- stateless: z.boolean().optional(),
107
- /**
108
- * True for an internal self-transition: the action runs within the current state without
109
- * advancing the workflow (e.g. `save`, which mutates record data but stays put). Scoped by
110
- * `allowedStates`, rendered as a self-loop in the graph, and has no `nextState`. Distinct from
111
- * `stateless` (which has no workflow presence at all): a self-transition is graph-owned and,
112
- * unlike a stateless command, persists record data on dispatch.
113
- */
114
- selfTransition: z.boolean().optional(),
115
- /** JS function body stored as a string; executed client-side via AsyncFunction with injected API surface */
116
- clientHandler: z.string().optional(),
117
- })
118
- .meta({
119
- title: 'ActionDefinition',
120
- description: 'Action definition within a workflow',
121
- });
122
- /**
123
- * Reactive field-validation trigger — advisory, client-side only.
124
- *
125
- * A Trigger is a docbuilder-authored validator: when any field in `on` is edited, its
126
- * `clientHandler` runs (client-side, no rollback) and may flag a field inline to block save
127
- * in the UI. It is deliberately a **sibling** to {@link (ActionDefinition:type)}, not a member of it —
128
- * a reactive validator is not a user-invoked action, so it lives in the `triggers` map on
129
- * {@link (WorkflowMeta:type)} and never appears to action readers (transition/command dropdowns, the FSM graph).
130
- *
131
- * The two bindings are independent: `on` is the fire-set (which fields' edits run it), while the
132
- * `setError(field, msg)` call inside `clientHandler` chooses which field displays the error.
133
- * @public
134
- */
135
- export const TriggerDefinition = z
136
- .object({
137
- /** Optional display label; the map key is the trigger's identity */
138
- label: z.string().optional(),
139
- /** Fieldnames whose edits fire this trigger (fires when any listed field changes) */
140
- on: z.array(z.string()),
141
- /** JS function body stored as a string; run client-side with `{ record, value, setError }`. Advisory. */
142
- clientHandler: z.string(),
143
- })
144
- .meta({
145
- title: 'TriggerDefinition',
146
- description: 'Reactive field-validation trigger — advisory client-side',
147
- });
148
- /**
149
- * Whether a workflow action may run from `currentState`.
150
- *
151
- * Single source of truth for the "is this action available here" rule, shared by
152
- * the frontend (`getAvailableTransitions`) and the server-side dispatch guard so
153
- * the two can never disagree. Empty or absent `allowedStates` means the action is
154
- * available in ALL states — a plain `allowedStates.includes(currentState)` would
155
- * wrongly block such actions everywhere.
156
- *
157
- * @public
158
- */
159
- export function isActionAllowedInState(action, currentState) {
160
- const allowedStates = action.allowedStates;
161
- if (!allowedStates || allowedStates.length === 0)
162
- return true;
163
- return allowedStates.includes(currentState);
164
- }
165
- /**
166
- * DocBuilder graph layout — node positions for the workflow-state graph, keyed by state name.
167
- * Pure authoring view-state: persisted in the doctype JSON so an author's manual arrangement
168
- * survives reloads, but — exactly like {@link (WorkflowMeta:type)}'s `triggers` — it is client-only
169
- * and never mirrored into the runtime GraphQL SDL (see the WorkflowMeta type in the host SDLs, which
170
- * expose only `states`/`actions`). The shape mirrors VueFlow's node fields; `position` is the node's
171
- * canvas coordinate and `targetPosition`/`sourcePosition` are the handle sides.
172
- * @public
173
- */
174
- export const WorkflowLayout = z.record(z.string(), z.object({
175
- position: z.object({ x: z.number(), y: z.number() }).optional(),
176
- targetPosition: z.enum(['left', 'top', 'right', 'bottom']).optional(),
177
- sourcePosition: z.enum(['left', 'top', 'right', 'bottom']).optional(),
178
- }));
179
- /**
180
- * Workflow metadata - states and actions for a doctype
181
- * @public
182
- */
183
- export const WorkflowMeta = z
184
- .object({
185
- /** List of workflow states */
186
- states: z.array(z.string()).optional(),
187
- /** Actions available in this workflow */
188
- actions: z.record(z.string(), ActionDefinition).optional(),
189
- /** Reactive field-validation triggers (advisory, client-side), keyed by trigger name */
190
- triggers: z.record(z.string(), TriggerDefinition).optional(),
191
- /**
192
- * DocBuilder node positions keyed by state name — authoring view-state. Persisted here so a
193
- * doctype author's manual graph arrangement survives reloads; like `triggers`, it is client-only
194
- * and never enters the runtime GraphQL SDL. See {@link (WorkflowLayout:variable)}.
195
- */
196
- layout: WorkflowLayout.optional(),
197
- })
198
- .meta({
199
- title: 'WorkflowMeta',
200
- description: 'Workflow metadata - states and actions for a doctype',
201
- });
202
- /**
203
- * Doctype metadata - complete definition of a doctype
204
- * @public
205
- */
206
- export const DoctypeMeta = z
207
- .object({
208
- /** Display name of the doctype */
209
- name: z.string().min(1),
210
- /** URL-friendly slug (kebab-case) */
211
- slug: z.string().min(1).optional(),
212
- /**
213
- * Field on this doctype used when displaying a reference to one of its records.
214
- * When a record elsewhere holds an inline foreign key to this doctype, the middleware
215
- * returns that field as `{ id, displayText }`, reading `displayText` from this field.
216
- */
217
- displayField: z.string().min(1).optional(),
218
- /**
219
- * URL path this doctype registers at, written literally — `/order` for a collection,
220
- * `/order/:id` for a record. Absent means the doctype has no page of its own, which is the
221
- * common case: a child table is reached inside its parent, never at a URL.
222
- *
223
- * A path rather than a segment because the record parameter has to be somewhere, and a host
224
- * that reads a bare segment has to know which kind of doctype it is holding to decide where
225
- * to put it. Writing it out means nothing downstream re-derives it.
226
- */
227
- route: z.string().startsWith('/').optional(),
228
- /** Field definitions (a link field is one carrying `doctype`) */
229
- fields: z.array(DoctypeFieldSchema),
230
- /** Relationship links to other doctypes */
231
- links: z.record(z.string(), LinkDeclaration).optional(),
232
- /** Workflow configuration */
233
- workflow: WorkflowMeta.optional(),
234
- /** Parent doctype for inheritance */
235
- inherits: z.string().optional(),
236
- })
237
- .meta({
238
- title: 'DoctypeMeta',
239
- description: 'Doctype metadata - complete definition of a doctype',
240
- })
241
- .superRefine((doctype, ctx) => {
242
- // A record is identified by exactly one field here, and that is the design rather than a
243
- // limitation awaiting composite support. A doctype describes the **API surface** a client
244
- // interacts with, not the table behind it; how a composite database key maps onto a single
245
- // identity on that surface is the server's business, and the client neither sees nor
246
- // encodes the parts. So there is nothing for a doctype-level composite key to express.
247
- //
248
- // Declaring several is therefore malformed, and silently so: `getPrimaryKeyField` takes the
249
- // first match and the rest are ignored, leaving an adapter to key records on a column that
250
- // need not be unique — `stonecropRecord`'s row map then keeps whichever row comes last.
251
- // Refusing at the gate is what makes it say so.
252
- //
253
- // Counts the flattened set, because that is the set `getPrimaryKeyField` resolves over. The
254
- // two asked different questions while this scanned top level only: a doctype with one key
255
- // declared at each level passed the gate and then had one of them silently dropped.
256
- //
257
- // Zero keys stays legal and is not an omission: a surrogate-key doctype declares none and
258
- // resolves through `getRecordIdField`'s documented `id` fallback.
259
- const declared = flattenFields(doctype.fields).filter(f => f.kind === 'field' && f.primaryKey);
260
- if (declared.length > 1) {
261
- ctx.addIssue({
262
- code: 'custom',
263
- path: ['fields'],
264
- message: `Doctype declares ${declared.length} primaryKey fields (${declared
265
- .map(f => (f.kind === 'field' ? f.fieldname : ''))
266
- .join(', ')}); a record is identified by exactly one field. A composite database key is mapped to a single identity by the adapter, so a doctype never declares its parts`,
267
- });
268
- }
269
- // Through `getDisplayField` rather than a scan written here, because the adapter builds its
270
- // SELECT from that same call. The two hand-rolled versions disagreed in both directions at
271
- // once: this gate scanned top-level only, so it rejected a fieldset-nested field that would
272
- // have worked, while neither side excluded `computed` fields, so a nomination naming one
273
- // passed the gate and then failed as a missing column at query time.
274
- if (doctype.displayField && !getDisplayField(doctype.fields, doctype.displayField)) {
275
- const named = flattenFields(doctype.fields).find(f => f.fieldname === doctype.displayField);
276
- ctx.addIssue({
277
- code: 'custom',
278
- path: ['displayField'],
279
- message: named
280
- ? `displayField "${doctype.displayField}" names a computed field, which has no column to read a display value from`
281
- : `displayField "${doctype.displayField}" is not declared on this doctype`,
282
- });
283
- }
284
- });
285
- /**
286
- * The one string a doctype is addressed by.
287
- *
288
- * A doctype carries two names — `name` (`OrderItem`) and `slug` (`order-item`) — and every registry
289
- * must agree on which one keys it. Three implementations had drifted apart: the adapter's registry is
290
- * keyed by `name` and its `getMeta` also scans for a matching `slug`, so it accepts **either**; the
291
- * client's registry is keyed by a slug it derives itself and accepts **only** that; and
292
- * `Doctype.fromObject` dropped an authored `slug` on the floor and re-derived one regardless. The
293
- * adapter's accepted set was therefore a strict superset of the client's, and a link target written
294
- * as the Name booted the server, passed its reference check, served rows over GraphQL, and was
295
- * silently dropped by the client — an expanding child table rendering as one empty text input, with
296
- * nothing logged.
297
- *
298
- * Resolving through this in both runtimes is what makes the two answers the same answer. It is the
299
- * derivation only; a *lookup* still belongs to whichever registry owns the corpus, because the two
300
- * corpora legitimately differ (a client registers lazily, and a client-only host has no adapter at
301
- * all).
302
- *
303
- * An authored `slug` wins over the derived one because the authored doctype is the source of truth:
304
- * generation verifies a file and never overwrites it, so a doctype that states its own slug means it.
305
- * Deriving unconditionally is what `fromObject` did, and it made an authored `slug` a silent no-op on
306
- * one side of the wire while the other honoured it.
307
- *
308
- * @param doctype - anything carrying a doctype's `name` and optional authored `slug`
309
- * @returns the canonical slug
310
- * @public
311
- *
312
- * @example
313
- * ```typescript
314
- * getDoctypeSlug({ name: 'OrderItem' }) // 'order-item'
315
- * getDoctypeSlug({ name: 'Planner', slug: 'planner-board' }) // 'planner-board'
316
- * ```
317
- */
318
- export function getDoctypeSlug(doctype) {
319
- // `||` rather than `??`: an empty authored slug is not a usable registry key, and this is
320
- // reachable — `Doctype.fromObject` builds a doctype without going through the Zod gate, which is
321
- // where `slug: z.string().min(1)` would have refused it.
322
- return doctype.slug || toSlug(doctype.name);
323
- }
324
- /**
325
- * Suffix appended to a link fieldname for its pre-resolved display text in record payloads.
326
- *
327
- * @deprecated The `__display` suffix pattern is no longer used. Inline link fields are enriched
328
- * server-side by `@stonecrop/graphql-middleware` as `{ id, displayText }` objects on the link
329
- * field itself.
330
- * @public
331
- */
332
- export const LINK_DISPLAY_SUFFIX = '__display';
333
- /**
334
- * Build the payload key for a link field's display text (e.g. `customerId__display`).
335
- *
336
- * @deprecated The `__display` suffix pattern is no longer used. Inline link fields are enriched
337
- * server-side by `@stonecrop/graphql-middleware` as `{ id, displayText }` objects on the link
338
- * field itself.
339
- * @public
340
- */
341
- export function linkDisplayFieldname(fieldname) {
342
- return `${fieldname}${LINK_DISPLAY_SUFFIX}`;
343
- }