@coffer-org/sdk 3.2.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/derive.d.ts +3 -2
- package/dist/derive.js +24 -12
- package/dist/extend.js +2 -1
- package/dist/fields/normalize.d.ts +1 -0
- package/dist/fields.d.ts +32 -22
- package/dist/fields.js +158 -65
- package/dist/parts.d.ts +33 -0
- package/dist/parts.js +120 -0
- package/dist/shelf.d.ts +2 -0
- package/dist/shelf.js +50 -6
- package/dist/showcase.d.ts +1 -0
- package/package.json +1 -1
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,
|
|
11
|
-
export declare function
|
|
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
|
-
|
|
2
|
+
function topLevelFields(fields) {
|
|
3
3
|
const out = [];
|
|
4
4
|
for (const it of fields) {
|
|
5
|
-
if (isField(it))
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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,
|
|
30
|
+
for (const [key, fn] of derivedEntries(fields)) {
|
|
19
31
|
try {
|
|
20
|
-
out[key] =
|
|
32
|
+
out[key] = await fn({ record });
|
|
21
33
|
}
|
|
22
34
|
catch (e) {
|
|
23
35
|
console.warn(`[derive] '${key}' threw`, e);
|
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) {
|
package/dist/fields.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ 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';
|
|
8
10
|
import { presets } from './field-presets.ts';
|
|
9
|
-
export type CorePrim = 'text' | 'number' | 'date' | 'time' | 'datetime' | 'checkbox' | 'select' | 'json' | 'check' | 'measured' | 'relation' | 'file' | 'per-weekday'
|
|
11
|
+
export type CorePrim = 'text' | 'number' | 'date' | 'time' | 'datetime' | 'checkbox' | 'select' | 'json' | 'check' | 'measured' | 'relation' | 'file' | 'per-weekday';
|
|
10
12
|
export type Prim = CorePrim | (string & {});
|
|
11
13
|
export type ColumnType = 'text' | 'integer' | 'real' | 'boolean' | 'date' | 'time' | 'datetime';
|
|
12
14
|
export type OptionIcon = {
|
|
@@ -45,6 +47,9 @@ export interface ComputeCtx {
|
|
|
45
47
|
library: string;
|
|
46
48
|
shelf: string;
|
|
47
49
|
};
|
|
50
|
+
self?: Record<string, unknown>;
|
|
51
|
+
parent?: Record<string, unknown>;
|
|
52
|
+
ref?: (key: string) => Promise<Record<string, unknown> | null>;
|
|
48
53
|
}
|
|
49
54
|
export type ComputeFn = (ctx: ComputeCtx) => unknown;
|
|
50
55
|
export interface DividerEl {
|
|
@@ -104,6 +109,12 @@ export interface FieldClient {
|
|
|
104
109
|
json?: true;
|
|
105
110
|
derived?: true;
|
|
106
111
|
columns?: Record<string, ColumnType>;
|
|
112
|
+
parts?: {
|
|
113
|
+
role: string;
|
|
114
|
+
key: string | null;
|
|
115
|
+
mode: string;
|
|
116
|
+
value?: unknown;
|
|
117
|
+
}[];
|
|
107
118
|
hidden?: boolean | Condition;
|
|
108
119
|
}
|
|
109
120
|
export declare function group(o: {
|
|
@@ -175,6 +186,7 @@ export type FieldOptions = string | unknown[];
|
|
|
175
186
|
export interface FieldCoreOpts {
|
|
176
187
|
key?: string;
|
|
177
188
|
value?: unknown;
|
|
189
|
+
store?: boolean;
|
|
178
190
|
label?: string;
|
|
179
191
|
rules?: Rules;
|
|
180
192
|
view?: View;
|
|
@@ -195,12 +207,15 @@ export declare function wrapKey(opts: {
|
|
|
195
207
|
emphasis?: 'hero' | 'muted';
|
|
196
208
|
noLabel?: boolean;
|
|
197
209
|
role?: 'cover' | 'avatar';
|
|
210
|
+
store?: boolean;
|
|
198
211
|
derive?: import('./derive.ts').DeriveSpec;
|
|
199
212
|
}, meta: FieldMeta): FieldItem | StaticEl | FieldMeta;
|
|
200
213
|
export interface FieldMeta extends FieldClient {
|
|
201
214
|
column: ColumnType;
|
|
202
215
|
zod: z.ZodTypeAny;
|
|
203
216
|
derive?: import('./derive.ts').DeriveSpec;
|
|
217
|
+
compute?: unknown;
|
|
218
|
+
parts?: ResolvedPart[];
|
|
204
219
|
}
|
|
205
220
|
export declare function isJsonStored(field: FieldMeta | FieldClient): boolean;
|
|
206
221
|
export { LANGUAGES, SEX_OPTIONS, WEEKDAY_OPTIONS } from './fields/constants.ts';
|
|
@@ -423,6 +438,7 @@ export declare function check(o: CheckOpts & {
|
|
|
423
438
|
}): FieldMeta;
|
|
424
439
|
export interface MeasuredOpts extends FieldCoreOpts {
|
|
425
440
|
options: FieldOptions;
|
|
441
|
+
parts?: PartsSpec;
|
|
426
442
|
}
|
|
427
443
|
export declare function measured(o: MeasuredOpts & {
|
|
428
444
|
key: string;
|
|
@@ -456,7 +472,9 @@ export declare function amount(o: AmountOpts & {
|
|
|
456
472
|
key?: undefined;
|
|
457
473
|
value?: undefined;
|
|
458
474
|
}): FieldMeta;
|
|
459
|
-
export
|
|
475
|
+
export interface MoneyOpts extends FieldCoreOpts {
|
|
476
|
+
parts?: PartsSpec;
|
|
477
|
+
}
|
|
460
478
|
export declare function money(o: MoneyOpts & {
|
|
461
479
|
key: string;
|
|
462
480
|
}): FieldItem;
|
|
@@ -468,7 +486,9 @@ export declare function money(o: MoneyOpts & {
|
|
|
468
486
|
key?: undefined;
|
|
469
487
|
value?: undefined;
|
|
470
488
|
}): FieldMeta;
|
|
471
|
-
export
|
|
489
|
+
export interface CodeFieldOpts extends FieldCoreOpts {
|
|
490
|
+
parts?: PartsSpec;
|
|
491
|
+
}
|
|
472
492
|
export declare function code(o: CodeFieldOpts & {
|
|
473
493
|
key: string;
|
|
474
494
|
}): FieldItem;
|
|
@@ -480,7 +500,9 @@ export declare function code(o: CodeFieldOpts & {
|
|
|
480
500
|
key?: undefined;
|
|
481
501
|
value?: undefined;
|
|
482
502
|
}): FieldMeta;
|
|
483
|
-
export
|
|
503
|
+
export interface GeoOpts extends FieldCoreOpts {
|
|
504
|
+
parts?: PartsSpec;
|
|
505
|
+
}
|
|
484
506
|
export declare function geo(o: GeoOpts & {
|
|
485
507
|
key: string;
|
|
486
508
|
}): FieldItem;
|
|
@@ -504,23 +526,10 @@ export declare function perWeekday(o: {
|
|
|
504
526
|
export type RowSubField = FieldClient & {
|
|
505
527
|
key: string;
|
|
506
528
|
};
|
|
507
|
-
export interface LookupOpts extends FieldCoreOpts {
|
|
508
|
-
from: string;
|
|
509
|
-
pick: string[];
|
|
510
|
-
}
|
|
511
|
-
export declare function lookup(o: LookupOpts & {
|
|
512
|
-
key: string;
|
|
513
|
-
}): FieldItem;
|
|
514
|
-
export declare function lookup(o: LookupOpts & {
|
|
515
|
-
value: unknown;
|
|
516
|
-
key?: undefined;
|
|
517
|
-
}): StaticEl;
|
|
518
|
-
export declare function lookup(o: LookupOpts & {
|
|
519
|
-
key?: undefined;
|
|
520
|
-
value?: undefined;
|
|
521
|
-
}): FieldMeta;
|
|
522
529
|
export declare function clientToMeta(fc: FieldClient): FieldMeta;
|
|
523
|
-
|
|
530
|
+
interface PeriodOpts extends FieldCoreOpts {
|
|
531
|
+
parts?: PartsSpec;
|
|
532
|
+
}
|
|
524
533
|
export declare function period(o: PeriodOpts & {
|
|
525
534
|
key: string;
|
|
526
535
|
}): FieldItem | GroupEl;
|
|
@@ -642,7 +651,9 @@ export interface KeyValueOpts extends FieldCoreOpts {
|
|
|
642
651
|
export declare function keyValue(raw: KeyValueOpts & {
|
|
643
652
|
key: string;
|
|
644
653
|
}): GroupEl;
|
|
645
|
-
export
|
|
654
|
+
export interface RangeOpts extends FieldCoreOpts {
|
|
655
|
+
parts?: PartsSpec;
|
|
656
|
+
}
|
|
646
657
|
export declare function numberRange(o: RangeOpts & {
|
|
647
658
|
key: string;
|
|
648
659
|
}): FieldItem;
|
|
@@ -701,7 +712,6 @@ declare const PRIMITIVES: {
|
|
|
701
712
|
money: typeof money;
|
|
702
713
|
code: typeof code;
|
|
703
714
|
geo: typeof geo;
|
|
704
|
-
lookup: typeof lookup;
|
|
705
715
|
perWeekday: typeof perWeekday;
|
|
706
716
|
period: typeof period;
|
|
707
717
|
file: typeof file;
|
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,25 @@ 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
|
+
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) {
|
|
146
|
+
m = { ...m, compute: opts.value, derived: true, hints: { ...m.hints, noEditControl: true } };
|
|
147
|
+
if (opts.store === false)
|
|
148
|
+
m = { ...m, virtual: true };
|
|
149
|
+
return { key: opts.key, type: m };
|
|
150
|
+
}
|
|
132
151
|
if (opts.key)
|
|
133
152
|
return { key: opts.key, type: m };
|
|
134
153
|
if ('value' in opts && opts.value !== undefined)
|
|
@@ -595,6 +614,10 @@ export function check(raw) {
|
|
|
595
614
|
};
|
|
596
615
|
return wrapKey(o, meta);
|
|
597
616
|
}
|
|
617
|
+
const MEASURED_ROLES = (units) => [
|
|
618
|
+
{ role: 'value', accepts: ['number'], fallback: () => real({}) },
|
|
619
|
+
{ role: 'unit', accepts: ['select', 'relation'], fallback: () => select({ options: units }) },
|
|
620
|
+
];
|
|
598
621
|
export function measured(raw) {
|
|
599
622
|
const o = normalizeOpts(raw);
|
|
600
623
|
const required = o.required ?? false;
|
|
@@ -608,15 +631,16 @@ export function measured(raw) {
|
|
|
608
631
|
numSchema = numSchema.min(cfg.min);
|
|
609
632
|
if (cfg.max != null)
|
|
610
633
|
numSchema = numSchema.max(cfg.max);
|
|
611
|
-
const
|
|
634
|
+
const parts = resolveParts(MEASURED_ROLES(o.options), o.parts);
|
|
635
|
+
const rowSchema = z.object(partsRowShape({
|
|
612
636
|
value: numSchema,
|
|
613
637
|
unit: z.string().refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') }),
|
|
614
|
-
});
|
|
615
|
-
const s = z.unknown().
|
|
638
|
+
}, parts, required !== true));
|
|
639
|
+
const s = z.unknown().transform((raw, ctx) => {
|
|
616
640
|
const parsed = jsonValue(raw);
|
|
617
641
|
if (typeof parsed === 'string') {
|
|
618
642
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
619
|
-
return;
|
|
643
|
+
return z.NEVER;
|
|
620
644
|
}
|
|
621
645
|
const r = rowSchema.safeParse(parsed);
|
|
622
646
|
if (!r.success) {
|
|
@@ -632,7 +656,9 @@ export function measured(raw) {
|
|
|
632
656
|
code: z.ZodIssueCode.custom,
|
|
633
657
|
message: vmsgIssue ? vmsgIssue.message : vmsg('measured_structure'),
|
|
634
658
|
});
|
|
659
|
+
return z.NEVER;
|
|
635
660
|
}
|
|
661
|
+
return stripServerOwnedParts(r.data, parts);
|
|
636
662
|
});
|
|
637
663
|
const meta = {
|
|
638
664
|
kind: 'measured',
|
|
@@ -646,7 +672,8 @@ export function measured(raw) {
|
|
|
646
672
|
...(cfg.max != null && { max: cfg.max }),
|
|
647
673
|
...(cfg.step != null && { step: cfg.step }),
|
|
648
674
|
},
|
|
649
|
-
columns:
|
|
675
|
+
columns: partColumns(parts),
|
|
676
|
+
parts,
|
|
650
677
|
zod: optionalize(s, required),
|
|
651
678
|
};
|
|
652
679
|
return wrapKey(o, meta);
|
|
@@ -655,9 +682,7 @@ export function unit(raw) {
|
|
|
655
682
|
const o = normalizeOpts(raw);
|
|
656
683
|
const required = o.required ?? false;
|
|
657
684
|
const units = resolveUnits(o.options);
|
|
658
|
-
const s = z
|
|
659
|
-
.string(reqErr())
|
|
660
|
-
.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') });
|
|
661
686
|
const meta = {
|
|
662
687
|
kind: 'unit',
|
|
663
688
|
label: o.label ?? '',
|
|
@@ -690,6 +715,10 @@ export function amount(raw) {
|
|
|
690
715
|
};
|
|
691
716
|
return wrapKey(o, meta);
|
|
692
717
|
}
|
|
718
|
+
const MONEY_ROLES = [
|
|
719
|
+
{ role: 'value', accepts: ['number'], fallback: () => real({}) },
|
|
720
|
+
{ role: 'currency', accepts: ['select', 'relation'], fallback: () => select({ options: CURRENCY_CODES }) },
|
|
721
|
+
];
|
|
693
722
|
export function money(raw) {
|
|
694
723
|
const o = normalizeOpts(raw);
|
|
695
724
|
const required = o.required ?? false;
|
|
@@ -699,19 +728,23 @@ export function money(raw) {
|
|
|
699
728
|
numSchema = numSchema.min(cfg.min);
|
|
700
729
|
if (cfg.max != null)
|
|
701
730
|
numSchema = numSchema.max(cfg.max);
|
|
702
|
-
const
|
|
731
|
+
const parts = resolveParts(MONEY_ROLES, o.parts);
|
|
732
|
+
const rowSchema = z.object(partsRowShape({
|
|
703
733
|
value: numSchema,
|
|
704
734
|
currency: z.string().refine(isCurrencyCode, { message: vmsg('money_currency') }),
|
|
705
|
-
});
|
|
706
|
-
const s = z.unknown().
|
|
735
|
+
}, parts, required !== true));
|
|
736
|
+
const s = z.unknown().transform((raw, ctx) => {
|
|
707
737
|
const parsed = jsonValue(raw);
|
|
708
738
|
if (typeof parsed === 'string') {
|
|
709
739
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
710
|
-
return;
|
|
740
|
+
return z.NEVER;
|
|
711
741
|
}
|
|
712
742
|
const r = rowSchema.safeParse(parsed);
|
|
713
|
-
if (!r.success)
|
|
743
|
+
if (!r.success) {
|
|
714
744
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
|
|
745
|
+
return z.NEVER;
|
|
746
|
+
}
|
|
747
|
+
return stripServerOwnedParts(r.data, parts);
|
|
715
748
|
});
|
|
716
749
|
const meta = {
|
|
717
750
|
kind: 'money',
|
|
@@ -724,24 +757,34 @@ export function money(raw) {
|
|
|
724
757
|
...(cfg.min != null && { min: cfg.min }),
|
|
725
758
|
...(cfg.max != null && { max: cfg.max }),
|
|
726
759
|
},
|
|
727
|
-
columns:
|
|
760
|
+
columns: partColumns(parts),
|
|
761
|
+
parts,
|
|
728
762
|
zod: optionalize(s, required),
|
|
729
763
|
};
|
|
730
764
|
return wrapKey(o, meta);
|
|
731
765
|
}
|
|
766
|
+
const CODE_ROLES = [
|
|
767
|
+
{ role: 'code', accepts: ['text'], fallback: () => string({}) },
|
|
768
|
+
{ role: 'lang', accepts: ['text'], fallback: () => string({}) },
|
|
769
|
+
];
|
|
732
770
|
export function code(raw) {
|
|
733
771
|
const o = normalizeOpts(raw);
|
|
734
772
|
const required = o.required ?? false;
|
|
735
773
|
const cfg = o.config ?? {};
|
|
736
|
-
const
|
|
737
|
-
const
|
|
774
|
+
const parts = resolveParts(CODE_ROLES, o.parts);
|
|
775
|
+
const rowSchema = z.object(partsRowShape({ code: z.string(), lang: z.string() }, parts, required !== true));
|
|
776
|
+
const s = z.unknown().transform((raw, ctx) => {
|
|
738
777
|
const parsed = jsonValue(raw);
|
|
739
778
|
if (typeof parsed === 'string') {
|
|
740
779
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
741
|
-
return;
|
|
780
|
+
return z.NEVER;
|
|
742
781
|
}
|
|
743
|
-
|
|
782
|
+
const r = rowSchema.safeParse(parsed);
|
|
783
|
+
if (!r.success) {
|
|
744
784
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
|
|
785
|
+
return z.NEVER;
|
|
786
|
+
}
|
|
787
|
+
return stripServerOwnedParts(r.data, parts);
|
|
745
788
|
});
|
|
746
789
|
const base = {
|
|
747
790
|
kind: 'code',
|
|
@@ -750,33 +793,48 @@ export function code(raw) {
|
|
|
750
793
|
prim: 'code',
|
|
751
794
|
column: 'text',
|
|
752
795
|
hints: { ...(cfg.defaultLang && { defaultLang: cfg.defaultLang }) },
|
|
753
|
-
columns:
|
|
796
|
+
columns: partColumns(parts),
|
|
797
|
+
parts,
|
|
754
798
|
zod: optionalize(s, required),
|
|
755
799
|
};
|
|
756
800
|
return wrapKey(o, base);
|
|
757
801
|
}
|
|
802
|
+
const GEO_ROLES = [
|
|
803
|
+
{ role: 'lat', accepts: ['number'], fallback: () => real({}) },
|
|
804
|
+
{ role: 'lng', accepts: ['number'], fallback: () => real({}) },
|
|
805
|
+
{ role: 'label', accepts: ['text'], fallback: () => string({}) },
|
|
806
|
+
];
|
|
758
807
|
export function geo(raw) {
|
|
759
808
|
const o = normalizeOpts(raw);
|
|
760
809
|
const required = o.required ?? false;
|
|
761
|
-
const
|
|
810
|
+
const parts = resolveParts(GEO_ROLES, o.parts);
|
|
811
|
+
const rowSchema = z.object(partsRowShape({
|
|
762
812
|
lat: z.number().min(-90).max(90),
|
|
763
813
|
lng: z.number().min(-180).max(180),
|
|
764
814
|
label: z.string().nullish(),
|
|
765
|
-
});
|
|
766
|
-
const
|
|
815
|
+
}, parts, required !== true));
|
|
816
|
+
const clientCoordKeys = parts
|
|
817
|
+
.filter((p) => (p.role === 'lat' || p.role === 'lng') && p.mode === 'stored')
|
|
818
|
+
.map(partValueKey);
|
|
819
|
+
const s = z.unknown().transform((raw, ctx) => {
|
|
767
820
|
const parsed = jsonValue(raw);
|
|
768
821
|
const p = parsed;
|
|
769
|
-
|
|
822
|
+
const noCoords = p == null || (typeof p === 'object' && clientCoordKeys.length > 0 && clientCoordKeys.every((k) => p[k] == null));
|
|
823
|
+
if (noCoords) {
|
|
770
824
|
if (required)
|
|
771
825
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('required') });
|
|
772
|
-
return;
|
|
826
|
+
return raw;
|
|
773
827
|
}
|
|
774
828
|
if (typeof parsed === 'string') {
|
|
775
829
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
776
|
-
return;
|
|
830
|
+
return z.NEVER;
|
|
777
831
|
}
|
|
778
|
-
|
|
832
|
+
const r = rowSchema.safeParse(parsed);
|
|
833
|
+
if (!r.success) {
|
|
779
834
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
|
|
835
|
+
return z.NEVER;
|
|
836
|
+
}
|
|
837
|
+
return stripServerOwnedParts(r.data, parts);
|
|
780
838
|
});
|
|
781
839
|
const base = {
|
|
782
840
|
kind: 'geo',
|
|
@@ -785,7 +843,8 @@ export function geo(raw) {
|
|
|
785
843
|
prim: 'geo',
|
|
786
844
|
column: 'text',
|
|
787
845
|
hints: {},
|
|
788
|
-
columns:
|
|
846
|
+
columns: partColumns(parts),
|
|
847
|
+
parts,
|
|
789
848
|
zod: optionalize(s, required),
|
|
790
849
|
};
|
|
791
850
|
return wrapKey(o, base);
|
|
@@ -802,23 +861,21 @@ export function perWeekday(o) {
|
|
|
802
861
|
kind: 'per-weekday',
|
|
803
862
|
};
|
|
804
863
|
}
|
|
805
|
-
export function lookup(raw) {
|
|
806
|
-
const o = normalizeOpts(raw);
|
|
807
|
-
const meta = {
|
|
808
|
-
kind: 'lookup',
|
|
809
|
-
label: o.label ?? '',
|
|
810
|
-
required: false,
|
|
811
|
-
prim: 'lookup',
|
|
812
|
-
column: 'text',
|
|
813
|
-
virtual: true,
|
|
814
|
-
hints: { from: o.from, pick: o.pick, compareWith: o.compareWith ?? null },
|
|
815
|
-
zod: z.any().optional(),
|
|
816
|
-
};
|
|
817
|
-
return wrapKey(o, meta);
|
|
818
|
-
}
|
|
819
864
|
export function clientToMeta(fc) {
|
|
820
|
-
|
|
821
|
-
}
|
|
865
|
+
const { parts: _parts, ...rest } = fc;
|
|
866
|
+
return { ...rest, column: 'text', zod: z.any() };
|
|
867
|
+
}
|
|
868
|
+
function endpointReader(parts, keys) {
|
|
869
|
+
const pinned = new Map();
|
|
870
|
+
for (const p of parts)
|
|
871
|
+
if (p.mode === 'pinned')
|
|
872
|
+
pinned.set(p.role, p.value);
|
|
873
|
+
return (role, stored) => (pinned.has(role) ? pinned.get(role) : stored?.[keys[role]]);
|
|
874
|
+
}
|
|
875
|
+
const PERIOD_ROLES = (mkSub) => [
|
|
876
|
+
{ role: 'from', accepts: ['date', 'datetime'], fallback: () => mkSub('from', 'core.period.from').type },
|
|
877
|
+
{ role: 'until', accepts: ['date', 'datetime'], fallback: () => mkSub('until', 'core.period.until').type },
|
|
878
|
+
];
|
|
822
879
|
export function period(raw) {
|
|
823
880
|
const o = normalizeOpts(raw);
|
|
824
881
|
const granularity = (o.config?.granularity ?? 'day');
|
|
@@ -839,9 +896,13 @@ export function period(raw) {
|
|
|
839
896
|
const fromField = mkSub('from', 'core.period.from');
|
|
840
897
|
const untilField = mkSub('until', 'core.period.until');
|
|
841
898
|
const subDefs = { from: fromField.type, until: untilField.type };
|
|
842
|
-
const
|
|
899
|
+
const parts = resolveParts(PERIOD_ROLES(mkSub), o.parts);
|
|
900
|
+
const periodKeys = partValueKeys(parts);
|
|
901
|
+
const endpoint = endpointReader(parts, periodKeys);
|
|
902
|
+
const defaultShape = {};
|
|
843
903
|
for (const [key, fm] of Object.entries(subDefs))
|
|
844
|
-
|
|
904
|
+
defaultShape[key] = fm.zod.optional();
|
|
905
|
+
const rowShape = partsRowShape(defaultShape, parts);
|
|
845
906
|
let s = z.object(rowShape).passthrough();
|
|
846
907
|
s = s.superRefine((val, ctx) => {
|
|
847
908
|
if (val == null)
|
|
@@ -857,16 +918,18 @@ export function period(raw) {
|
|
|
857
918
|
}
|
|
858
919
|
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
859
920
|
for (const item of items) {
|
|
860
|
-
const
|
|
861
|
-
|
|
921
|
+
const obj = item;
|
|
922
|
+
const r = obj != null && typeof obj === 'object' && !Array.isArray(obj) ? stripServerOwnedParts(obj, parts) : obj;
|
|
923
|
+
const f = endpoint('from', r);
|
|
924
|
+
const u = endpoint('until', r);
|
|
925
|
+
if (f && u && String(f) > String(u))
|
|
862
926
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('period_order') });
|
|
863
927
|
}
|
|
864
928
|
});
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
];
|
|
929
|
+
s = s.transform((val) => val != null && typeof val === 'object' && !Array.isArray(val)
|
|
930
|
+
? stripServerOwnedParts(val, parts)
|
|
931
|
+
: val);
|
|
932
|
+
const subFields = parts.map((p) => ({ key: partValueKey(p), ...toClient(p.meta) }));
|
|
870
933
|
const meta = {
|
|
871
934
|
kind: 'period',
|
|
872
935
|
label: o.label ?? '',
|
|
@@ -874,7 +937,8 @@ export function period(raw) {
|
|
|
874
937
|
prim: 'period',
|
|
875
938
|
column: 'text',
|
|
876
939
|
hints: { fields: subFields, rowMultiple: false, unique: [], granularity },
|
|
877
|
-
columns:
|
|
940
|
+
columns: partColumns(parts),
|
|
941
|
+
parts,
|
|
878
942
|
zod: optionalize(s, required),
|
|
879
943
|
};
|
|
880
944
|
return wrapKey(o, meta);
|
|
@@ -978,35 +1042,46 @@ export function keyValue(raw) {
|
|
|
978
1042
|
required: o.required,
|
|
979
1043
|
});
|
|
980
1044
|
}
|
|
981
|
-
|
|
1045
|
+
const RANGE_ROLES = (isInt) => [
|
|
1046
|
+
{ role: 'from', accepts: ['number'], fallback: () => (isInt ? int({}) : real({})) },
|
|
1047
|
+
{ role: 'to', accepts: ['number'], fallback: () => (isInt ? int({}) : real({})) },
|
|
1048
|
+
];
|
|
1049
|
+
function makeRange(kind, isInt, raw) {
|
|
982
1050
|
const o = normalizeOpts(raw);
|
|
983
1051
|
const required = o.required ?? false;
|
|
984
1052
|
const cfg = o.config ?? {};
|
|
985
1053
|
let n = z.number().finite();
|
|
986
|
-
if (
|
|
1054
|
+
if (isInt)
|
|
987
1055
|
n = n.int({ message: vmsg('int') });
|
|
988
1056
|
if (cfg.min != null)
|
|
989
1057
|
n = n.min(cfg.min);
|
|
990
1058
|
if (cfg.max != null)
|
|
991
1059
|
n = n.max(cfg.max);
|
|
992
|
-
const
|
|
993
|
-
const
|
|
1060
|
+
const parts = resolveParts(RANGE_ROLES(isInt), o.parts);
|
|
1061
|
+
const rangeKeys = partValueKeys(parts);
|
|
1062
|
+
const rowSchema = z.object(partsRowShape({ from: n, to: n }, parts, required !== true));
|
|
1063
|
+
const endpoint = endpointReader(parts, rangeKeys);
|
|
1064
|
+
const s = z.unknown().transform((raw, ctx) => {
|
|
994
1065
|
if (typeof raw === 'string') {
|
|
995
1066
|
try {
|
|
996
1067
|
JSON.parse(raw);
|
|
997
1068
|
}
|
|
998
1069
|
catch {
|
|
999
1070
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
1000
|
-
return;
|
|
1071
|
+
return z.NEVER;
|
|
1001
1072
|
}
|
|
1002
1073
|
}
|
|
1003
1074
|
const r = rowSchema.safeParse(jsonValue(raw));
|
|
1004
1075
|
if (!r.success) {
|
|
1005
1076
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_structure') });
|
|
1006
|
-
return;
|
|
1077
|
+
return z.NEVER;
|
|
1007
1078
|
}
|
|
1008
|
-
|
|
1079
|
+
const stored = stripServerOwnedParts(r.data, parts);
|
|
1080
|
+
const fromV = endpoint('from', stored);
|
|
1081
|
+
const toV = endpoint('to', stored);
|
|
1082
|
+
if (fromV != null && toV != null && fromV > toV)
|
|
1009
1083
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
|
|
1084
|
+
return stored;
|
|
1010
1085
|
});
|
|
1011
1086
|
const base = {
|
|
1012
1087
|
kind,
|
|
@@ -1019,7 +1094,8 @@ function makeRange(kind, int, raw) {
|
|
|
1019
1094
|
...(cfg.max != null && { max: cfg.max }),
|
|
1020
1095
|
...(cfg.step != null && { step: cfg.step }),
|
|
1021
1096
|
},
|
|
1022
|
-
columns:
|
|
1097
|
+
columns: partColumns(parts),
|
|
1098
|
+
parts,
|
|
1023
1099
|
zod: optionalize(s, required),
|
|
1024
1100
|
};
|
|
1025
1101
|
return wrapKey(o, base);
|
|
@@ -1082,7 +1158,6 @@ const PRIMITIVES = {
|
|
|
1082
1158
|
money,
|
|
1083
1159
|
code,
|
|
1084
1160
|
geo,
|
|
1085
|
-
lookup,
|
|
1086
1161
|
perWeekday,
|
|
1087
1162
|
period,
|
|
1088
1163
|
file,
|
|
@@ -1122,6 +1197,24 @@ export const field = new Proxy({}, {
|
|
|
1122
1197
|
has: (_t, k) => k in composedField(),
|
|
1123
1198
|
});
|
|
1124
1199
|
export function toClient(field) {
|
|
1125
|
-
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived } = field;
|
|
1126
|
-
return {
|
|
1200
|
+
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived, parts } = field;
|
|
1201
|
+
return {
|
|
1202
|
+
kind,
|
|
1203
|
+
label,
|
|
1204
|
+
required,
|
|
1205
|
+
prim,
|
|
1206
|
+
hints,
|
|
1207
|
+
options,
|
|
1208
|
+
relation,
|
|
1209
|
+
virtual,
|
|
1210
|
+
json,
|
|
1211
|
+
hidden,
|
|
1212
|
+
derived,
|
|
1213
|
+
parts: parts?.map((p) => ({
|
|
1214
|
+
role: p.role,
|
|
1215
|
+
key: p.key,
|
|
1216
|
+
mode: p.mode,
|
|
1217
|
+
...(typeof p.value !== 'function' && p.value !== undefined && { value: p.value }),
|
|
1218
|
+
})),
|
|
1219
|
+
};
|
|
1127
1220
|
}
|
package/dist/parts.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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[], 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[];
|
|
33
|
+
export declare function stripServerOwnedParts(value: Record<string, unknown>, parts: ResolvedPart[]): Record<string, unknown>;
|
package/dist/parts.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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, tolerateNull = false) {
|
|
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
|
+
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
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
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
|
+
}
|
|
104
|
+
function serverOwnedSlot(p) {
|
|
105
|
+
if (!p.key)
|
|
106
|
+
return p.role;
|
|
107
|
+
return p.mode === 'computedStored' ? p.key : null;
|
|
108
|
+
}
|
|
109
|
+
export function stripServerOwnedParts(value, parts) {
|
|
110
|
+
let out = value;
|
|
111
|
+
for (const p of parts) {
|
|
112
|
+
const slot = serverOwnedSlot(p);
|
|
113
|
+
if (slot != null && slot in out) {
|
|
114
|
+
if (out === value)
|
|
115
|
+
out = { ...value };
|
|
116
|
+
delete out[slot];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
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
|
@@ -3,7 +3,8 @@ 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 {
|
|
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
|
|
38
|
-
if (
|
|
38
|
+
const col = `${key}__${sub}`;
|
|
39
|
+
if (!(col in row))
|
|
39
40
|
continue;
|
|
41
|
+
const v = row[col];
|
|
40
42
|
out[sub] = v;
|
|
41
|
-
|
|
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,6 +73,41 @@ export function fieldMap(items) {
|
|
|
65
73
|
}
|
|
66
74
|
return m;
|
|
67
75
|
}
|
|
76
|
+
function assertNoKeylessCompute(items, owner) {
|
|
77
|
+
for (const it of items) {
|
|
78
|
+
if (isStatic(it) && typeof it.value === 'function')
|
|
79
|
+
throw new Error(`${owner}: computed element needs a key — the server computes it and needs a name to return it under`);
|
|
80
|
+
if (isGroup(it))
|
|
81
|
+
assertNoKeylessCompute(it.fields, owner);
|
|
82
|
+
}
|
|
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
|
+
}
|
|
68
111
|
export function defineShelf(m) {
|
|
69
112
|
const keys = fieldEntries(m.fields).map(([k]) => k);
|
|
70
113
|
const dup = keys.find((k, i) => keys.indexOf(k) !== i);
|
|
@@ -86,9 +129,10 @@ export function defineShelf(m) {
|
|
|
86
129
|
console.warn(`[shelf] ${m.library}/${m.shelf}: ${titles.length} title fields (${titles.map(([k]) => k).join(', ')}) — '${titles[0][0]}' wins`);
|
|
87
130
|
if (m.standalone !== false && !m.single && titleKey(m) === 'id')
|
|
88
131
|
console.warn(`[shelf] ${m.library}/${m.shelf}: no title declared (field.title() or views.title) — the record renders no heading`);
|
|
132
|
+
checkFieldDeclaration(m.fields, `[shelf] ${m.library}/${m.shelf}`);
|
|
89
133
|
const known = derivableKeys(m.fields);
|
|
90
|
-
for (const [key,
|
|
91
|
-
for (const dep of
|
|
134
|
+
for (const [key, deps] of legacyDeriveDeps(m.fields)) {
|
|
135
|
+
for (const dep of deps) {
|
|
92
136
|
if (!known.has(dep))
|
|
93
137
|
throw new Error(`[defineShelf] ${m.library}/${m.shelf}: derive on '${key}' depends on unknown key '${dep}'`);
|
|
94
138
|
}
|
package/dist/showcase.d.ts
CHANGED