@stonecrop/schema 0.24.0 → 0.26.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.
@@ -249,6 +249,7 @@ export declare const DoctypeMeta: z.ZodObject<{
249
249
  name: z.ZodString;
250
250
  slug: z.ZodOptional<z.ZodString>;
251
251
  displayField: z.ZodOptional<z.ZodString>;
252
+ route: z.ZodOptional<z.ZodString>;
252
253
  fields: z.ZodArray<z.ZodType<import("./field").DoctypeField, unknown, z.core.$ZodTypeInternals<import("./field").DoctypeField, unknown>>>;
253
254
  links: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
254
255
  target: z.ZodString;
@@ -314,6 +315,43 @@ export declare const DoctypeMeta: z.ZodObject<{
314
315
  * @public
315
316
  */
316
317
  export type DoctypeMeta = z.infer<typeof DoctypeMeta>;
318
+ /**
319
+ * The one string a doctype is addressed by.
320
+ *
321
+ * A doctype carries two names — `name` (`OrderItem`) and `slug` (`order-item`) — and every registry
322
+ * must agree on which one keys it. Three implementations had drifted apart: the adapter's registry is
323
+ * keyed by `name` and its `getMeta` also scans for a matching `slug`, so it accepts **either**; the
324
+ * client's registry is keyed by a slug it derives itself and accepts **only** that; and
325
+ * `Doctype.fromObject` dropped an authored `slug` on the floor and re-derived one regardless. The
326
+ * adapter's accepted set was therefore a strict superset of the client's, and a link target written
327
+ * as the Name booted the server, passed its reference check, served rows over GraphQL, and was
328
+ * silently dropped by the client — an expanding child table rendering as one empty text input, with
329
+ * nothing logged.
330
+ *
331
+ * Resolving through this in both runtimes is what makes the two answers the same answer. It is the
332
+ * derivation only; a *lookup* still belongs to whichever registry owns the corpus, because the two
333
+ * corpora legitimately differ (a client registers lazily, and a client-only host has no adapter at
334
+ * all).
335
+ *
336
+ * An authored `slug` wins over the derived one because the authored doctype is the source of truth:
337
+ * generation verifies a file and never overwrites it, so a doctype that states its own slug means it.
338
+ * Deriving unconditionally is what `fromObject` did, and it made an authored `slug` a silent no-op on
339
+ * one side of the wire while the other honoured it.
340
+ *
341
+ * @param doctype - anything carrying a doctype's `name` and optional authored `slug`
342
+ * @returns the canonical slug
343
+ * @public
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * getDoctypeSlug({ name: 'OrderItem' }) // 'order-item'
348
+ * getDoctypeSlug({ name: 'Planner', slug: 'planner-board' }) // 'planner-board'
349
+ * ```
350
+ */
351
+ export declare function getDoctypeSlug(doctype: {
352
+ name: string;
353
+ slug?: string;
354
+ }): string;
317
355
  /**
318
356
  * Suffix appended to a link fieldname for its pre-resolved display text in record payloads.
319
357
  *
@@ -1 +1 @@
1
- {"version":3,"file":"doctype.d.ts","sourceRoot":"","sources":["../../src/doctype.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"doctype.d.ts","sourceRoot":"","sources":["../../src/doctype.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { DoctypeFieldSchema, flattenFields, getDisplayField } from './field';
3
+ import { toSlug } from './naming';
3
4
  /**
4
5
  * Cardinality for relationship links.
5
6
  * @public
@@ -214,6 +215,16 @@ export const DoctypeMeta = z
214
215
  * returns that field as `{ id, displayText }`, reading `displayText` from this field.
215
216
  */
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(),
217
228
  /** Field definitions (a link field is one carrying `doctype`) */
218
229
  fields: z.array(DoctypeFieldSchema),
219
230
  /** Relationship links to other doctypes */
@@ -239,9 +250,13 @@ export const DoctypeMeta = z
239
250
  // need not be unique — `stonecropRecord`'s row map then keeps whichever row comes last.
240
251
  // Refusing at the gate is what makes it say so.
241
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
+ //
242
257
  // Zero keys stays legal and is not an omission: a surrogate-key doctype declares none and
243
258
  // resolves through `getRecordIdField`'s documented `id` fallback.
244
- const declared = doctype.fields.filter(f => f.kind === 'field' && f.primaryKey);
259
+ const declared = flattenFields(doctype.fields).filter(f => f.kind === 'field' && f.primaryKey);
245
260
  if (declared.length > 1) {
246
261
  ctx.addIssue({
247
262
  code: 'custom',
@@ -267,6 +282,45 @@ export const DoctypeMeta = z
267
282
  });
268
283
  }
269
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
+ }
270
324
  /**
271
325
  * Suffix appended to a link fieldname for its pre-resolved display text in record payloads.
272
326
  *
@@ -157,6 +157,29 @@ export interface TableField {
157
157
  * @public
158
158
  */
159
159
  export type DoctypeField = ValueField | FieldsetField | TableField;
160
+ /**
161
+ * Which of the three field shapes an entry has, read from the entry's own structure.
162
+ *
163
+ * The single definition of that question. It had three copies before this — the parser's
164
+ * `injectKind`, {@link stripFieldKind}'s agreement check, and the docbuilder's own
165
+ * `isValueField` in another package — each free to drift, and drift here re-types a field rather
166
+ * than throwing: a value field read as a fieldset loses its column, a fieldset read as a value
167
+ * field loses every child.
168
+ *
169
+ * Deliberately **shape-only**: a declared `kind` is ignored. Two callers depend on that. The
170
+ * stripper compares this against the declaration to decide whether removing it is lossless, which
171
+ * it cannot do if this honours it. The docbuilder reads raw JSON off disk and classifies entries to
172
+ * decide which to render as editable rows — and `kind` is Stonecrop's own discriminant, not
173
+ * something a doctype author writes, so a tool reading a file has no business consulting it.
174
+ *
175
+ * `injectKind` is the one place a declaration still wins, and only to leave an already-parsed
176
+ * object untouched on its way back through.
177
+ *
178
+ * @param field - a field entry, authored or parsed
179
+ * @returns the kind its shape implies
180
+ * @public
181
+ */
182
+ export declare function inferFieldKind(field: unknown): DoctypeField['kind'];
160
183
  /**
161
184
  * Recursively injects the `kind` discriminant into a raw field object and, for fieldsets,
162
185
  * into each of its nested `schema` children — mirroring exactly what Zod's `preprocess`
@@ -173,6 +196,27 @@ export type DoctypeField = ValueField | FieldsetField | TableField;
173
196
  * @public
174
197
  */
175
198
  export declare function normalizeFieldKind(field: unknown): unknown;
199
+ /**
200
+ * Remove the `kind` discriminant from a field, recursing into a fieldset's children.
201
+ *
202
+ * The outbound half of the boundary {@link normalizeFieldKind} owns inbound. `kind` is a
203
+ * discriminated-union tag the parser synthesizes, not something an author writes, so nothing that
204
+ * *writes* a doctype should put it on disk — the generator and the docbuilder's save both call
205
+ * this. Without it the two round-trip asymmetrically: every save adds a key the file never had.
206
+ *
207
+ * Strips only when `injectKind` would restore exactly what was removed. A fieldset carrying no
208
+ * `schema` re-infers as a plain field, so its `kind` is kept rather than silently re-typing the
209
+ * document; `DoctypeMeta` requires `schema` on a fieldset, so that shape is already invalid and
210
+ * belongs to the load gate, not here.
211
+ *
212
+ * Table `columns` are {@link ColumnSchema} entries rather than `DoctypeField`s and never carry an
213
+ * injected `kind`, so they are passed through untouched — the same asymmetry `injectKind` has.
214
+ *
215
+ * @param field - a field object, as held in memory after parsing
216
+ * @returns the field without `kind`, safe to serialize
217
+ * @public
218
+ */
219
+ export declare function stripFieldKind(field: unknown): unknown;
176
220
  /**
177
221
  * The field properties a `source: 'introspected'` marker freezes — the ones the database owns.
178
222
  *
@@ -194,15 +238,19 @@ export declare const INTROSPECTED_IDENTITY_PROPS: readonly ["fieldname", "primar
194
238
  * route/store key from it. Call this; never re-derive the rule at the call site, or the two will
195
239
  * drift and the client will key records by a column the server never queried.
196
240
  *
197
- * Two deliberate limits, both matching the shape `primaryKey` actually has:
198
- * - Only **top-level** fields are scanned. `primaryKey` is a `ValueField` flag and a fieldset's
199
- * children are not identity columns, so a nested match would be an authoring error, not a PK.
200
- * - The **first** match wins. Identity is single-valued by design — a doctype describes the API
201
- * surface, and mapping a composite database key onto one identity there is the adapter's job —
202
- * so a doctype declaring several is malformed rather than composite. `DoctypeMeta` rejects that
203
- * at the load gate; this stays total for callers holding fields that never went through it.
241
+ * Two deliberate rules, both matching the shape `primaryKey` actually has:
242
+ * - Fieldset children are **included**, via {@link flattenFields}. A fieldset is layout, not
243
+ * scope: its children are fields of the doctype with columns of their own, which is why the
244
+ * adapter's SELECT already descends and why `getDisplayField` does too. Scanning top level only
245
+ * did not *refuse* a nested declaration — it ignored one, so an author marked identity and
246
+ * nothing honoured it and nothing said so.
247
+ * - The **first** match in document order wins. Identity is single-valued by design — a doctype
248
+ * describes the API surface, and mapping a composite database key onto one identity there is the
249
+ * adapter's job — so a doctype declaring several is malformed rather than composite.
250
+ * `DoctypeMeta` rejects that at the load gate; this stays total for callers holding fields that
251
+ * never went through it.
204
252
  *
205
- * @param fields - the doctype's top-level fields
253
+ * @param fields - the doctype's fields; fieldset children are descended into
206
254
  * @returns the primary-key field, or `undefined` for a PK-less doctype
207
255
  * @public
208
256
  */
@@ -1 +1 @@
1
- {"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../src/field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAEzC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,wFAQtB,CAAA;AAEH;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEvD;;;GAGG;AACH,eAAO,MAAM,eAAe;;iBAQzB,CAAA;AAEH;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAM7D;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAC1B,+DAA+D;IAC/D,IAAI,EAAE,OAAO,CAAA;IACb,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;kFAC8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAA;IACrD,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;wFAGoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB;kCAC8B;IAC9B,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,4FAA4F;IAC5F,WAAW,CAAC,EAAE,WAAW,GAAG,KAAK,GAAG,YAAY,GAAG,YAAY,CAAA;IAC/D;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,cAAc,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,8DAA8D;IAC9D,IAAI,EAAE,UAAU,CAAA;IAChB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAA;IACjB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB,uEAAuE;IACvE,MAAM,EAAE,YAAY,EAAE,CAAA;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,yDAAyD;IACzD,IAAI,EAAE,OAAO,CAAA;IACb,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAA;IACjB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oFAAoF;IACpF,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,uFAAuF;IACvF,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,uDAAuD;IACvD,IAAI,CAAC,EAAE,eAAe,CAAA;CACtB;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,CAAA;AAkClE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS1D;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,uFAO9B,CAAA;AAEV;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,UAAU,GAAG,SAAS,CAE1F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC,EAAE,CAU1F;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,UAAU,GAAG,SAAS,CAKxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAExE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,GAAG,SAAS,CAUpB;AA6ED;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA2B,CAAA;AAExD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAA8B,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA2B,CAAA;AAExD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,mFAA6B,CAAA"}
1
+ {"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../src/field.ts"],"names":[],"mappings":"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"}
package/dist/src/field.js CHANGED
@@ -40,6 +40,37 @@ export const FieldValidation = z
40
40
  // ---------------------------------------------------------------------------
