@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.
@@ -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?: ShelfListView;
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.standalone !== false && !m.single && !m.views?.list?.fields?.length)
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 fm = fieldMap(m.fields);
82
- const inlineNonImage = m.views?.inline?.find((k) => fm[k] && !isImageKind(fm[k].kind));
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?.fields?.length
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?.list?.kind ?? 'table';
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)) {
@@ -5,7 +5,7 @@ export const USERS_SHELF = defineShelf({
5
5
  library: '',
6
6
  label: 'auth.usersTitle',
7
7
  icon: 'lucide:users',
8
- views: { list: { fields: ['login', 'displayName', 'role', 'disabled'] } },
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": "2.2.1",
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"