@coffer-org/sdk 2.2.0 → 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/countries.js CHANGED
@@ -144,7 +144,12 @@ export const COUNTRIES = [
144
144
  { code: 'RW', name: 'Руанда', nameRu: 'Руанда', nameEn: 'Rwanda' },
145
145
  { code: 'KN', name: 'Сент-Кітс і Невіс', nameRu: 'Сент-Китс и Невис', nameEn: 'St. Kitts & Nevis' },
146
146
  { code: 'LC', name: 'Сент-Люсія', nameRu: 'Сент-Люсия', nameEn: 'St. Lucia' },
147
- { code: 'VC', name: 'Сент-Вінсент і Гренадіни', nameRu: 'Сент-Винсент и Гренадины', nameEn: 'St. Vincent & Grenadines' },
147
+ {
148
+ code: 'VC',
149
+ name: 'Сент-Вінсент і Гренадіни',
150
+ nameRu: 'Сент-Винсент и Гренадины',
151
+ nameEn: 'St. Vincent & Grenadines',
152
+ },
148
153
  { code: 'WS', name: 'Самоа', nameRu: 'Самоа', nameEn: 'Samoa' },
149
154
  { code: 'SM', name: 'Сан-Марино', nameRu: 'Сан-Марино', nameEn: 'San Marino' },
150
155
  { code: 'ST', name: 'Сан-Томе і Принсіпі', nameRu: 'Сан-Томе и Принсипи', nameEn: 'São Tomé & Príncipe' },
@@ -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
+ }
@@ -71,11 +71,7 @@ export function internalOauthGrants(o) {
71
71
  label: o.label ?? o.key,
72
72
  multiple: true,
73
73
  view: { kind: 'internalOauthGrants' },
74
- fields: [
75
- string({ key: 'clientName' }),
76
- string({ key: 'createdAt' }),
77
- string({ key: 'lastUsedAt' }),
78
- ],
74
+ fields: [string({ key: 'clientName' }), string({ key: 'createdAt' }), string({ key: 'lastUsedAt' })],
79
75
  });
80
76
  }
81
77
  const SLUG_RE = /^[a-z0-9-]+$/;