41
41
  // Zod runtime validation schemas
42
42
  // ---------------------------------------------------------------------------
43
+ /**
44
+ * Which of the three field shapes an entry has, read from the entry's own structure.
45
+ *
46
+ * The single definition of that question. It had three copies before this — the parser's
47
+ * `injectKind`, {@link stripFieldKind}'s agreement check, and the docbuilder's own
48
+ * `isValueField` in another package — each free to drift, and drift here re-types a field rather
49
+ * than throwing: a value field read as a fieldset loses its column, a fieldset read as a value
50
+ * field loses every child.
51
+ *
52
+ * Deliberately **shape-only**: a declared `kind` is ignored. Two callers depend on that. The
53
+ * stripper compares this against the declaration to decide whether removing it is lossless, which
54
+ * it cannot do if this honours it. The docbuilder reads raw JSON off disk and classifies entries to
55
+ * decide which to render as editable rows — and `kind` is Stonecrop's own discriminant, not
56
+ * something a doctype author writes, so a tool reading a file has no business consulting it.
57
+ *
58
+ * `injectKind` is the one place a declaration still wins, and only to leave an already-parsed
59
+ * object untouched on its way back through.
60
+ *
61
+ * @param field - a field entry, authored or parsed
62
+ * @returns the kind its shape implies
63
+ * @public
64
+ */
65
+ export function inferFieldKind(field) {
66
+ if (typeof field !== 'object' || field === null || Array.isArray(field))
67
+ return 'field';
68
+ if ('schema' in field)
69
+ return 'fieldset';
70
+ if ('columns' in field)
71
+ return 'table';
72
+ return 'field';
73
+ }
43
74
  /**
44
75
  * Infers the `kind` discriminant from the structural properties of a raw field
45
76
  * object, then injects it if absent. This allows authored JSON to omit `kind`
@@ -63,13 +94,14 @@ function injectKind(data) {
63
94
  return data;
64
95
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- safe: non-null, non-array object verified by guards above
65
96
  const obj = data;
97
+ // An explicit `kind` is left exactly as it was found: this is the one place a declaration still
98
+ // beats the shape, and only so an already-parsed object survives a second pass unchanged.
99
+ // Returning `data` itself rather than rebuilding it also preserves identity and key order.
100
+ // Nothing outside this function shares that precedence — a reader classifying a file on disk
101
+ // wants `inferFieldKind`, because `kind` is ours and no author writes it.
66
102
  if ('kind' in obj)
67
103
  return data;
68
- if ('schema' in obj)
69
- return { kind: 'fieldset', ...obj };
70
- if ('columns' in obj)
71
- return { kind: 'table', ...obj };
72
- return { kind: 'field', ...obj };
104
+ return { kind: inferFieldKind(obj), ...obj };
73
105
  }
74
106
  /**
75
107
  * Recursively injects the `kind` discriminant into a raw field object and, for fieldsets,
@@ -97,6 +129,39 @@ export function normalizeFieldKind(field) {
97
129
  }
98
130
  return injected;
99
131
  }
132
+ /**
133
+ * Remove the `kind` discriminant from a field, recursing into a fieldset's children.
134
+ *
135
+ * The outbound half of the boundary {@link normalizeFieldKind} owns inbound. `kind` is a
136
+ * discriminated-union tag the parser synthesizes, not something an author writes, so nothing that
137
+ * *writes* a doctype should put it on disk — the generator and the docbuilder's save both call
138
+ * this. Without it the two round-trip asymmetrically: every save adds a key the file never had.
139
+ *
140
+ * Strips only when `injectKind` would restore exactly what was removed. A fieldset carrying no
141
+ * `schema` re-infers as a plain field, so its `kind` is kept rather than silently re-typing the
142
+ * document; `DoctypeMeta` requires `schema` on a fieldset, so that shape is already invalid and
143
+ * belongs to the load gate, not here.
144
+ *
145
+ * Table `columns` are {@link ColumnSchema} entries rather than `DoctypeField`s and never carry an
146
+ * injected `kind`, so they are passed through untouched — the same asymmetry `injectKind` has.
147
+ *
148
+ * @param field - a field object, as held in memory after parsing
149
+ * @returns the field without `kind`, safe to serialize
150
+ * @public
151
+ */
152
+ export function stripFieldKind(field) {
153
+ if (typeof field !== 'object' || field === null || Array.isArray(field))
154
+ return field;
155
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- safe: non-null, non-array object verified by the guard above
156
+ const obj = field;
157
+ if (obj.kind !== undefined && obj.kind !== inferFieldKind(obj))
158
+ return field;
159
+ const { kind: _kind, ...rest } = obj;
160
+ if (Array.isArray(rest.schema)) {
161
+ return { ...rest, schema: rest.schema.map(stripFieldKind) };
162
+ }
163
+ return rest;
164
+ }
100
165
  /**
101
166
  * The field properties a `source: 'introspected'` marker freezes — the ones the database owns.
102
167
  *
@@ -125,20 +190,24 @@ export const INTROSPECTED_IDENTITY_PROPS = [
125
190
  * route/store key from it. Call this; never re-derive the rule at the call site, or the two will
126
191
  * drift and the client will key records by a column the server never queried.
127
192
  *
128
- * Two deliberate limits, both matching the shape `primaryKey` actually has:
129
- * - Only **top-level** fields are scanned. `primaryKey` is a `ValueField` flag and a fieldset's
130
- * children are not identity columns, so a nested match would be an authoring error, not a PK.
131
- * - The **first** match wins. Identity is single-valued by design — a doctype describes the API
132
- * surface, and mapping a composite database key onto one identity there is the adapter's job —
133
- * so a doctype declaring several is malformed rather than composite. `DoctypeMeta` rejects that
134
- * at the load gate; this stays total for callers holding fields that never went through it.
193
+ * Two deliberate rules, both matching the shape `primaryKey` actually has:
194
+ * - Fieldset children are **included**, via {@link flattenFields}. A fieldset is layout, not
195
+ * scope: its children are fields of the doctype with columns of their own, which is why the
196
+ * adapter's SELECT already descends and why `getDisplayField` does too. Scanning top level only
197
+ * did not *refuse* a nested declaration — it ignored one, so an author marked identity and
198
+ * nothing honoured it and nothing said so.
199
+ * - The **first** match in document order wins. Identity is single-valued by design — a doctype
200
+ * describes the API surface, and mapping a composite database key onto one identity there is the
201
+ * adapter's job — so a doctype declaring several is malformed rather than composite.
202
+ * `DoctypeMeta` rejects that at the load gate; this stays total for callers holding fields that
203
+ * never went through it.
135
204
  *
136
- * @param fields - the doctype's top-level fields
205
+ * @param fields - the doctype's fields; fieldset children are descended into
137
206
  * @returns the primary-key field, or `undefined` for a PK-less doctype
138
207
  * @public
139
208
  */
