@coffer-org/sdk 3.3.0 → 3.4.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/dist/extend.js CHANGED
@@ -1,10 +1,11 @@
1
- import { layoutToClient, fieldEntries, buildShape } from "./shelf.js";
1
+ import { layoutToClient, fieldEntries, buildShape, checkFieldDeclaration } from "./shelf.js";
2
2
  import { z } from 'zod';
3
3
  export function defineExtend(e) {
4
4
  const keys = fieldEntries(e.fields).map(([k]) => k);
5
5
  const dup = keys.find((k, i) => keys.indexOf(k) !== i);
6
6
  if (dup)
7
7
  throw new Error(`[extend] ${e.id}: duplicate key '${dup}'`);
8
+ checkFieldDeclaration(e.fields, `[extend] ${e.id}`);
8
9
  return e;
9
10
  }
10
11
  export function extendMatches(e, library, shelf) {
@@ -50,6 +50,7 @@ export interface FieldConfig {
50
50
  export type NormalizedOpts = {
51
51
  key?: string;
52
52
  value?: unknown;
53
+ store?: boolean;
53
54
  label?: string;
54
55
  options?: string | unknown[];
55
56
  slots?: string[];
package/dist/fields.d.ts CHANGED
@@ -8,7 +8,7 @@ export type { Rules, View, FieldConfig } from './fields/normalize.ts';
8
8
  export { normalizeOpts } from './fields/normalize.ts';
9
9
  import { type Rules, type View } from './fields/normalize.ts';
10
10
  import { presets } from './field-presets.ts';
11
- export type CorePrim = 'text' | 'number' | 'date' | 'time' | 'datetime' | 'checkbox' | 'select' | 'json' | 'check' | 'measured' | 'relation' | 'file' | 'per-weekday' | 'lookup';
11
+ export type CorePrim = 'text' | 'number' | 'date' | 'time' | 'datetime' | 'checkbox' | 'select' | 'json' | 'check' | 'measured' | 'relation' | 'file' | 'per-weekday';
12
12
  export type Prim = CorePrim | (string & {});
13
13
  export type ColumnType = 'text' | 'integer' | 'real' | 'boolean' | 'date' | 'time' | 'datetime';
14
14
  export type OptionIcon = {
@@ -48,6 +48,8 @@ export interface ComputeCtx {
48
48
  shelf: string;
49
49
  };
50
50
  self?: Record<string, unknown>;
51
+ parent?: Record<string, unknown>;
52
+ ref?: (key: string) => Promise<Record<string, unknown> | null>;
51
53
  }
52
54
  export type ComputeFn = (ctx: ComputeCtx) => unknown;
53
55
  export interface DividerEl {
@@ -184,6 +186,7 @@ export type FieldOptions = string | unknown[];
184
186
  export interface FieldCoreOpts {
185
187
  key?: string;
186
188
  value?: unknown;
189
+ store?: boolean;
187
190
  label?: string;
188
191
  rules?: Rules;
189
192
  view?: View;
@@ -204,6 +207,7 @@ export declare function wrapKey(opts: {
204
207
  emphasis?: 'hero' | 'muted';
205
208
  noLabel?: boolean;
206
209
  role?: 'cover' | 'avatar';
210
+ store?: boolean;
207
211
  derive?: import('./derive.ts').DeriveSpec;
208
212
  }, meta: FieldMeta): FieldItem | StaticEl | FieldMeta;
209
213
  export interface FieldMeta extends FieldClient {
@@ -522,21 +526,6 @@ export declare function perWeekday(o: {
522
526
  export type RowSubField = FieldClient & {
523
527
  key: string;
524
528
  };
525
- export interface LookupOpts extends FieldCoreOpts {
526
- from: string;
527
- pick: string[];
528
- }
529
- export declare function lookup(o: LookupOpts & {
530
- key: string;
531
- }): FieldItem;
532
- export declare function lookup(o: LookupOpts & {
533
- value: unknown;
534
- key?: undefined;
535
- }): StaticEl;
536
- export declare function lookup(o: LookupOpts & {
537
- key?: undefined;
538
- value?: undefined;
539
- }): FieldMeta;
540
529
  export declare function clientToMeta(fc: FieldClient): FieldMeta;
541
530
  interface PeriodOpts extends FieldCoreOpts {
542
531
  parts?: PartsSpec;
@@ -723,7 +712,6 @@ declare const PRIMITIVES: {
723
712
  money: typeof money;
724
713
  code: typeof code;
725
714
  geo: typeof geo;
726
- lookup: typeof lookup;
727
715
  perWeekday: typeof perWeekday;
728
716
  period: typeof period;
729
717
  file: typeof file;
package/dist/fields.js CHANGED
@@ -134,8 +134,18 @@ export function wrapKey(opts, meta) {
134
134
  `"the server computes this field"; use value, the canonical spelling`);
135
135
  if (opts.derive)
136
136
  m = { ...m, derive: opts.derive, derived: true, hints: { ...m.hints, noEditControl: true } };
137
- if (opts.key && 'value' in opts && opts.value !== undefined) {
137
+ const computed = opts.key && 'value' in opts && opts.value !== undefined;
138
+ if (opts.derive && opts.store === false)
139
+ throw new Error(`[wrapKey] '${opts.key}': derive writes a column on every write, so store: false contradicts it; ` +
140
+ `spell the computation as \`value\` instead.`);
141
+ if (opts.store === false && !computed)
142
+ throw new Error(`[wrapKey] '${opts.key ?? '(keyless)'}': store: false needs key + value — it drops the COLUMN of a ` +
143
+ `computed field, so that the server recomputes it on every read. A field without \`value\` has ` +
144
+ `nothing to recompute; a field without \`key\` already stores nothing.`);
145
+ if (computed) {
138
146
  m = { ...m, compute: opts.value, derived: true, hints: { ...m.hints, noEditControl: true } };
147
+ if (opts.store === false)
148
+ m = { ...m, virtual: true };
139
149
  return { key: opts.key, type: m };
140
150
  }
141
151
  if (opts.key)
@@ -625,7 +635,7 @@ export function measured(raw) {
625
635
  const rowSchema = z.object(partsRowShape({
626
636
  value: numSchema,
627
637
  unit: z.string().refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') }),
628
- }, parts));
638
+ }, parts, required !== true));
629
639
  const s = z.unknown().transform((raw, ctx) => {
630
640
  const parsed = jsonValue(raw);
631
641
  if (typeof parsed === 'string') {
@@ -672,9 +682,7 @@ export function unit(raw) {
672
682
  const o = normalizeOpts(raw);
673
683
  const required = o.required ?? false;
674
684
  const units = resolveUnits(o.options);
675
- const s = z
676
- .string(reqErr())
677
- .refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') });
685
+ const s = z.string(reqErr()).refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') });
678
686
  const meta = {
679
687
  kind: 'unit',
680
688
  label: o.label ?? '',
@@ -724,7 +732,7 @@ export function money(raw) {
724
732
  const rowSchema = z.object(partsRowShape({
725
733
  value: numSchema,
726
734
  currency: z.string().refine(isCurrencyCode, { message: vmsg('money_currency') }),
727
- }, parts));
735
+ }, parts, required !== true));
728
736
  const s = z.unknown().transform((raw, ctx) => {
729
737
  const parsed = jsonValue(raw);
730
738
  if (typeof parsed === 'string') {
@@ -764,7 +772,7 @@ export function code(raw) {
764
772
  const required = o.required ?? false;
765
773
  const cfg = o.config ?? {};
766
774
  const parts = resolveParts(CODE_ROLES, o.parts);
767
- const rowSchema = z.object(partsRowShape({ code: z.string(), lang: z.string() }, parts));
775
+ const rowSchema = z.object(partsRowShape({ code: z.string(), lang: z.string() }, parts, required !== true));
768
776
  const s = z.unknown().transform((raw, ctx) => {
769
777
  const parsed = jsonValue(raw);
770
778
  if (typeof parsed === 'string') {
@@ -804,15 +812,14 @@ export function geo(raw) {
804
812
  lat: z.number().min(-90).max(90),
805
813
  lng: z.number().min(-180).max(180),
806
814
  label: z.string().nullish(),
807
- }, parts));
815
+ }, parts, required !== true));
808
816
  const clientCoordKeys = parts
809
817
  .filter((p) => (p.role === 'lat' || p.role === 'lng') && p.mode === 'stored')
810
818
  .map(partValueKey);
811
819
  const s = z.unknown().transform((raw, ctx) => {
812
820
  const parsed = jsonValue(raw);
813
821
  const p = parsed;
814
- const noCoords = p == null ||
815
- (typeof p === 'object' && clientCoordKeys.length > 0 && clientCoordKeys.every((k) => p[k] == null));
822
+ const noCoords = p == null || (typeof p === 'object' && clientCoordKeys.length > 0 && clientCoordKeys.every((k) => p[k] == null));
816
823
  if (noCoords) {
817
824
  if (required)
818
825
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('required') });
@@ -854,20 +861,6 @@ export function perWeekday(o) {
854
861
  kind: 'per-weekday',
855
862
  };
856
863
  }
857
- export function lookup(raw) {
858
- const o = normalizeOpts(raw);
859
- const meta = {
860
- kind: 'lookup',
861
- label: o.label ?? '',
862
- required: false,
863
- prim: 'lookup',
864
- column: 'text',
865
- virtual: true,
866
- hints: { from: o.from, pick: o.pick, compareWith: o.compareWith ?? null },
867
- zod: z.any().optional(),
868
- };
869
- return wrapKey(o, meta);
870
- }
871
864
  export function clientToMeta(fc) {
872
865
  const { parts: _parts, ...rest } = fc;
873
866
  return { ...rest, column: 'text', zod: z.any() };
@@ -1066,7 +1059,7 @@ function makeRange(kind, isInt, raw) {
1066
1059
  n = n.max(cfg.max);
1067
1060
  const parts = resolveParts(RANGE_ROLES(isInt), o.parts);
1068
1061
  const rangeKeys = partValueKeys(parts);
1069
- const rowSchema = z.object(partsRowShape({ from: n, to: n }, parts));
1062
+ const rowSchema = z.object(partsRowShape({ from: n, to: n }, parts, required !== true));
1070
1063
  const endpoint = endpointReader(parts, rangeKeys);
1071
1064
  const s = z.unknown().transform((raw, ctx) => {
1072
1065
  if (typeof raw === 'string') {
@@ -1165,7 +1158,6 @@ const PRIMITIVES = {
1165
1158
  money,
1166
1159
  code,
1167
1160
  geo,
1168
- lookup,
1169
1161
  perWeekday,
1170
1162
  period,
1171
1163
  file,
package/dist/parts.d.ts CHANGED
@@ -24,5 +24,10 @@ export interface PartSlot {
24
24
  export declare function partValueKeys(parts: PartSlot[]): Record<string, string>;
25
25
  export declare function resolveParts(roles: RoleDef[], spec: PartsSpec | undefined): ResolvedPart[];
26
26
  export declare function partColumns(parts: ResolvedPart[]): Record<string, ColumnType>;
27
- export declare function partsRowShape(shape: Record<string, z.ZodTypeAny>, parts: ResolvedPart[]): Record<string, z.ZodTypeAny>;
27
+ export declare function partsRowShape(shape: Record<string, z.ZodTypeAny>, parts: ResolvedPart[], tolerateNull?: boolean): Record<string, z.ZodTypeAny>;
28
+ export declare function mixedOwnedStoredParts(parts: ResolvedPart[]): {
29
+ server: string[];
30
+ client: string[];
31
+ } | null;
32
+ export declare function mandatedClientParts(required: unknown, parts: ResolvedPart[]): string[];
28
33
  export declare function stripServerOwnedParts(value: Record<string, unknown>, parts: ResolvedPart[]): Record<string, unknown>;
package/dist/parts.js CHANGED
@@ -72,7 +72,7 @@ export function partColumns(parts) {
72
72
  cols[p.key] = p.meta.column;
73
73
  return cols;
74
74
  }
75
- export function partsRowShape(shape, parts) {
75
+ export function partsRowShape(shape, parts, tolerateNull = false) {
76
76
  const out = { ...shape };
77
77
  for (const p of parts)
78
78
  if (partValueKey(p) !== p.role)
@@ -82,11 +82,25 @@ export function partsRowShape(shape, parts) {
82
82
  out[p.role] = z.unknown().optional();
83
83
  else if (p.mode === 'computedStored')
84
84
  out[p.key] = z.unknown().optional();
85
- else
86
- out[p.key] = p.overridden ? p.meta.zod : (shape[p.role] ?? z.unknown().optional());
85
+ else {
86
+ const own = p.overridden ? p.meta.zod : (shape[p.role] ?? z.unknown().optional());
87
+ out[p.key] = tolerateNull && !(p.overridden && p.meta.required === true) ? own.nullable() : own;
88
+ }
87
89
  }
88
90
  return out;
89
91
  }
92
+ export function mixedOwnedStoredParts(parts) {
93
+ const server = parts.filter((p) => p.mode === 'computedStored').map((p) => p.role);
94
+ const client = parts.filter((p) => p.mode === 'stored').map((p) => p.role);
95
+ return server.length > 0 && client.length > 0 ? { server, client } : null;
96
+ }
97
+ export function mandatedClientParts(required, parts) {
98
+ const speaks = (flag) => flag !== undefined && flag !== false;
99
+ const field = speaks(required);
100
+ return parts
101
+ .filter((p) => p.mode === 'stored' && (field || (p.overridden && speaks(p.meta.required))))
102
+ .map((p) => p.role);
103
+ }
90
104
  function serverOwnedSlot(p) {
91
105
  if (!p.key)
92
106
  return p.role;
package/dist/shelf.d.ts CHANGED
@@ -28,12 +28,14 @@ export interface ShelfDef {
28
28
  export declare function fieldEntries(items: LayoutEl[]): [string, FieldMeta][];
29
29
  export declare function storageColumns(key: string, field: FieldMeta): [string, ColumnType][];
30
30
  export declare function cellValue(field: FieldMeta, key: string, row: Record<string, unknown>): unknown;
31
+ export declare function magnitudeSub(field: FieldMeta): string | undefined;
31
32
  export interface CollectionEntry {
32
33
  key: string;
33
34
  group: GroupEl;
34
35
  }
35
36
  export declare function collectionGroups(items: LayoutEl[]): CollectionEntry[];
36
37
  export declare function fieldMap(items: LayoutEl[]): Record<string, FieldMeta>;
38
+ export declare function checkFieldDeclaration(fields: LayoutEl[], owner: string): void;
37
39
  export declare function defineShelf(m: ShelfDef): ShelfDef;
38
40
  export declare const SYSTEM_FIELDS: readonly ["id", "createdAt", "updatedAt"];
39
41
  export declare function titleKey(m: ShelfDef): string;
package/dist/shelf.js CHANGED
@@ -4,6 +4,7 @@ import { resolveFlag } from "./condition.js";
4
4
  import { parseUrl } from "./fields/url.js";
5
5
  import { vmsg } from "./fields/validation.js";
6
6
  import { derivableKeys, legacyDeriveDeps } from "./derive.js";
7
+ import { mixedOwnedStoredParts, mandatedClientParts } from "./parts.js";
7
8
  export function fieldEntries(items) {
8
9
  const out = [];
9
10
  for (const it of items) {
@@ -34,14 +35,21 @@ export function cellValue(field, key, row) {
34
35
  const out = {};
35
36
  let filled = false;
36
37
  for (const sub of Object.keys(field.columns)) {
37
- const v = row[`${key}__${sub}`];
38
- if (v == null || v === '')
38
+ const col = `${key}__${sub}`;
39
+ if (!(col in row))
39
40
  continue;
41
+ const v = row[col];
40
42
  out[sub] = v;
41
- filled = true;
43
+ if (v != null && v !== '')
44
+ filled = true;
42
45
  }
43
46
  return filled ? out : undefined;
44
47
  }
48
+ export function magnitudeSub(field) {
49
+ if (!field.columns)
50
+ return undefined;
51
+ return Object.keys(field.columns)[0];
52
+ }
45
53
  export function collectionGroups(items) {
46
54
  const out = [];
47
55
  for (const it of items) {
@@ -65,14 +73,41 @@ export function fieldMap(items) {
65
73
  }
66
74
  return m;
67
75
  }
68
- function assertNoKeylessCompute(items, m) {
76
+ function assertNoKeylessCompute(items, owner) {
69
77
  for (const it of items) {
70
78
  if (isStatic(it) && typeof it.value === 'function')
71
- throw new Error(`${m.library}/${m.shelf}: computed element needs a key — the server computes it and needs a name to return it under`);
79
+ throw new Error(`${owner}: computed element needs a key — the server computes it and needs a name to return it under`);
72
80
  if (isGroup(it))
73
- assertNoKeylessCompute(it.fields, m);
81
+ assertNoKeylessCompute(it.fields, owner);
74
82
  }
75
83
  }
84
+ function warnMixedOwnedParts(items, owner, path = '') {
85
+ for (const it of items) {
86
+ if (isGroup(it)) {
87
+ warnMixedOwnedParts(it.fields, owner, it.key ? `${path}${it.key}.` : path);
88
+ continue;
89
+ }
90
+ if (!isField(it) || !it.type.parts)
91
+ continue;
92
+ const mixed = mixedOwnedStoredParts(it.type.parts);
93
+ if (!mixed)
94
+ continue;
95
+ const mandated = mandatedClientParts(it.type.required, it.type.parts);
96
+ if (!mandated.length)
97
+ continue;
98
+ console.warn(`${owner}: composite '${path}${it.key}' mandates a client-owned stored part (${mandated.join(', ')}) ` +
99
+ `and mixes it with a server-owned stored part (${mixed.server.join(', ')}) — ` +
100
+ `a mandated part is the client's to declare, so the write path does NOT materialize this composite ` +
101
+ `and the computed part stays absent until a client sends the field. Either drop \`required\` ` +
102
+ `(from the field or from the ${mandated.join('/')} filling), or give every stored part a \`value\` ` +
103
+ `(the composite becomes fully server-owned), or drop the \`key\` of ` +
104
+ `${mixed.client.join('/')} so it is unstored (pinned/computed and injected on read).`);
105
+ }
106
+ }
107
+ export function checkFieldDeclaration(fields, owner) {
108
+ assertNoKeylessCompute(fields, owner);
109
+ warnMixedOwnedParts(fields, owner);
110
+ }
76
111
  export function defineShelf(m) {
77
112
  const keys = fieldEntries(m.fields).map(([k]) => k);
78
113
  const dup = keys.find((k, i) => keys.indexOf(k) !== i);
@@ -94,7 +129,7 @@ export function defineShelf(m) {
94
129
  console.warn(`[shelf] ${m.library}/${m.shelf}: ${titles.length} title fields (${titles.map(([k]) => k).join(', ')}) — '${titles[0][0]}' wins`);
95
130
  if (m.standalone !== false && !m.single && titleKey(m) === 'id')
96
131
  console.warn(`[shelf] ${m.library}/${m.shelf}: no title declared (field.title() or views.title) — the record renders no heading`);
97
- assertNoKeylessCompute(m.fields, m);
132
+ checkFieldDeclaration(m.fields, `[shelf] ${m.library}/${m.shelf}`);
98
133
  const known = derivableKeys(m.fields);
99
134
  for (const [key, deps] of legacyDeriveDeps(m.fields)) {
100
135
  for (const dep of deps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/sdk",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"