@coffer-org/sdk 2.2.1 → 3.0.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 +12 -0
- package/dist/derive.js +42 -0
- package/dist/fields.d.ts +28 -0
- package/dist/fields.js +45 -2
- package/dist/shelf.d.ts +2 -5
- package/dist/shelf.js +40 -15
- package/dist/users-shelf.js +1 -1
- package/package.json +5 -1
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type LayoutEl } from './fields.ts';
|
|
2
|
+
export interface DeriveCtx {
|
|
3
|
+
record: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export type DeriveFn = (ctx: DeriveCtx) => unknown;
|
|
6
|
+
export interface DeriveSpec {
|
|
7
|
+
deps: string[];
|
|
8
|
+
fn: DeriveFn;
|
|
9
|
+
}
|
|
10
|
+
export declare function derivedEntries(fields: LayoutEl[]): [string, DeriveSpec][];
|
|
11
|
+
export declare function applyDerived(fields: LayoutEl[], record: Record<string, unknown>): Record<string, unknown>;
|
|
12
|
+
export declare function derivableKeys(fields: LayoutEl[]): Set<string>;
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isField, isGroup } from "./fields.js";
|
|
2
|
+
export function derivedEntries(fields) {
|
|
3
|
+
const out = [];
|
|
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
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function applyDerived(fields, record) {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const [key, spec] of derivedEntries(fields)) {
|
|
19
|
+
try {
|
|
20
|
+
out[key] = spec.fn({ record });
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.warn(`[derive] '${key}' threw`, e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
export function derivableKeys(fields) {
|
|
29
|
+
const keys = new Set();
|
|
30
|
+
for (const it of fields) {
|
|
31
|
+
if (isField(it))
|
|
32
|
+
keys.add(it.key);
|
|
33
|
+
else if (isGroup(it)) {
|
|
34
|
+
if (it.key)
|
|
35
|
+
keys.add(it.key);
|
|
36
|
+
if (!it.key)
|
|
37
|
+
for (const k of derivableKeys(it.fields))
|
|
38
|
+
keys.add(k);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return keys;
|
|
42
|
+
}
|
package/dist/fields.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { type UnitsSpec } from './units.ts';
|
|
2
3
|
export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField, } from './fields/validation.ts';
|
|
3
4
|
import type { Condition } from './condition.ts';
|
|
4
5
|
export type { Rules, View, FieldConfig } from './fields/normalize.ts';
|
|
@@ -99,6 +100,7 @@ export interface FieldClient {
|
|
|
99
100
|
virtual?: boolean;
|
|
100
101
|
default?: string | number | boolean | null;
|
|
101
102
|
json?: true;
|
|
103
|
+
derived?: true;
|
|
102
104
|
columns?: Record<string, ColumnType>;
|
|
103
105
|
hidden?: boolean | Condition;
|
|
104
106
|
}
|
|
@@ -180,6 +182,7 @@ export interface FieldCoreOpts {
|
|
|
180
182
|
emphasis?: 'hero' | 'muted';
|
|
181
183
|
noLabel?: boolean;
|
|
182
184
|
role?: 'cover' | 'avatar';
|
|
185
|
+
derive?: import('./derive.ts').DeriveSpec;
|
|
183
186
|
}
|
|
184
187
|
export declare function wrapKey(opts: {
|
|
185
188
|
key?: string;
|
|
@@ -190,10 +193,12 @@ export declare function wrapKey(opts: {
|
|
|
190
193
|
emphasis?: 'hero' | 'muted';
|
|
191
194
|
noLabel?: boolean;
|
|
192
195
|
role?: 'cover' | 'avatar';
|
|
196
|
+
derive?: import('./derive.ts').DeriveSpec;
|
|
193
197
|
}, meta: FieldMeta): FieldItem | StaticEl | FieldMeta;
|
|
194
198
|
export interface FieldMeta extends FieldClient {
|
|
195
199
|
column: ColumnType;
|
|
196
200
|
zod: z.ZodTypeAny;
|
|
201
|
+
derive?: import('./derive.ts').DeriveSpec;
|
|
197
202
|
}
|
|
198
203
|
export declare function isJsonStored(field: FieldMeta | FieldClient): boolean;
|
|
199
204
|
export { LANGUAGES, SEX_OPTIONS, WEEKDAY_OPTIONS } from './fields/constants.ts';
|
|
@@ -427,6 +432,27 @@ export declare function measured(o: MeasuredOpts & {
|
|
|
427
432
|
key?: undefined;
|
|
428
433
|
value?: undefined;
|
|
429
434
|
}): FieldMeta;
|
|
435
|
+
export type UnitScope = string;
|
|
436
|
+
export interface UnitOpts extends FieldCoreOpts {
|
|
437
|
+
options: UnitsSpec;
|
|
438
|
+
}
|
|
439
|
+
export declare function unit(o: UnitOpts & {
|
|
440
|
+
key: string;
|
|
441
|
+
}): FieldItem;
|
|
442
|
+
export declare function unit(o: UnitOpts & {
|
|
443
|
+
key?: undefined;
|
|
444
|
+
value?: undefined;
|
|
445
|
+
}): FieldMeta;
|
|
446
|
+
export interface AmountOpts extends FieldCoreOpts {
|
|
447
|
+
unitFrom: UnitScope;
|
|
448
|
+
}
|
|
449
|
+
export declare function amount(o: AmountOpts & {
|
|
450
|
+
key: string;
|
|
451
|
+
}): FieldItem;
|
|
452
|
+
export declare function amount(o: AmountOpts & {
|
|
453
|
+
key?: undefined;
|
|
454
|
+
value?: undefined;
|
|
455
|
+
}): FieldMeta;
|
|
430
456
|
export type MoneyOpts = FieldCoreOpts;
|
|
431
457
|
export declare function money(o: MoneyOpts & {
|
|
432
458
|
key: string;
|
|
@@ -667,6 +693,8 @@ declare const PRIMITIVES: {
|
|
|
667
693
|
json: typeof json;
|
|
668
694
|
check: typeof check;
|
|
669
695
|
measured: typeof measured;
|
|
696
|
+
unit: typeof unit;
|
|
697
|
+
amount: typeof amount;
|
|
670
698
|
money: typeof money;
|
|
671
699
|
code: typeof code;
|
|
672
700
|
geo: typeof geo;
|
package/dist/fields.js
CHANGED
|
@@ -127,6 +127,8 @@ export function wrapKey(opts, meta) {
|
|
|
127
127
|
m = { ...m, default: opts.default };
|
|
128
128
|
if (opts.hidden !== undefined)
|
|
129
129
|
m = { ...m, hidden: opts.hidden };
|
|
130
|
+
if (opts.derive)
|
|
131
|
+
m = { ...m, derive: opts.derive, derived: true, hints: { ...m.hints, noEditControl: true } };
|
|
130
132
|
if (opts.key)
|
|
131
133
|
return { key: opts.key, type: m };
|
|
132
134
|
if ('value' in opts && opts.value !== undefined)
|
|
@@ -635,6 +637,45 @@ export function measured(raw) {
|
|
|
635
637
|
};
|
|
636
638
|
return wrapKey(o, meta);
|
|
637
639
|
}
|
|
640
|
+
export function unit(raw) {
|
|
641
|
+
const o = normalizeOpts(raw);
|
|
642
|
+
const required = o.required ?? false;
|
|
643
|
+
const units = resolveUnits(o.options);
|
|
644
|
+
const s = z
|
|
645
|
+
.string(reqErr())
|
|
646
|
+
.refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') });
|
|
647
|
+
const meta = {
|
|
648
|
+
kind: 'unit',
|
|
649
|
+
label: o.label ?? '',
|
|
650
|
+
required,
|
|
651
|
+
prim: 'select',
|
|
652
|
+
column: 'text',
|
|
653
|
+
hints: { source: typeof o.options === 'string' ? o.options : null },
|
|
654
|
+
options: units.map((u) => ({ value: u.value, title: u.label })),
|
|
655
|
+
zod: optionalize(s, required),
|
|
656
|
+
};
|
|
657
|
+
return wrapKey(o, meta);
|
|
658
|
+
}
|
|
659
|
+
export function amount(raw) {
|
|
660
|
+
const o = normalizeOpts(raw);
|
|
661
|
+
const required = o.required ?? false;
|
|
662
|
+
const cfg = (o.config ?? {});
|
|
663
|
+
let s = z.coerce.number(typeErr());
|
|
664
|
+
if (cfg.min != null)
|
|
665
|
+
s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
|
|
666
|
+
if (cfg.max != null)
|
|
667
|
+
s = s.max(cfg.max, { message: vmsg('max', { max: cfg.max }) });
|
|
668
|
+
const meta = {
|
|
669
|
+
kind: 'amount',
|
|
670
|
+
label: o.label ?? '',
|
|
671
|
+
required,
|
|
672
|
+
prim: 'number',
|
|
673
|
+
column: 'real',
|
|
674
|
+
hints: { unitFrom: o.unitFrom, min: cfg.min, max: cfg.max, step: cfg.step ?? 'any' },
|
|
675
|
+
zod: optionalize(s, required),
|
|
676
|
+
};
|
|
677
|
+
return wrapKey(o, meta);
|
|
678
|
+
}
|
|
638
679
|
export function money(raw) {
|
|
639
680
|
const o = normalizeOpts(raw);
|
|
640
681
|
const required = o.required ?? false;
|
|
@@ -1022,6 +1063,8 @@ const PRIMITIVES = {
|
|
|
1022
1063
|
json,
|
|
1023
1064
|
check,
|
|
1024
1065
|
measured,
|
|
1066
|
+
unit,
|
|
1067
|
+
amount,
|
|
1025
1068
|
money,
|
|
1026
1069
|
code,
|
|
1027
1070
|
geo,
|
|
@@ -1065,6 +1108,6 @@ export const field = new Proxy({}, {
|
|
|
1065
1108
|
has: (_t, k) => k in composedField(),
|
|
1066
1109
|
});
|
|
1067
1110
|
export function toClient(field) {
|
|
1068
|
-
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
|
|
1069
|
-
return { kind, label, required, prim, hints, options, relation, virtual, json, hidden };
|
|
1111
|
+
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived } = field;
|
|
1112
|
+
return { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived };
|
|
1070
1113
|
}
|
package/dist/shelf.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { FieldMeta, LayoutEl, ColumnType } from './fields.ts';
|
|
3
3
|
import type { FieldClient, GroupEl, PseudoEl } from './fields.ts';
|
|
4
|
-
export interface ShelfListView {
|
|
5
|
-
kind?: string;
|
|
6
|
-
fields: string[];
|
|
7
|
-
}
|
|
8
4
|
export interface ShelfViews {
|
|
9
5
|
title?: string;
|
|
10
6
|
inline?: string[];
|
|
11
|
-
list?:
|
|
7
|
+
list?: string[];
|
|
8
|
+
listKind?: string;
|
|
12
9
|
groupBy?: string[];
|
|
13
10
|
defaultGroupBy?: string;
|
|
14
11
|
select?: {
|
package/dist/shelf.js
CHANGED
|
@@ -2,6 +2,8 @@ import { z } from 'zod';
|
|
|
2
2
|
import { toClient, isField, isGroup, isStatic, isCollectionGroup, isEmbeddedGroup } from "./fields.js";
|
|
3
3
|
import { resolveFlag } from "./condition.js";
|
|
4
4
|
import { parseUrl } from "./fields/url.js";
|
|
5
|
+
import { vmsg } from "./fields/validation.js";
|
|
6
|
+
import { derivedEntries, derivableKeys } from "./derive.js";
|
|
5
7
|
export function fieldEntries(items) {
|
|
6
8
|
const out = [];
|
|
7
9
|
for (const it of items) {
|
|
@@ -70,24 +72,45 @@ export function defineShelf(m) {
|
|
|
70
72
|
throw new Error(`[shelf] ${m.library}/${m.shelf}: duplicate key '${dup}'`);
|
|
71
73
|
if (m.single && !m.claude)
|
|
72
74
|
throw new Error(`[shelf] ${m.library}/${m.shelf}: single shelf requires \`claude\` — the agent cannot find it otherwise`);
|
|
73
|
-
if (m.
|
|
75
|
+
if (m.views?.list !== undefined && !Array.isArray(m.views.list))
|
|
76
|
+
throw new Error(`[shelf] ${m.library}/${m.shelf}: views.list is a flat array of field keys — ` +
|
|
77
|
+
`the { kind, fields } form is gone; use views.list: [...] plus views.listKind`);
|
|
78
|
+
if (m.standalone !== false && !m.single && !m.views?.list?.length)
|
|
74
79
|
console.warn(`[shelf] ${m.library}/${m.shelf}: standalone shelf without an explicit views.list`);
|
|
80
|
+
if (m.views?.title && !ownFieldEntries(m.fields).some(([k]) => k === m.views.title))
|
|
81
|
+
throw new Error(`[shelf] ${m.library}/${m.shelf}: views.title '${m.views.title}' is not a field`);
|
|
82
|
+
if (m.views?.list?.includes(titleKey(m)))
|
|
83
|
+
console.warn(`[shelf] ${m.library}/${m.shelf}: views.list repeats the title '${titleKey(m)}' — the title is declared separately`);
|
|
84
|
+
const titles = ownFieldEntries(m.fields).filter(([, f]) => f.kind === 'title');
|
|
85
|
+
if (titles.length > 1)
|
|
86
|
+
console.warn(`[shelf] ${m.library}/${m.shelf}: ${titles.length} title fields (${titles.map(([k]) => k).join(', ')}) — '${titles[0][0]}' wins`);
|
|
87
|
+
if (m.standalone !== false && !m.single && titleKey(m) === 'id')
|
|
88
|
+
console.warn(`[shelf] ${m.library}/${m.shelf}: no title declared (field.title() or views.title) — the record renders no heading`);
|
|
89
|
+
const known = derivableKeys(m.fields);
|
|
90
|
+
for (const [key, spec] of derivedEntries(m.fields)) {
|
|
91
|
+
for (const dep of spec.deps) {
|
|
92
|
+
if (!known.has(dep))
|
|
93
|
+
throw new Error(`[defineShelf] ${m.library}/${m.shelf}: derive on '${key}' depends on unknown key '${dep}'`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
75
96
|
return m;
|
|
76
97
|
}
|
|
77
98
|
export const SYSTEM_FIELDS = ['id', 'createdAt', 'updatedAt'];
|
|
99
|
+
function ownFieldEntries(items) {
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const it of items) {
|
|
102
|
+
if (isField(it))
|
|
103
|
+
out.push([it.key, it.type]);
|
|
104
|
+
else if (isGroup(it) && !isCollectionGroup(it) && !isEmbeddedGroup(it))
|
|
105
|
+
out.push(...ownFieldEntries(it.fields));
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
78
109
|
export function titleKey(m) {
|
|
79
110
|
if (m.views?.title)
|
|
80
111
|
return m.views.title;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
if (inlineNonImage)
|
|
84
|
-
return inlineNonImage;
|
|
85
|
-
if (fm['name'])
|
|
86
|
-
return 'name';
|
|
87
|
-
const firstText = fieldEntries(m.fields).find(([, f]) => !f.virtual && isTextPrim(f.prim));
|
|
88
|
-
if (firstText)
|
|
89
|
-
return firstText[0];
|
|
90
|
-
return 'id';
|
|
112
|
+
const declared = ownFieldEntries(m.fields).find(([, f]) => f.kind === 'title');
|
|
113
|
+
return declared ? declared[0] : 'id';
|
|
91
114
|
}
|
|
92
115
|
export function recordTitle(m, record) {
|
|
93
116
|
return String(record[titleKey(m)] ?? record['id'] ?? '');
|
|
@@ -128,13 +151,11 @@ export function inlineText(m, row) {
|
|
|
128
151
|
}
|
|
129
152
|
export function viewKeys(m) {
|
|
130
153
|
const tk = titleKey(m);
|
|
131
|
-
const rest = m.views?.list?.
|
|
132
|
-
? realKeys(m, m.views.list.fields.filter((k) => k !== tk))
|
|
133
|
-
: [];
|
|
154
|
+
const rest = m.views?.list?.length ? realKeys(m, m.views.list.filter((k) => k !== tk)) : [];
|
|
134
155
|
return [tk, ...rest];
|
|
135
156
|
}
|
|
136
157
|
export function listKind(m) {
|
|
137
|
-
return m.views?.
|
|
158
|
+
return m.views?.listKind ?? 'table';
|
|
138
159
|
}
|
|
139
160
|
const GROUPABLE_PRIMS = new Set(['select', 'checkbox', 'relation']);
|
|
140
161
|
export function groupableFields(m) {
|
|
@@ -198,6 +219,10 @@ export function buildShape(fields, partial) {
|
|
|
198
219
|
if (isField(it)) {
|
|
199
220
|
if (it.type.virtual)
|
|
200
221
|
continue;
|
|
222
|
+
if (it.type.derive) {
|
|
223
|
+
shape[it.key] = z.never({ error: () => vmsg('derived') }).optional();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
201
226
|
shape[it.key] = partial ? it.type.zod.optional() : it.type.zod;
|
|
202
227
|
}
|
|
203
228
|
else if (isGroup(it)) {
|
package/dist/users-shelf.js
CHANGED
|
@@ -5,7 +5,7 @@ export const USERS_SHELF = defineShelf({
|
|
|
5
5
|
library: '',
|
|
6
6
|
label: 'auth.usersTitle',
|
|
7
7
|
icon: 'lucide:users',
|
|
8
|
-
views: {
|
|
8
|
+
views: { title: 'login', list: ['displayName', 'role', 'disabled'] },
|
|
9
9
|
fields: [
|
|
10
10
|
field.string({ key: 'login', label: 'auth.fieldLogin' }),
|
|
11
11
|
field.string({ key: 'displayName', label: 'auth.fieldDisplayName' }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./derive": {
|
|
17
|
+
"types": "./dist/derive.d.ts",
|
|
18
|
+
"default": "./dist/derive.js"
|
|
19
|
+
},
|
|
16
20
|
"./*": {
|
|
17
21
|
"types": "./dist/*.d.ts",
|
|
18
22
|
"default": "./dist/*.js"
|