@@ -164,24 +160,155 @@ export function link(raw) {
164
160
  const TEL_RE = /^\+?[\d\s()-]{4,}$/;
165
161
  const LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
166
162
  const CSS_COLOR_NAMES = new Set([
167
- 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue',
168
- 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk',
169
- 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki',
170
- 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen',
171
- 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue',
172
- 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite',
173
- 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory',
174
- 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
175
- 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen',
176
- 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen',
177
- 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen',
178
- 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream',
179
- 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid',
180
- 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum',
181
- 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown',
182
- 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen',
183
- 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow',
184
- 'yellowgreen', 'transparent',
163
+ 'aliceblue',
164
+ 'antiquewhite',
165
+ 'aqua',
166
+ 'aquamarine',
167
+ 'azure',
168
+ 'beige',
169
+ 'bisque',
170
+ 'black',
171
+ 'blanchedalmond',
172
+ 'blue',
173
+ 'blueviolet',
174
+ 'brown',
175
+ 'burlywood',
176
+ 'cadetblue',
177
+ 'chartreuse',
178
+ 'chocolate',
179
+ 'coral',
180
+ 'cornflowerblue',
181
+ 'cornsilk',
182
+ 'crimson',
183
+ 'cyan',
184
+ 'darkblue',
185
+ 'darkcyan',
186
+ 'darkgoldenrod',
187
+ 'darkgray',
188
+ 'darkgreen',
189
+ 'darkgrey',
190
+ 'darkkhaki',
191
+ 'darkmagenta',
192
+ 'darkolivegreen',
193
+ 'darkorange',
194
+ 'darkorchid',
195
+ 'darkred',
196
+ 'darksalmon',
197
+ 'darkseagreen',
198
+ 'darkslateblue',
199
+ 'darkslategray',
200
+ 'darkslategrey',
201
+ 'darkturquoise',
202
+ 'darkviolet',
203
+ 'deeppink',
204
+ 'deepskyblue',
205
+ 'dimgray',
206
+ 'dimgrey',
207
+ 'dodgerblue',
208
+ 'firebrick',
209
+ 'floralwhite',
210
+ 'forestgreen',
211
+ 'fuchsia',
212
+ 'gainsboro',
213
+ 'ghostwhite',
214
+ 'gold',
215
+ 'goldenrod',
216
+ 'gray',
217
+ 'green',
218
+ 'greenyellow',
219
+ 'grey',
220
+ 'honeydew',
221
+ 'hotpink',
222
+ 'indianred',
223
+ 'indigo',
224
+ 'ivory',
225
+ 'khaki',
226
+ 'lavender',
227
+ 'lavenderblush',
228
+ 'lawngreen',
229
+ 'lemonchiffon',
230
+ 'lightblue',
231
+ 'lightcoral',
232
+ 'lightcyan',
233
+ 'lightgoldenrodyellow',
234
+ 'lightgray',
235
+ 'lightgreen',
236
+ 'lightgrey',
237
+ 'lightpink',
238
+ 'lightsalmon',
239
+ 'lightseagreen',
240
+ 'lightskyblue',
241
+ 'lightslategray',
242
+ 'lightslategrey',
243
+ 'lightsteelblue',
244
+ 'lightyellow',
245
+ 'lime',
246
+ 'limegreen',
247
+ 'linen',
248
+ 'magenta',
249
+ 'maroon',
250
+ 'mediumaquamarine',
251
+ 'mediumblue',
252
+ 'mediumorchid',
253
+ 'mediumpurple',
254
+ 'mediumseagreen',
255
+ 'mediumslateblue',
256
+ 'mediumspringgreen',
257
+ 'mediumturquoise',
258
+ 'mediumvioletred',
259
+ 'midnightblue',
260
+ 'mintcream',
261
+ 'mistyrose',
262
+ 'moccasin',
263
+ 'navajowhite',
264
+ 'navy',
265
+ 'oldlace',
266
+ 'olive',
267
+ 'olivedrab',
268
+ 'orange',
269
+ 'orangered',
270
+ 'orchid',
271
+ 'palegoldenrod',
272
+ 'palegreen',
273
+ 'paleturquoise',
274
+ 'palevioletred',
275
+ 'papayawhip',
276
+ 'peachpuff',
277
+ 'peru',
278
+ 'pink',
279
+ 'plum',
280
+ 'powderblue',
281
+ 'purple',
282
+ 'rebeccapurple',
283
+ 'red',
284
+ 'rosybrown',
285
+ 'royalblue',
286
+ 'saddlebrown',
287
+ 'salmon',
288
+ 'sandybrown',
289
+ 'seagreen',
290
+ 'seashell',
291
+ 'sienna',
292
+ 'silver',
293
+ 'skyblue',
294
+ 'slateblue',
295
+ 'slategray',
296
+ 'slategrey',
297
+ 'snow',
298
+ 'springgreen',
299
+ 'steelblue',
300
+ 'tan',
301
+ 'teal',
302
+ 'thistle',
303
+ 'tomato',
304
+ 'turquoise',
305
+ 'violet',
306
+ 'wheat',
307
+ 'white',
308
+ 'whitesmoke',
309
+ 'yellow',
310
+ 'yellowgreen',
311
+ 'transparent',
185
312
  ]);
186
313
  export function tag(raw) {
187
314
  const o = normalizeOpts(raw);
package/dist/fields.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField } from './fields/validation.ts';
2
+ import { type UnitsSpec } from './units.ts';
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';
5
6
  export { normalizeOpts } 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';
@@ -355,7 +360,7 @@ export declare function triState(o: TriStateOpts & {
355
360
  value?: undefined;
356
361
  }): FieldMeta;
357
362
  export interface SelectOpts extends FieldCoreOpts {
358
- options?: FieldOptions;
363
+ options?: string | (OptionItem | string)[];
359
364
  }
360
365
  export declare function select(o: SelectOpts & {
361
366
  key: string;
@@ -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
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { resolveUnits } from "./units.js";
3
3
  import { isCurrencyCode } from "./currencies.js";
4
4
  import { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue } from "./fields/validation.js";
5
- export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField } from "./fields/validation.js";
5
+ export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField, } from "./fields/validation.js";
6
6
  export { normalizeOpts } from "./fields/normalize.js";
7
7
  import { normalizeOpts } from "./fields/normalize.js";
8
8
  import { presets } from "./field-presets.js";
@@ -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)
@@ -185,7 +187,13 @@ export function applyMultiple(base, multiple) {
185
187
  if (!multiple)
186
188
  return base;
187
189
  const s = multipleZod(base.zod);
188
- return { ...base, column: 'text', hints: { ...base.hints, multiple: true }, json: true, zod: optionalize(s, base.required) };
190
+ return {
191
+ ...base,
192
+ column: 'text',
193
+ hints: { ...base.hints, multiple: true },
194
+ json: true,
195
+ zod: optionalize(s, base.required),
196
+ };
189
197
  }
