@coffer-org/sdk 3.1.0 → 3.3.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.
@@ -19,9 +19,11 @@ export interface Condition {
19
19
  export type Values = Record<string, unknown>;
20
20
  export declare const COMPILABLE_SCALAR_OPS: Set<string>;
21
21
  export declare const COMPILABLE_ARRAY_OPS: Set<string>;
22
+ export declare const COMPILABLE_MEMBER_OPS: Set<string>;
22
23
  export declare function evalCondition(cond: Condition | undefined, values: Values): boolean;
23
24
  export declare function resolveFlag(v: boolean | Condition | undefined, values: Values): boolean;
24
25
  export declare function conditionKeys(cond: Condition | undefined): string[];
26
+ export declare function conditionOps(cond: Condition | undefined, field: string): string[];
25
27
  export declare function visibleOptions<T extends {
26
28
  value?: string;
27
29
  when?: Condition;
package/dist/condition.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import sift from 'sift';
2
2
  export const COMPILABLE_SCALAR_OPS = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte']);
3
3
  export const COMPILABLE_ARRAY_OPS = new Set(['$in', '$nin']);
4
+ export const COMPILABLE_MEMBER_OPS = new Set(['$contains']);
4
5
  const createEqualsOperation = sift
5
6
  .createEqualsOperation;
6
7
  const isScalar = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
@@ -80,6 +81,27 @@ export function conditionKeys(cond) {
80
81
  }
81
82
  return [...new Set(out)];
82
83
  }
84
+ export function conditionOps(cond, field) {
85
+ if (!cond)
86
+ return [];
87
+ const out = [];
88
+ for (const [key, spec] of Object.entries(cond)) {
89
+ if (spec === undefined)
90
+ continue;
91
+ if (key === '$and' || key === '$or') {
92
+ for (const c of spec)
93
+ out.push(...conditionOps(c, field));
94
+ continue;
95
+ }
96
+ if (key !== field)
97
+ continue;
98
+ if (typeof spec === 'object' && spec !== null && !Array.isArray(spec))
99
+ out.push(...Object.keys(spec));
100
+ else
101
+ out.push('$eq');
102
+ }
103
+ return [...new Set(out)];
104
+ }
83
105
  export function visibleOptions(options, values, keep) {
84
106
  const kept = keep === undefined ? undefined : new Set(Array.isArray(keep) ? keep : [keep]);
85
107
  return options.filter((o) => (kept !== undefined && o.value !== undefined && kept.has(o.value)) || (o.when ? evalCondition(o.when, values) : true));
package/dist/derive.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface DeriveSpec {
7
7
  deps: string[];
8
8
  fn: DeriveFn;
9
9
  }
10
- export declare function derivedEntries(fields: LayoutEl[]): [string, DeriveSpec][];
11
- export declare function applyDerived(fields: LayoutEl[], record: Record<string, unknown>): Record<string, unknown>;
10
+ export declare function derivedEntries(fields: LayoutEl[]): [string, DeriveFn][];
11
+ export declare function legacyDeriveDeps(fields: LayoutEl[]): [string, string[]][];
12
+ export declare function applyDerived(fields: LayoutEl[], record: Record<string, unknown>): Promise<Record<string, unknown>>;
12
13
  export declare function derivableKeys(fields: LayoutEl[]): Set<string>;
package/dist/derive.js CHANGED
@@ -1,23 +1,35 @@
1
1
  import { isField, isGroup } from "./fields.js";
2
- export function derivedEntries(fields) {
2
+ function topLevelFields(fields) {
3
3
  const out = [];
4
4
  for (const it of fields) {
5
- if (isField(it)) {
6
- const spec = it.type.derive;
7
- if (spec)
8
- out.push([it.key, spec]);
9
- }
10
- else if (isGroup(it) && !it.key) {
11
- out.push(...derivedEntries(it.fields));
12
- }
5
+ if (isField(it))
6
+ out.push([it.key, it.type]);
7
+ else if (isGroup(it) && !it.key)
8
+ out.push(...topLevelFields(it.fields));
9
+ }
10
+ return out;
11
+ }
12
+ export function derivedEntries(fields) {
13
+ const out = [];
14
+ for (const [key, meta] of topLevelFields(fields)) {
15
+ const fn = meta.compute ?? meta.derive?.fn;
16
+ if (fn)
17
+ out.push([key, typeof fn === 'function' ? fn : () => fn]);
13
18
  }
14
19
  return out;
15
20
  }
16
- export function applyDerived(fields, record) {
21
+ export function legacyDeriveDeps(fields) {
22
+ const out = [];
23
+ for (const [key, meta] of topLevelFields(fields))
24
+ if (meta.derive)
25
+ out.push([key, meta.derive.deps]);
26
+ return out;
27
+ }
28
+ export async function applyDerived(fields, record) {
17
29
  const out = {};
18
- for (const [key, spec] of derivedEntries(fields)) {
30
+ for (const [key, fn] of derivedEntries(fields)) {
19
31
  try {
20
- out[key] = spec.fn({ record });
32
+ out[key] = await fn({ record });
21
33
  }
22
34
  catch (e) {
23
35
  console.warn(`[derive] '${key}' threw`, e);
package/dist/fields.d.ts CHANGED
@@ -2,6 +2,8 @@ import { z } from 'zod';
2
2
  import { type UnitsSpec } from './units.ts';
3
3
  export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField, } from './fields/validation.ts';
4
4
  import type { Condition } from './condition.ts';
5
+ import { type PartsSpec, type ResolvedPart } from './parts.ts';
6
+ export { partValueKey, partValueKeys, type PartSlot } from './parts.ts';
5
7
  export type { Rules, View, FieldConfig } from './fields/normalize.ts';
6
8
  export { normalizeOpts } from './fields/normalize.ts';
7
9
  import { type Rules, type View } from './fields/normalize.ts';
@@ -45,6 +47,7 @@ export interface ComputeCtx {
45
47
  library: string;
46
48
  shelf: string;
47
49
  };
50
+ self?: Record<string, unknown>;
48
51
  }
49
52
  export type ComputeFn = (ctx: ComputeCtx) => unknown;
50
53
  export interface DividerEl {
@@ -104,6 +107,12 @@ export interface FieldClient {
104
107
  json?: true;
105
108
  derived?: true;
106
109
  columns?: Record<string, ColumnType>;
110
+ parts?: {
111
+ role: string;
112
+ key: string | null;
113
+ mode: string;
114
+ value?: unknown;
115
+ }[];
107
116
  hidden?: boolean | Condition;
108
117
  }
109
118
  export declare function group(o: {
@@ -201,6 +210,8 @@ export interface FieldMeta extends FieldClient {
201
210
  column: ColumnType;
202
211
  zod: z.ZodTypeAny;
203
212
  derive?: import('./derive.ts').DeriveSpec;
213
+ compute?: unknown;
214
+ parts?: ResolvedPart[];
204
215
  }
205
216
  export declare function isJsonStored(field: FieldMeta | FieldClient): boolean;
206
217
  export { LANGUAGES, SEX_OPTIONS, WEEKDAY_OPTIONS } from './fields/constants.ts';
@@ -423,6 +434,7 @@ export declare function check(o: CheckOpts & {
423
434
  }): FieldMeta;
424
435
  export interface MeasuredOpts extends FieldCoreOpts {
425
436
  options: FieldOptions;
437
+ parts?: PartsSpec;
426
438
  }
427
439
  export declare function measured(o: MeasuredOpts & {
428
440
  key: string;
@@ -456,7 +468,9 @@ export declare function amount(o: AmountOpts & {
456
468
  key?: undefined;
457
469
  value?: undefined;
458
470
  }): FieldMeta;
459
- export type MoneyOpts = FieldCoreOpts;
471
+ export interface MoneyOpts extends FieldCoreOpts {
472
+ parts?: PartsSpec;
473
+ }
460
474
  export declare function money(o: MoneyOpts & {
461
475
  key: string;
462
476
  }): FieldItem;
@@ -468,7 +482,9 @@ export declare function money(o: MoneyOpts & {
468
482
  key?: undefined;
469
483
  value?: undefined;
470
484
  }): FieldMeta;
471
- export type CodeFieldOpts = FieldCoreOpts;
485
+ export interface CodeFieldOpts extends FieldCoreOpts {
486
+ parts?: PartsSpec;
487
+ }
472
488
  export declare function code(o: CodeFieldOpts & {
473
489
  key: string;
474
490
  }): FieldItem;
@@ -480,7 +496,9 @@ export declare function code(o: CodeFieldOpts & {
480
496
  key?: undefined;
481
497
  value?: undefined;
482
498
  }): FieldMeta;
483
- export type GeoOpts = FieldCoreOpts;
499
+ export interface GeoOpts extends FieldCoreOpts {
500
+ parts?: PartsSpec;
501
+ }
484
502
  export declare function geo(o: GeoOpts & {
485
503
  key: string;
486
504
  }): FieldItem;
@@ -520,7 +538,9 @@ export declare function lookup(o: LookupOpts & {
520
538
  value?: undefined;
521
539
  }): FieldMeta;
522
540
  export declare function clientToMeta(fc: FieldClient): FieldMeta;
523
- type PeriodOpts = FieldCoreOpts;
541
+ interface PeriodOpts extends FieldCoreOpts {
542
+ parts?: PartsSpec;
543
+ }
524
544
  export declare function period(o: PeriodOpts & {
525
545
  key: string;
526
546
  }): FieldItem | GroupEl;
@@ -642,7 +662,9 @@ export interface KeyValueOpts extends FieldCoreOpts {
642
662
  export declare function keyValue(raw: KeyValueOpts & {
643
663
  key: string;
644
664
  }): GroupEl;
645
- export type RangeOpts = FieldCoreOpts;
665
+ export interface RangeOpts extends FieldCoreOpts {
666
+ parts?: PartsSpec;
667
+ }
646
668
  export declare function numberRange(o: RangeOpts & {
647
669
  key: string;
648
670
  }): FieldItem;
package/dist/fields.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { resolveUnits } from "./units.js";
3
- import { isCurrencyCode } from "./currencies.js";
3
+ import { isCurrencyCode, CURRENCY_CODES } from "./currencies.js";
4
4
  import { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue } from "./fields/validation.js";
5
5
  export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField, } from "./fields/validation.js";
6
+ import { resolveParts, partColumns, partsRowShape, partValueKey, partValueKeys, stripServerOwnedParts, } from "./parts.js";
7
+ export { partValueKey, partValueKeys } from "./parts.js";
6
8
  export { normalizeOpts } from "./fields/normalize.js";
7
9
  import { normalizeOpts } from "./fields/normalize.js";
8
10
  import { presets } from "./field-presets.js";
@@ -127,8 +129,15 @@ export function wrapKey(opts, meta) {
127
129
  m = { ...m, default: opts.default };
128
130
  if (opts.hidden !== undefined)
129
131
  m = { ...m, hidden: opts.hidden };
132
+ if (opts.derive && opts.key && 'value' in opts && opts.value !== undefined)
133
+ throw new Error(`[wrapKey] '${opts.key}': both derive and value are set — they are two spellings of ` +
134
+ `"the server computes this field"; use value, the canonical spelling`);
130
135
  if (opts.derive)
131
136
  m = { ...m, derive: opts.derive, derived: true, hints: { ...m.hints, noEditControl: true } };
137
+ if (opts.key && 'value' in opts && opts.value !== undefined) {
138
+ m = { ...m, compute: opts.value, derived: true, hints: { ...m.hints, noEditControl: true } };
139
+ return { key: opts.key, type: m };
140
+ }
132
141
  if (opts.key)
133
142
  return { key: opts.key, type: m };
134
143
  if ('value' in opts && opts.value !== undefined)
@@ -595,6 +604,10 @@ export function check(raw) {
595
604
  };
596
605
  return wrapKey(o, meta);
597
606
  }
607
+ const MEASURED_ROLES = (units) => [
608
+ { role: 'value', accepts: ['number'], fallback: () => real({}) },
609
+ { role: 'unit', accepts: ['select', 'relation'], fallback: () => select({ options: units }) },
610
+ ];
598
611
  export function measured(raw) {
599
612
  const o = normalizeOpts(raw);
600
613
  const required = o.required ?? false;
@@ -608,15 +621,16 @@ export function measured(raw) {
608
621
  numSchema = numSchema.min(cfg.min);
609
622
  if (cfg.max != null)
610
623
  numSchema = numSchema.max(cfg.max);
611
- const rowSchema = z.object({
624
+ const parts = resolveParts(MEASURED_ROLES(o.options), o.parts);
625
+ const rowSchema = z.object(partsRowShape({
612
626
  value: numSchema,
613
627
  unit: z.string().refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') }),
614
- });
615
- const s = z.unknown().superRefine((raw, ctx) => {
628
+ }, parts));
629
+ const s = z.unknown().transform((raw, ctx) => {
616
630
  const parsed = jsonValue(raw);
617
631
  if (typeof parsed === 'string') {
618
632
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
619
- return;
633
+ return z.NEVER;
620
634
  }
621
635
  const r = rowSchema.safeParse(parsed);
622
636
  if (!r.success) {
@@ -632,7 +646,9 @@ export function measured(raw) {
632
646
  code: z.ZodIssueCode.custom,
633
647
  message: vmsgIssue ? vmsgIssue.message : vmsg('measured_structure'),
634
648
  });
649
+ return z.NEVER;
635
650
  }
651
+ return stripServerOwnedParts(r.data, parts);
636
652
  });
637
653
  const meta = {
638
654
  kind: 'measured',
@@ -646,7 +662,8 @@ export function measured(raw) {
646
662
  ...(cfg.max != null && { max: cfg.max }),
647
663
  ...(cfg.step != null && { step: cfg.step }),
648
664
  },
649
- columns: { value: 'real', unit: 'text' },
665
+ columns: partColumns(parts),
666
+ parts,
650
667
  zod: optionalize(s, required),
651
668
  };
652
669
  return wrapKey(o, meta);
@@ -690,6 +707,10 @@ export function amount(raw) {
690
707
  };
691
708
  return wrapKey(o, meta);
692
709
  }
710
+ const MONEY_ROLES = [
711
+ { role: 'value', accepts: ['number'], fallback: () => real({}) },
712
+ { role: 'currency', accepts: ['select', 'relation'], fallback: () => select({ options: CURRENCY_CODES }) },
713
+ ];
693
714
  export function money(raw) {
694
715
  const o = normalizeOpts(raw);
695
716
  const required = o.required ?? false;
@@ -699,19 +720,23 @@ export function money(raw) {
699
720
  numSchema = numSchema.min(cfg.min);
700
721
  if (cfg.max != null)
701
722
  numSchema = numSchema.max(cfg.max);
702
- const rowSchema = z.object({
723
+ const parts = resolveParts(MONEY_ROLES, o.parts);
724
+ const rowSchema = z.object(partsRowShape({
703
725
  value: numSchema,
704
726
  currency: z.string().refine(isCurrencyCode, { message: vmsg('money_currency') }),
705
- });
706
- const s = z.unknown().superRefine((raw, ctx) => {
727
+ }, parts));
728
+ const s = z.unknown().transform((raw, ctx) => {
707
729
  const parsed = jsonValue(raw);
708
730
  if (typeof parsed === 'string') {
709
731
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
710
- return;
732
+ return z.NEVER;
711
733
  }
712
734
  const r = rowSchema.safeParse(parsed);
713
- if (!r.success)
735
+ if (!r.success) {
714
736
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
737
+ return z.NEVER;
738
+ }
739
+ return stripServerOwnedParts(r.data, parts);
715
740
  });
716
741
  const meta = {
717
742
  kind: 'money',
@@ -724,24 +749,34 @@ export function money(raw) {
724
749
  ...(cfg.min != null && { min: cfg.min }),
725
750
  ...(cfg.max != null && { max: cfg.max }),
726
751
  },
727
- columns: { value: 'real', currency: 'text' },
752
+ columns: partColumns(parts),
753
+ parts,
728
754
  zod: optionalize(s, required),
729
755
  };
730
756
  return wrapKey(o, meta);
731
757
  }
758
+ const CODE_ROLES = [
759
+ { role: 'code', accepts: ['text'], fallback: () => string({}) },
760
+ { role: 'lang', accepts: ['text'], fallback: () => string({}) },
761
+ ];
732
762
  export function code(raw) {
733
763
  const o = normalizeOpts(raw);
734
764
  const required = o.required ?? false;
735
765
  const cfg = o.config ?? {};
736
- const rowSchema = z.object({ code: z.string(), lang: z.string() });
737
- const s = z.unknown().superRefine((raw, ctx) => {
766
+ const parts = resolveParts(CODE_ROLES, o.parts);
767
+ const rowSchema = z.object(partsRowShape({ code: z.string(), lang: z.string() }, parts));
768
+ const s = z.unknown().transform((raw, ctx) => {
738
769
  const parsed = jsonValue(raw);
739
770
  if (typeof parsed === 'string') {
740
771
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
741
- return;
772
+ return z.NEVER;
742
773
  }
743
- if (!rowSchema.safeParse(parsed).success)
774
+ const r = rowSchema.safeParse(parsed);
775
+ if (!r.success) {
744
776
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
777
+ return z.NEVER;
778
+ }
779
+ return stripServerOwnedParts(r.data, parts);
745
780
  });
746
781
  const base = {
747
782
  kind: 'code',
@@ -750,33 +785,49 @@ export function code(raw) {
750
785
  prim: 'code',
751
786
  column: 'text',
752
787
  hints: { ...(cfg.defaultLang && { defaultLang: cfg.defaultLang }) },
753
- columns: { code: 'text', lang: 'text' },
788
+ columns: partColumns(parts),
789
+ parts,
754
790
  zod: optionalize(s, required),
755
791
  };
756
792
  return wrapKey(o, base);
757
793
  }
794
+ const GEO_ROLES = [
795
+ { role: 'lat', accepts: ['number'], fallback: () => real({}) },
796
+ { role: 'lng', accepts: ['number'], fallback: () => real({}) },
797
+ { role: 'label', accepts: ['text'], fallback: () => string({}) },
798
+ ];
758
799
  export function geo(raw) {
759
800
  const o = normalizeOpts(raw);
760
801
  const required = o.required ?? false;
761
- const rowSchema = z.object({
802
+ const parts = resolveParts(GEO_ROLES, o.parts);
803
+ const rowSchema = z.object(partsRowShape({
762
804
  lat: z.number().min(-90).max(90),
763
805
  lng: z.number().min(-180).max(180),
764
806
  label: z.string().nullish(),
765
- });
766
- const s = z.unknown().superRefine((raw, ctx) => {
807
+ }, parts));
808
+ const clientCoordKeys = parts
809
+ .filter((p) => (p.role === 'lat' || p.role === 'lng') && p.mode === 'stored')
810
+ .map(partValueKey);
811
+ const s = z.unknown().transform((raw, ctx) => {
767
812
  const parsed = jsonValue(raw);
768
813
  const p = parsed;
769
- if (p == null || (typeof p === 'object' && p.lat == null && p.lng == null)) {
814
+ const noCoords = p == null ||
815
+ (typeof p === 'object' && clientCoordKeys.length > 0 && clientCoordKeys.every((k) => p[k] == null));
816
+ if (noCoords) {
770
817
  if (required)
771
818
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('required') });
772
- return;
819
+ return raw;
773
820
  }
774
821
  if (typeof parsed === 'string') {
775
822
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
776
- return;
823
+ return z.NEVER;
777
824
  }
778
- if (!rowSchema.safeParse(parsed).success)
825
+ const r = rowSchema.safeParse(parsed);
826
+ if (!r.success) {
779
827
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
828
+ return z.NEVER;
829
+ }
830
+ return stripServerOwnedParts(r.data, parts);
780
831
  });
781
832
  const base = {
782
833
  kind: 'geo',
@@ -785,7 +836,8 @@ export function geo(raw) {
785
836
  prim: 'geo',
786
837
  column: 'text',
787
838
  hints: {},
788
- columns: { lat: 'real', lng: 'real', label: 'text' },
839
+ columns: partColumns(parts),
840
+ parts,
789
841
  zod: optionalize(s, required),
790
842
  };
791
843
  return wrapKey(o, base);
@@ -817,8 +869,20 @@ export function lookup(raw) {
817
869
  return wrapKey(o, meta);
818
870
  }
819
871
  export function clientToMeta(fc) {
820
- return { ...fc, column: 'text', zod: z.any() };
821
- }
872
+ const { parts: _parts, ...rest } = fc;
873
+ return { ...rest, column: 'text', zod: z.any() };
874
+ }
875
+ function endpointReader(parts, keys) {
876
+ const pinned = new Map();
877
+ for (const p of parts)
878
+ if (p.mode === 'pinned')
879
+ pinned.set(p.role, p.value);
880
+ return (role, stored) => (pinned.has(role) ? pinned.get(role) : stored?.[keys[role]]);
881
+ }
882
+ const PERIOD_ROLES = (mkSub) => [
883
+ { role: 'from', accepts: ['date', 'datetime'], fallback: () => mkSub('from', 'core.period.from').type },
884
+ { role: 'until', accepts: ['date', 'datetime'], fallback: () => mkSub('until', 'core.period.until').type },
885
+ ];
822
886
  export function period(raw) {
823
887
  const o = normalizeOpts(raw);
824
888
  const granularity = (o.config?.granularity ?? 'day');
@@ -839,9 +903,13 @@ export function period(raw) {
839
903
  const fromField = mkSub('from', 'core.period.from');
840
904
  const untilField = mkSub('until', 'core.period.until');
841
905
  const subDefs = { from: fromField.type, until: untilField.type };
842
- const rowShape = {};
906
+ const parts = resolveParts(PERIOD_ROLES(mkSub), o.parts);
907
+ const periodKeys = partValueKeys(parts);
908
+ const endpoint = endpointReader(parts, periodKeys);
909
+ const defaultShape = {};
843
910
  for (const [key, fm] of Object.entries(subDefs))
844
- rowShape[key] = fm.zod.optional();
911
+ defaultShape[key] = fm.zod.optional();
912
+ const rowShape = partsRowShape(defaultShape, parts);
845
913
  let s = z.object(rowShape).passthrough();
846
914
  s = s.superRefine((val, ctx) => {
847
915
  if (val == null)
@@ -857,16 +925,18 @@ export function period(raw) {
857
925
  }
858
926
  const items = Array.isArray(parsed) ? parsed : [parsed];
859
927
  for (const item of items) {
860
- const r = item;
861
- if (r?.from && r?.until && String(r.from) > String(r.until))
928
+ const obj = item;
929
+ const r = obj != null && typeof obj === 'object' && !Array.isArray(obj) ? stripServerOwnedParts(obj, parts) : obj;
930
+ const f = endpoint('from', r);
931
+ const u = endpoint('until', r);
932
+ if (f && u && String(f) > String(u))
862
933
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('period_order') });
863
934
  }
864
935
  });
865
- const colType = granularity === 'datetime' ? 'datetime' : granularity === 'day' ? 'date' : 'text';
866
- const subFields = [
867
- { key: 'from', ...toClient(fromField.type) },
868
- { key: 'until', ...toClient(untilField.type) },
869
- ];
936
+ s = s.transform((val) => val != null && typeof val === 'object' && !Array.isArray(val)
937
+ ? stripServerOwnedParts(val, parts)
938
+ : val);
939
+ const subFields = parts.map((p) => ({ key: partValueKey(p), ...toClient(p.meta) }));
870
940
  const meta = {
871
941
  kind: 'period',
872
942
  label: o.label ?? '',
@@ -874,7 +944,8 @@ export function period(raw) {
874
944
  prim: 'period',
875
945
  column: 'text',
876
946
  hints: { fields: subFields, rowMultiple: false, unique: [], granularity },
877
- columns: { from: colType, until: colType },
947
+ columns: partColumns(parts),
948
+ parts,
878
949
  zod: optionalize(s, required),
879
950
  };
880
951
  return wrapKey(o, meta);
@@ -978,35 +1049,46 @@ export function keyValue(raw) {
978
1049
  required: o.required,
979
1050
  });
980
1051
  }
981
- function makeRange(kind, int, raw) {
1052
+ const RANGE_ROLES = (isInt) => [
1053
+ { role: 'from', accepts: ['number'], fallback: () => (isInt ? int({}) : real({})) },
1054
+ { role: 'to', accepts: ['number'], fallback: () => (isInt ? int({}) : real({})) },
1055
+ ];
1056
+ function makeRange(kind, isInt, raw) {
982
1057
  const o = normalizeOpts(raw);
983
1058
  const required = o.required ?? false;
984
1059
  const cfg = o.config ?? {};
985
1060
  let n = z.number().finite();
986
- if (int)
1061
+ if (isInt)
987
1062
  n = n.int({ message: vmsg('int') });
988
1063
  if (cfg.min != null)
989
1064
  n = n.min(cfg.min);
990
1065
  if (cfg.max != null)
991
1066
  n = n.max(cfg.max);
992
- const rowSchema = z.object({ from: n, to: n });
993
- const s = z.unknown().superRefine((raw, ctx) => {
1067
+ const parts = resolveParts(RANGE_ROLES(isInt), o.parts);
1068
+ const rangeKeys = partValueKeys(parts);
1069
+ const rowSchema = z.object(partsRowShape({ from: n, to: n }, parts));
1070
+ const endpoint = endpointReader(parts, rangeKeys);
1071
+ const s = z.unknown().transform((raw, ctx) => {
994
1072
  if (typeof raw === 'string') {
995
1073
  try {
996
1074
  JSON.parse(raw);
997
1075
  }
998
1076
  catch {
999
1077
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
1000
- return;
1078
+ return z.NEVER;
1001
1079
  }
1002
1080
  }
1003
1081
  const r = rowSchema.safeParse(jsonValue(raw));
1004
1082
  if (!r.success) {
1005
1083
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_structure') });
1006
- return;
1084
+ return z.NEVER;
1007
1085
  }
1008
- if (r.data.from > r.data.to)
1086
+ const stored = stripServerOwnedParts(r.data, parts);
1087
+ const fromV = endpoint('from', stored);
1088
+ const toV = endpoint('to', stored);
1089
+ if (fromV != null && toV != null && fromV > toV)
1009
1090
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
1091
+ return stored;
1010
1092
  });
1011
1093
  const base = {
1012
1094
  kind,
@@ -1019,7 +1101,8 @@ function makeRange(kind, int, raw) {
1019
1101
  ...(cfg.max != null && { max: cfg.max }),
1020
1102
  ...(cfg.step != null && { step: cfg.step }),
1021
1103
  },
1022
- columns: int ? { from: 'integer', to: 'integer' } : { from: 'real', to: 'real' },
1104
+ columns: partColumns(parts),
1105
+ parts,
1023
1106
  zod: optionalize(s, required),
1024
1107
  };
1025
1108
  return wrapKey(o, base);
@@ -1122,6 +1205,24 @@ export const field = new Proxy({}, {
1122
1205
  has: (_t, k) => k in composedField(),
1123
1206
  });
1124
1207
  export function toClient(field) {
1125
- const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived } = field;
1126
- return { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived };
1208
+ const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived, parts } = field;
1209
+ return {
1210
+ kind,
1211
+ label,
1212
+ required,
1213
+ prim,
1214
+ hints,
1215
+ options,
1216
+ relation,
1217
+ virtual,
1218
+ json,
1219
+ hidden,
1220
+ derived,
1221
+ parts: parts?.map((p) => ({
1222
+ role: p.role,
1223
+ key: p.key,
1224
+ mode: p.mode,
1225
+ ...(typeof p.value !== 'function' && p.value !== undefined && { value: p.value }),
1226
+ })),
1227
+ };
1127
1228
  }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { type ColumnType, type FieldItem, type FieldMeta, type StaticEl } from './fields.ts';
3
+ export interface RoleDef {
4
+ role: string;
5
+ accepts: string[];
6
+ fallback: () => FieldMeta;
7
+ }
8
+ export type PartInput = FieldItem | StaticEl | FieldMeta;
9
+ export type PartsSpec = Record<string, PartInput>;
10
+ export type PartMode = 'stored' | 'pinned' | 'computed' | 'computedStored';
11
+ export interface ResolvedPart {
12
+ role: string;
13
+ key: string | null;
14
+ meta: FieldMeta;
15
+ mode: PartMode;
16
+ overridden: boolean;
17
+ value?: unknown;
18
+ }
19
+ export declare function partValueKey(p: PartSlot): string;
20
+ export interface PartSlot {
21
+ role: string;
22
+ key: string | null;
23
+ }
24
+ export declare function partValueKeys(parts: PartSlot[]): Record<string, string>;
25
+ export declare function resolveParts(roles: RoleDef[], spec: PartsSpec | undefined): ResolvedPart[];
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>;
28
+ export declare function stripServerOwnedParts(value: Record<string, unknown>, parts: ResolvedPart[]): Record<string, unknown>;
package/dist/parts.js ADDED
@@ -0,0 +1,106 @@
1
+ import { z } from 'zod';
2
+ import { isField, isStatic } from "./fields.js";
3
+ export function partValueKey(p) {
4
+ return p.key ?? p.role;
5
+ }
6
+ export function partValueKeys(parts) {
7
+ const out = {};
8
+ for (const p of parts)
9
+ out[p.role] = partValueKey(p);
10
+ return out;
11
+ }
12
+ const NUMERIC_LIKE = /^\d+$/;
13
+ function readInput(input) {
14
+ if (isField(input)) {
15
+ const it = input;
16
+ return { key: it.key, meta: it.type, value: it.type.compute };
17
+ }
18
+ if (isStatic(input)) {
19
+ const el = input;
20
+ return { key: null, meta: el.type, value: el.value };
21
+ }
22
+ return { key: null, meta: input, value: undefined };
23
+ }
24
+ function modeOf(key, value) {
25
+ if (key && value !== undefined)
26
+ return 'computedStored';
27
+ if (key)
28
+ return 'stored';
29
+ return typeof value === 'function' ? 'computed' : 'pinned';
30
+ }
31
+ export function resolveParts(roles, spec) {
32
+ const known = new Set(roles.map((r) => r.role));
33
+ for (const name of Object.keys(spec ?? {})) {
34
+ if (NUMERIC_LIKE.test(name))
35
+ throw new Error(`parts: numeric-like role name '${name}' (object iteration would reorder the parts)`);
36
+ if (!known.has(name))
37
+ throw new Error(`parts: unknown role '${name}' (roles: ${[...known].join(', ')})`);
38
+ }
39
+ const out = [];
40
+ const usedKeys = new Set();
41
+ for (const def of roles) {
42
+ const input = spec?.[def.role];
43
+ const { key, meta, value } = input ? readInput(input) : { key: def.role, meta: def.fallback(), value: undefined };
44
+ if (!def.accepts.includes(meta.prim))
45
+ throw new Error(`parts: role '${def.role}' does not accept prim '${meta.prim}' (accepts: ${def.accepts.join(', ')})`);
46
+ const storageKey = key ?? (value === undefined ? def.role : null);
47
+ const mode = modeOf(storageKey, value);
48
+ if (storageKey && storageKey !== def.role && known.has(storageKey))
49
+ throw new Error(`parts: role '${def.role}' filling declares key '${storageKey}', which is another role of this composite — ` +
50
+ `a stored part's key must not collide with a role name (an unstored role occupies its role name in the ` +
51
+ `value object).`);
52
+ if (storageKey) {
53
+ if (usedKeys.has(storageKey))
54
+ throw new Error(`parts: duplicate storage key '${storageKey}'`);
55
+ usedKeys.add(storageKey);
56
+ }
57
+ out.push({
58
+ role: def.role,
59
+ key: storageKey,
60
+ meta,
61
+ mode,
62
+ overridden: input !== undefined,
63
+ ...(value !== undefined && { value }),
64
+ });
65
+ }
66
+ return out;
67
+ }
68
+ export function partColumns(parts) {
69
+ const cols = {};
70
+ for (const p of parts)
71
+ if (p.key)
72
+ cols[p.key] = p.meta.column;
73
+ return cols;
74
+ }
75
+ export function partsRowShape(shape, parts) {
76
+ const out = { ...shape };
77
+ for (const p of parts)
78
+ if (partValueKey(p) !== p.role)
79
+ delete out[p.role];
80
+ for (const p of parts) {
81
+ if (!p.key)
82
+ out[p.role] = z.unknown().optional();
83
+ else if (p.mode === 'computedStored')
84
+ out[p.key] = z.unknown().optional();
85
+ else
86
+ out[p.key] = p.overridden ? p.meta.zod : (shape[p.role] ?? z.unknown().optional());
87
+ }
88
+ return out;
89
+ }
90
+ function serverOwnedSlot(p) {
91
+ if (!p.key)
92
+ return p.role;
93
+ return p.mode === 'computedStored' ? p.key : null;
94
+ }
95
+ export function stripServerOwnedParts(value, parts) {
96
+ let out = value;
97
+ for (const p of parts) {
98
+ const slot = serverOwnedSlot(p);
99
+ if (slot != null && slot in out) {
100
+ if (out === value)
101
+ out = { ...value };
102
+ delete out[slot];
103
+ }
104
+ }
105
+ return out;
106
+ }
package/dist/shelf.js CHANGED
@@ -3,7 +3,7 @@ import { toClient, isField, isGroup, isStatic, isCollectionGroup, isEmbeddedGrou
3
3
  import { resolveFlag } from "./condition.js";
4
4
  import { parseUrl } from "./fields/url.js";
5
5
  import { vmsg } from "./fields/validation.js";
6
- import { derivedEntries, derivableKeys } from "./derive.js";
6
+ import { derivableKeys, legacyDeriveDeps } from "./derive.js";
7
7
  export function fieldEntries(items) {
8
8
  const out = [];
9
9
  for (const it of items) {
@@ -65,6 +65,14 @@ export function fieldMap(items) {
65
65
  }
66
66
  return m;
67
67
  }
68
+ function assertNoKeylessCompute(items, m) {
69
+ for (const it of items) {
70
+ 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`);
72
+ if (isGroup(it))
73
+ assertNoKeylessCompute(it.fields, m);
74
+ }
75
+ }
68
76
  export function defineShelf(m) {
69
77
  const keys = fieldEntries(m.fields).map(([k]) => k);
70
78
  const dup = keys.find((k, i) => keys.indexOf(k) !== i);
@@ -86,9 +94,10 @@ export function defineShelf(m) {
86
94
  console.warn(`[shelf] ${m.library}/${m.shelf}: ${titles.length} title fields (${titles.map(([k]) => k).join(', ')}) — '${titles[0][0]}' wins`);
87
95
  if (m.standalone !== false && !m.single && titleKey(m) === 'id')
88
96
  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);
89
98
  const known = derivableKeys(m.fields);
90
- for (const [key, spec] of derivedEntries(m.fields)) {
91
- for (const dep of spec.deps) {
99
+ for (const [key, deps] of legacyDeriveDeps(m.fields)) {
100
+ for (const dep of deps) {
92
101
  if (!known.has(dep))
93
102
  throw new Error(`[defineShelf] ${m.library}/${m.shelf}: derive on '${key}' depends on unknown key '${dep}'`);
94
103
  }
@@ -10,6 +10,7 @@ export interface ShowcaseDef {
10
10
  };
11
11
  filter: Condition;
12
12
  columns: string[];
13
+ defaultGroupBy?: string;
13
14
  createDefaults?: Record<string, unknown>;
14
15
  }
15
16
  export declare function defineShowcase(s: ShowcaseDef): ShowcaseDef;
package/dist/showcase.js CHANGED
@@ -1,5 +1,9 @@
1
- import { COMPILABLE_SCALAR_OPS, COMPILABLE_ARRAY_OPS } from "./condition.js";
2
- const COMPILABLE_OPS = new Set([...COMPILABLE_SCALAR_OPS, ...COMPILABLE_ARRAY_OPS]);
1
+ import { COMPILABLE_SCALAR_OPS, COMPILABLE_ARRAY_OPS, COMPILABLE_MEMBER_OPS } from "./condition.js";
2
+ const COMPILABLE_OPS = new Set([
3
+ ...COMPILABLE_SCALAR_OPS,
4
+ ...COMPILABLE_ARRAY_OPS,
5
+ ...COMPILABLE_MEMBER_OPS,
6
+ ]);
3
7
  function assertCompilableOps(cond, id) {
4
8
  for (const [key, spec] of Object.entries(cond)) {
5
9
  if (spec === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/sdk",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"