140
209
  export function getPrimaryKeyField(fields) {
141
- return fields.find((f) => f.kind === 'field' && Boolean(f.primaryKey));
210
+ return flattenFields(fields).find((f) => f.kind === 'field' && Boolean(f.primaryKey));
142
211
  }
143
212
  /**
144
213
  * Recursively flatten Fieldset containers into a flat array of non-container fields.
@@ -2,11 +2,11 @@ export type { InteractionMode } from './mode';
2
2
  export { TableViewConfig } from './table';
3
3
  export { CANONICAL_COMPONENTS, COMPONENT_CATEGORY, COMPONENT_LINK_EXPANSION, componentCategory, componentLinkExpansion, resolveLinkRenderMode, type ComponentCategory, type LinkExpansion, type LinkRenderMode, } from './component-meta';
4
4
  export type { DoctypeField, FieldOptions, FieldValidation, FieldsetField, TableField, ValueField } from './field';
5
- export { DoctypeFieldSchema, FieldsetFieldSchema, flattenFields, getDisplayField, getPrimaryKeyField, getRecordIdentity, getRecordIdField, INTROSPECTED_IDENTITY_PROPS, normalizeFieldKind, TableFieldSchema, ValueFieldSchema, } from './field';
6
- export { ActionDefinition, TriggerDefinition, WorkflowLayout, WorkflowMeta, isActionAllowedInState, LINK_DISPLAY_SUFFIX, linkDisplayFieldname, } from './doctype';
5
+ export { DoctypeFieldSchema, FieldsetFieldSchema, flattenFields, getDisplayField, getPrimaryKeyField, getRecordIdentity, getRecordIdField, INTROSPECTED_IDENTITY_PROPS, normalizeFieldKind, inferFieldKind, stripFieldKind, TableFieldSchema, ValueFieldSchema, } from './field';
6
+ export { ActionDefinition, TriggerDefinition, WorkflowLayout, WorkflowMeta, getDoctypeSlug, isActionAllowedInState, LINK_DISPLAY_SUFFIX, linkDisplayFieldname, } from './doctype';
7
7
  export type { Cardinality, CustomFetch, DataClient, DoctypeContext, DoctypeMeta, DoctypeRef, FetchStrategy, GetRecordOptions, GetRecordResult, GetRecordsOptions, GetRecordsResult, LazyFetch, LinkDeclaration, SerializedFunction, SyncFetch, } from './doctype';
8
8
  export { parseDoctype, parseField, validateDoctype, validateField, type ValidationError, type ValidationResult, } from './validation';
9
- export { buildScalarMap, classifyFieldType, convertGraphQLSchema, defaultIsEntityField, defaultIsEntityType, formatDoctypeDrift, GQL_SCALAR_MAP, INTERNAL_SCALARS, mergeIntrospectedDoctype, WELL_KNOWN_SCALARS, type AuthoredDoctype, type ConvertedGraphQLDoctype, type DoctypeDrift, type GraphQLConversionFieldMeta, type GraphQLConversionOptions, type IntrospectionSource, type MergeResult, } from './converter';
9
+ export { aggregateDoctypeName, buildAggregateDoctype, buildScalarMap, classifyFieldType, convertGraphQLSchema, defaultIsEntityField, defaultIsEntityType, formatDoctypeDrift, GQL_SCALAR_MAP, INTERNAL_SCALARS, mergeIntrospectedDoctype, planGeneration, WELL_KNOWN_SCALARS, type AuthoredDoctype, type ConvertedGraphQLDoctype, type DoctypeDrift, type GraphQLConversionFieldMeta, type GenerationPlanEntry, type GenerationPlanOptions, type GraphQLConversionOptions, type IntrospectionSource, type MergeOptions, type MergeResult, } from './converter';
10
10
  export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from './naming';
11
11
  export type { ColumnSchema } from './column-schema';
12
12
  export type { BadgeDescriptor, BadgePresentation, BadgeSpec, BadgeSpecObject, BadgeVariant, SelectOptions, } from './badge';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAG7C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAGzC,OAAO,EACN,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,GACnB,MAAM,kBAAkB,CAAA;AAGzB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACjH,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,2BAA2B,EAC3B,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,GAChB,MAAM,SAAS,CAAA;AAKhB,OAAO,EACN,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,WAAW,CAAA;AAClB,YAAY,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,EACd,WAAW,EACX,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,SAAS,GACT,MAAM,WAAW,CAAA;AAGlB,OAAO,EACN,YAAY,EACZ,UAAU,EACV,eAAe,EACf,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACrB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACN,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,WAAW,GAChB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGtH,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAGnD,YAAY,EACX,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,YAAY,EACZ,aAAa,GACb,MAAM,SAAS,CAAA;AAChB,OAAO,EACN,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,aAAa,GACb,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAG7C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAGzC,OAAO,EACN,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,GACnB,MAAM,kBAAkB,CAAA;AAGzB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACjH,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,2BAA2B,EAC3B,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GAChB,MAAM,SAAS,CAAA;AAKhB,OAAO,EACN,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,WAAW,CAAA;AAClB,YAAY,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,EACd,WAAW,EACX,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,SAAS,GACT,MAAM,WAAW,CAAA;AAGlB,OAAO,EACN,YAAY,EACZ,UAAU,EACV,eAAe,EACf,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACrB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACN,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,WAAW,GAChB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGtH,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAGnD,YAAY,EACX,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,YAAY,EACZ,aAAa,GACb,MAAM,SAAS,CAAA;AAChB,OAAO,EACN,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,aAAa,GACb,MAAM,SAAS,CAAA"}
package/dist/src/index.js CHANGED
@@ -2,15 +2,15 @@
2
2
  export { TableViewConfig } from './table';
3
3
  // Component → semantic category — the single source of "what kind of value does this component render"
4
4
  export { CANONICAL_COMPONENTS, COMPONENT_CATEGORY, COMPONENT_LINK_EXPANSION, componentCategory, componentLinkExpansion, resolveLinkRenderMode, } from './component-meta';
5
- export { DoctypeFieldSchema, FieldsetFieldSchema, flattenFields, getDisplayField, getPrimaryKeyField, getRecordIdentity, getRecordIdField, INTROSPECTED_IDENTITY_PROPS, normalizeFieldKind, TableFieldSchema, ValueFieldSchema, } from './field';
5
+ export { DoctypeFieldSchema, FieldsetFieldSchema, flattenFields, getDisplayField, getPrimaryKeyField, getRecordIdentity, getRecordIdField, INTROSPECTED_IDENTITY_PROPS, normalizeFieldKind, inferFieldKind, stripFieldKind, TableFieldSchema, ValueFieldSchema, } from './field';
6
6
  // Doctype schema
7
7
  // ActionDefinition and WorkflowMeta are exported as values (Zod schemas) so consumers can use
8
8
  // .safeParse(), .shape, etc. at runtime. TypeScript types are inferred from the same exports.
9
- export { ActionDefinition, TriggerDefinition, WorkflowLayout, WorkflowMeta, isActionAllowedInState, LINK_DISPLAY_SUFFIX, linkDisplayFieldname, } from './doctype';
9
+ export { ActionDefinition, TriggerDefinition, WorkflowLayout, WorkflowMeta, getDoctypeSlug, isActionAllowedInState, LINK_DISPLAY_SUFFIX, linkDisplayFieldname, } from './doctype';
10
10
  // Validation helpers
11
11
  export { parseDoctype, parseField, validateDoctype, validateField, } from './validation';
12
12
  // GraphQL to Doctype conversion
13
- export { buildScalarMap, classifyFieldType, convertGraphQLSchema, defaultIsEntityField, defaultIsEntityType, formatDoctypeDrift, GQL_SCALAR_MAP, INTERNAL_SCALARS, mergeIntrospectedDoctype, WELL_KNOWN_SCALARS, } from './converter';
13
+ export { aggregateDoctypeName, buildAggregateDoctype, buildScalarMap, classifyFieldType, convertGraphQLSchema, defaultIsEntityField, defaultIsEntityType, formatDoctypeDrift, GQL_SCALAR_MAP, INTERNAL_SCALARS, mergeIntrospectedDoctype, planGeneration, WELL_KNOWN_SCALARS, } from './converter';
14
14
  // Naming utilities
15
15
  export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from './naming';
16
16
  export { hasBadgeOptions, isBadgeDescriptor, isSelectChoiceMap, isSelectOptions, lookupBadge, selectChoices, } from './badge';