@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.
- package/dist/condition.d.ts +2 -0
- package/dist/condition.js +22 -0
- package/dist/derive.d.ts +3 -2
- package/dist/derive.js +24 -12
- package/dist/fields.d.ts +27 -5
- package/dist/fields.js +148 -47
- package/dist/parts.d.ts +28 -0
- package/dist/parts.js +106 -0
- package/dist/shelf.js +12 -3
- package/dist/showcase.d.ts +1 -0
- package/dist/showcase.js +6 -2
- package/package.json +1 -1
package/dist/condition.d.ts
CHANGED
|
@@ -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,
|
|
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/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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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().
|
|
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:
|
|
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
|
|
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().
|
|
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:
|
|
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
|
|
737
|
-
const
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
861
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
|
993
|
-
const
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
}
|
package/dist/parts.d.ts
ADDED
|
@@ -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 {
|
|
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,
|
|
91
|
-
for (const dep of
|
|
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
|
}
|
package/dist/showcase.d.ts
CHANGED
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([
|
|
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)
|