190
198
  export function string(raw) {
191
199
  const o = normalizeOpts(raw);
@@ -421,11 +429,14 @@ export function triState(raw) {
421
429
  };
422
430
  return wrapKey(o, meta);
423
431
  }
432
+ function toOptionItem(o) {
433
+ return typeof o === 'string' ? { value: o, title: o } : o;
434
+ }
424
435
  export function select(raw) {
425
436
  const o = normalizeOpts(raw);
426
437
  const required = o.required ?? false;
427
438
  const source = typeof o.options === 'string' ? o.options : null;
428
- const inlineOpts = Array.isArray(o.options) ? o.options : [];
439
+ const inlineOpts = Array.isArray(o.options) ? o.options.map(toOptionItem) : [];
429
440
  const s = inlineOpts.length > 0
430
441
  ? z.enum(inlineOpts.map((x) => x.value), { error: () => vmsg('enum') })
431
442
  : z.string(reqErr());
@@ -455,7 +466,10 @@ export function relation(raw) {
455
466
  });
456
467
  }
457
468
  else {
458
- s = z.coerce.number().int().positive({ message: vmsg('int') });
469
+ s = z.coerce
470
+ .number()
471
+ .int()
472
+ .positive({ message: vmsg('int') });
459
473
  }
460
474
  const meta = {
461
475
  kind: 'relation',
@@ -509,9 +523,7 @@ export function check(raw) {
509
523
  const required = o.required ?? false;
510
524
  const multiple = o.multiple ?? false;
511
525
  const slots = o.slots && o.slots.length > 0 ? o.slots : [''];
512
- const contentFields = o.fields?.length
513
- ? o.fields
514
- : [string({ key: 'text', label: 'core.fields.text' })];
526
+ const contentFields = o.fields?.length ? o.fields : [string({ key: 'text', label: 'core.fields.text' })];
515
527
  const contentKeys = new Set();
516
528
  for (const f of contentFields) {
517
529
  if (contentKeys.has(f.key))
@@ -525,7 +537,14 @@ export function check(raw) {
525
537
  const checkFields = slots.map((label, i) => boolean({ key: `check${i}`, label: label || 'core.fields.check' }));
526
538
  if (multiple) {
527
539
  const fields = [...checkFields, ...contentFields];
528
- return group({ key: o.key, label: o.label ?? o.key, multiple: true, required: required, fields, view: { kind: 'checklist' } });
540
+ return group({
541
+ key: o.key,
542
+ label: o.label ?? o.key,
543
+ multiple: true,
544
+ required: required,
545
+ fields,
546
+ view: { kind: 'checklist' },
547
+ });
529
548
  }
530
549
  const shape = {};
531
550
  const columns = {};
@@ -618,6 +637,45 @@ export function measured(raw) {
618
637
  };
619
638
  return wrapKey(o, meta);
620
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
+ }
621
679
  export function money(raw) {
622
680
  const o = normalizeOpts(raw);
623
681
  const required = o.required ?? false;
@@ -1005,6 +1063,8 @@ const PRIMITIVES = {
1005
1063
  json,
1006
1064
  check,
1007
1065
  measured,
1066
+ unit,
1067
+ amount,
1008
1068
  money,
1009
1069
  code,
1010
1070
  geo,
@@ -1048,6 +1108,6 @@ export const field = new Proxy({}, {
1048
1108
  has: (_t, k) => k in composedField(),
1049
1109
  });
1050
1110
  export function toClient(field) {
1051
- const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
1052
- 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 };
1053
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.0",
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"