@coffer-org/plugin-transit 2.1.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.
@@ -6,7 +6,7 @@ export default defineShelf({
6
6
  label: 'transit.bus_route.label',
7
7
  icon: 'lucide:bus',
8
8
  views: {
9
- list: { fields: ['route_number', 'from', 'to'] },
9
+ list: ['route_number', 'from', 'to'],
10
10
  },
11
11
  fields: [
12
12
  field.title({ key: 'name', label: 'core.fields.name' }),
package/dist/index.js CHANGED
@@ -9,7 +9,12 @@ export default definePlugin({
9
9
  dependsOn: [],
10
10
  libraries: [
11
11
  {
12
- meta: defineLibrary({ id: 'transit', label: 'transit.library.label', icon: 'lucide:bus', agent: 'Local bus routes and their departure schedules (bus_route).' }),
12
+ meta: defineLibrary({
13
+ id: 'transit',
14
+ label: 'transit.library.label',
15
+ icon: 'lucide:bus',
16
+ agent: 'Local bus routes and their departure schedules (bus_route).',
17
+ }),
13
18
  shelves: [bus_route],
14
19
  },
15
20
  ],
package/dist/schema.js CHANGED
@@ -4502,6 +4502,8 @@ function normalizeOpts(rawIn) {
4502
4502
  unique: r.unique,
4503
4503
  fixed: r.fixed,
4504
4504
  exts: r.exts,
4505
+ ext: r.ext,
4506
+ maxBytes: r.maxBytes,
4505
4507
  lead: r.lead,
4506
4508
  by: r.by,
4507
4509
  min: r.min,
@@ -4913,6 +4915,42 @@ function snippet(raw) {
4913
4915
  }
4914
4916
  });
4915
4917
  }
4918
+ /** A NUL byte, or U+FFFD left behind by decoding non-UTF-8 bytes as text. */
4919
+ function isBinaryText(v) {
4920
+ return v.includes("\0") || v.includes("�");
4921
+ }
4922
+ function source(raw) {
4923
+ const o = normalizeOpts(raw);
4924
+ const required = o.required ?? false;
4925
+ const ext = o.ext ?? "txt";
4926
+ const maxBytes = o.maxBytes ?? 262144;
4927
+ const s = string$1(reqTypeErr()).superRefine((v, ctx) => {
4928
+ if (isBinaryText(v)) {
4929
+ ctx.addIssue({
4930
+ code: ZodIssueCode.custom,
4931
+ message: vmsg("source_binary")
4932
+ });
4933
+ return;
4934
+ }
4935
+ if (new TextEncoder().encode(v).length > maxBytes) ctx.addIssue({
4936
+ code: ZodIssueCode.custom,
4937
+ message: vmsg("source_too_large", { maxBytes })
4938
+ });
4939
+ });
4940
+ return wrapKey(o, {
4941
+ kind: "source",
4942
+ label: o.label ?? "",
4943
+ required,
4944
+ prim: "text",
4945
+ column: "text",
4946
+ hints: {
4947
+ ext,
4948
+ maxBytes,
4949
+ noEditControl: true
4950
+ },
4951
+ zod: optionalize(s, required)
4952
+ });
4953
+ }
4916
4954
  function rating(raw) {
4917
4955
  const o = normalizeOpts(raw);
4918
4956
  const required = o.required ?? false;
@@ -5049,6 +5087,7 @@ var presets = {
5049
5087
  tags,
5050
5088
  markdown,
5051
5089
  snippet,
5090
+ source,
5052
5091
  rating,
5053
5092
  duration,
5054
5093
  reminder,
@@ -5974,6 +6013,7 @@ function keyed(o) {
5974
6013
  };
5975
6014
  }
5976
6015
  var isCollectionGroup = (g) => g.key != null && g.multiple === true;
6016
+ var isEmbeddedGroup = (g) => g.key != null && g.multiple !== true;
5977
6017
  /** Horizontal divider. */
5978
6018
  function divider() {
5979
6019
  return { el: "divider" };
@@ -6038,6 +6078,15 @@ function wrapKey(opts, meta) {
6038
6078
  ...m,
6039
6079
  hidden: opts.hidden
6040
6080
  };
6081
+ if (opts.derive) m = {
6082
+ ...m,
6083
+ derive: opts.derive,
6084
+ derived: true,
6085
+ hints: {
6086
+ ...m.hints,
6087
+ noEditControl: true
6088
+ }
6089
+ };
6041
6090
  if (opts.key) return {
6042
6091
  key: opts.key,
6043
6092
  type: m
@@ -6352,11 +6401,18 @@ function triState(raw) {
6352
6401
  zod: optionalize(s, required)
6353
6402
  });
6354
6403
  }
6404
+ /** Inline option entry → OptionItem (plain string = value and label at once). */
6405
+ function toOptionItem(o) {
6406
+ return typeof o === "string" ? {
6407
+ value: o,
6408
+ title: o
6409
+ } : o;
6410
+ }
6355
6411
  function select(raw) {
6356
6412
  const o = normalizeOpts(raw);
6357
6413
  const required = o.required ?? false;
6358
6414
  const source = typeof o.options === "string" ? o.options : null;
6359
- const inlineOpts = Array.isArray(o.options) ? o.options : [];
6415
+ const inlineOpts = Array.isArray(o.options) ? o.options.map(toOptionItem) : [];
6360
6416
  const s = inlineOpts.length > 0 ? _enum(inlineOpts.map((x) => x.value), { error: () => vmsg("enum") }) : string$1(reqErr());
6361
6417
  return wrapKey(o, applyMultiple({
6362
6418
  kind: "select",
@@ -6429,15 +6485,23 @@ function check(raw) {
6429
6485
  const required = o.required ?? false;
6430
6486
  const multiple = o.multiple ?? false;
6431
6487
  const slots = o.slots && o.slots.length > 0 ? o.slots : [""];
6488
+ const contentFields = o.fields?.length ? o.fields : [string({
6489
+ key: "text",
6490
+ label: "core.fields.text"
6491
+ })];
6492
+ const contentKeys = /* @__PURE__ */ new Set();
6493
+ for (const f of contentFields) {
6494
+ if (contentKeys.has(f.key)) throw new Error(`[field.check] duplicate content field '${f.key}'`);
6495
+ if (/^check\d+$/.test(f.key)) throw new Error(`[field.check] content field '${f.key}' is reserved for checkbox slots`);
6496
+ if (!multiple && (f.type.virtual || f.type.columns)) throw new Error(`[field.check] single content field '${f.key}' must be a scalar stored field`);
6497
+ contentKeys.add(f.key);
6498
+ }
6499
+ const checkFields = slots.map((label, i) => boolean({
6500
+ key: `check${i}`,
6501
+ label: label || "core.fields.check"
6502
+ }));
6432
6503
  if (multiple) {
6433
- const fields = slots.map((label, i) => boolean({
6434
- key: `check${i}`,
6435
- label: label || "core.fields.check"
6436
- }));
6437
- fields.push(string({
6438
- key: "text",
6439
- label: "core.fields.text"
6440
- }));
6504
+ const fields = [...checkFields, ...contentFields];
6441
6505
  return group({
6442
6506
  key: o.key,
6443
6507
  label: o.label ?? o.key,
@@ -6447,8 +6511,12 @@ function check(raw) {
6447
6511
  view: { kind: "checklist" }
6448
6512
  });
6449
6513
  }
6450
- const shape = { text: string$1() };
6451
- const columns = { text: "text" };
6514
+ const shape = {};
6515
+ const columns = {};
6516
+ for (const f of contentFields) {
6517
+ shape[f.key] = f.type.zod;
6518
+ columns[f.key] = f.type.column;
6519
+ }
6452
6520
  slots.forEach((_, i) => {
6453
6521
  shape[`check${i}`] = boolean$1();
6454
6522
  columns[`check${i}`] = "boolean";
@@ -6474,7 +6542,13 @@ function check(raw) {
6474
6542
  required,
6475
6543
  prim: "check",
6476
6544
  column: "text",
6477
- hints: { slots },
6545
+ hints: {
6546
+ slots,
6547
+ fields: contentFields.map((f) => ({
6548
+ key: f.key,
6549
+ ...toClient(f.type)
6550
+ }))
6551
+ },
6478
6552
  columns,
6479
6553
  zod: optionalize(s, required)
6480
6554
  });
@@ -6535,6 +6609,47 @@ function measured(raw) {
6535
6609
  zod: optionalize(s, required)
6536
6610
  });
6537
6611
  }
6612
+ function unit(raw) {
6613
+ const o = normalizeOpts(raw);
6614
+ const required = o.required ?? false;
6615
+ const units = resolveUnits(o.options);
6616
+ const s = string$1(reqErr()).refine((v) => units.some((u) => u.value === v), { message: vmsg("measured_unit") });
6617
+ return wrapKey(o, {
6618
+ kind: "unit",
6619
+ label: o.label ?? "",
6620
+ required,
6621
+ prim: "select",
6622
+ column: "text",
6623
+ hints: { source: typeof o.options === "string" ? o.options : null },
6624
+ options: units.map((u) => ({
6625
+ value: u.value,
6626
+ title: u.label
6627
+ })),
6628
+ zod: optionalize(s, required)
6629
+ });
6630
+ }
6631
+ function amount(raw) {
6632
+ const o = normalizeOpts(raw);
6633
+ const required = o.required ?? false;
6634
+ const cfg = o.config ?? {};
6635
+ let s = number(typeErr());
6636
+ if (cfg.min != null) s = s.min(cfg.min, { message: vmsg("min", { min: cfg.min }) });
6637
+ if (cfg.max != null) s = s.max(cfg.max, { message: vmsg("max", { max: cfg.max }) });
6638
+ return wrapKey(o, {
6639
+ kind: "amount",
6640
+ label: o.label ?? "",
6641
+ required,
6642
+ prim: "number",
6643
+ column: "real",
6644
+ hints: {
6645
+ unitFrom: o.unitFrom,
6646
+ min: cfg.min,
6647
+ max: cfg.max,
6648
+ step: cfg.step ?? "any"
6649
+ },
6650
+ zod: optionalize(s, required)
6651
+ });
6652
+ }
6538
6653
  function money(raw) {
6539
6654
  const o = normalizeOpts(raw);
6540
6655
  const required = o.required ?? false;
@@ -7012,6 +7127,8 @@ var PRIMITIVES = {
7012
7127
  json,
7013
7128
  check,
7014
7129
  measured,
7130
+ unit,
7131
+ amount,
7015
7132
  money,
7016
7133
  code,
7017
7134
  geo,
@@ -7057,7 +7174,7 @@ var field = new Proxy({}, {
7057
7174
  has: (_t, k) => k in composedField()
7058
7175
  });
7059
7176
  function toClient(field) {
7060
- const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
7177
+ const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived } = field;
7061
7178
  return {
7062
7179
  kind,
7063
7180
  label,
@@ -7068,7 +7185,8 @@ function toClient(field) {
7068
7185
  relation,
7069
7186
  virtual,
7070
7187
  json,
7071
- hidden
7188
+ hidden,
7189
+ derived
7072
7190
  };
7073
7191
  }
7074
7192
  //#endregion
@@ -7751,6 +7869,41 @@ var createDefaultQueryTester = function(query, options) {
7751
7869
  };
7752
7870
  createDefaultQueryTester.createEqualsOperation;
7753
7871
  //#endregion
7872
+ //#region ../sdk/src/derive.ts
7873
+ /**
7874
+ * Derived fields: a value the SERVER computes on every write and stores in a real
7875
+ * column. Unlike a computed StaticEl (`value: fn`), which the browser renders and
7876
+ * nothing persists, a derived field keeps its column — so list views, filters,
7877
+ * sorting, field.lookup and MCP consumers see it like any other field.
7878
+ *
7879
+ * The function is pure, synchronous and isomorphic: it receives the fully hydrated
7880
+ * record (base scalars, embedded groups, collections, extends) and no database
7881
+ * handle. That is what lets the same declaration run on the server at write time
7882
+ * and in the browser for live preview.
7883
+ *
7884
+ * Scope: top-level fields of the record only. A derived field inside a collection
7885
+ * row is not supported.
7886
+ */
7887
+ /** [key, spec] for every top-level derived field. Layout groups are flattened. */
7888
+ function derivedEntries(fields) {
7889
+ const out = [];
7890
+ for (const it of fields) if (isField(it)) {
7891
+ const spec = it.type.derive;
7892
+ if (spec) out.push([it.key, spec]);
7893
+ } else if (isGroup(it) && !it.key) out.push(...derivedEntries(it.fields));
7894
+ return out;
7895
+ }
7896
+ /** Keys a shelf's derive specs may reference: its own fields plus its collections. */
7897
+ function derivableKeys(fields) {
7898
+ const keys = /* @__PURE__ */ new Set();
7899
+ for (const it of fields) if (isField(it)) keys.add(it.key);
7900
+ else if (isGroup(it)) {
7901
+ if (it.key) keys.add(it.key);
7902
+ if (!it.key) for (const k of derivableKeys(it.fields)) keys.add(k);
7903
+ }
7904
+ return keys;
7905
+ }
7906
+ //#endregion
7754
7907
  //#region ../sdk/src/shelf.ts
7755
7908
  /** Flat [storageKey, FieldMeta] pairs of the parent table's columns.
7756
7909
  * layout group → flattened; embedded group → prefix `key__`; collection → skipped. */
@@ -7768,9 +7921,49 @@ function defineShelf(m) {
7768
7921
  const keys = fieldEntries(m.fields).map(([k]) => k);
7769
7922
  const dup = keys.find((k, i) => keys.indexOf(k) !== i);
7770
7923
  if (dup) throw new Error(`[shelf] ${m.library}/${m.shelf}: duplicate key '${dup}'`);
7771
- if (m.standalone !== false && !m.views?.list?.fields?.length) console.warn(`[shelf] ${m.library}/${m.shelf}: standalone shelf without an explicit views.list`);
7924
+ if (m.single && !m.claude) throw new Error(`[shelf] ${m.library}/${m.shelf}: single shelf requires \`claude\` the agent cannot find it otherwise`);
7925
+ if (m.views?.list !== void 0 && !Array.isArray(m.views.list)) throw new Error(`[shelf] ${m.library}/${m.shelf}: views.list is a flat array of field keys — the { kind, fields } form is gone; use views.list: [...] plus views.listKind`);
7926
+ if (m.standalone !== false && !m.single && !m.views?.list?.length) console.warn(`[shelf] ${m.library}/${m.shelf}: standalone shelf without an explicit views.list`);
7927
+ if (m.views?.title && !ownFieldEntries(m.fields).some(([k]) => k === m.views.title)) throw new Error(`[shelf] ${m.library}/${m.shelf}: views.title '${m.views.title}' is not a field`);
7928
+ if (m.views?.list?.includes(titleKey(m))) console.warn(`[shelf] ${m.library}/${m.shelf}: views.list repeats the title '${titleKey(m)}' — the title is declared separately`);
7929
+ const titles = ownFieldEntries(m.fields).filter(([, f]) => f.kind === "title");
7930
+ if (titles.length > 1) console.warn(`[shelf] ${m.library}/${m.shelf}: ${titles.length} title fields (${titles.map(([k]) => k).join(", ")}) — '${titles[0][0]}' wins`);
7931
+ if (m.standalone !== false && !m.single && titleKey(m) === "id") console.warn(`[shelf] ${m.library}/${m.shelf}: no title declared (field.title() or views.title) — the record renders no heading`);
7932
+ const known = derivableKeys(m.fields);
7933
+ for (const [key, spec] of derivedEntries(m.fields)) for (const dep of spec.deps) if (!known.has(dep)) throw new Error(`[defineShelf] ${m.library}/${m.shelf}: derive on '${key}' depends on unknown key '${dep}'`);
7772
7934
  return m;
7773
7935
  }
7936
+ /**
7937
+ * Contract of a real entity:
7938
+ * id — uuid (the ONLY required system field, auto).
7939
+ * The rest (including name) are regular fields in ShelfDef.fields.
7940
+ * created_at/updated_at — system infrastructure (outside fields, auto-managed).
7941
+ */
7942
+ /**
7943
+ * Keyed fields of the record itself, in declaration order: top level and plain
7944
+ * layout groups. An embedded group or a collection describes a sub-record, so its
7945
+ * fields are not candidates for the record's own heading.
7946
+ */
7947
+ function ownFieldEntries(items) {
7948
+ const out = [];
7949
+ for (const it of items) if (isField(it)) out.push([it.key, it.type]);
7950
+ else if (isGroup(it) && !isCollectionGroup(it) && !isEmbeddedGroup(it)) out.push(...ownFieldEntries(it.fields));
7951
+ return out;
7952
+ }
7953
+ /**
7954
+ * Key of the record's title field (lists, relation pickers, inlines, record heading).
7955
+ * Priority: views.title → the first keyed kind:'title' field → 'id'.
7956
+ *
7957
+ * The title is declared, never guessed: a shelf that wants a heading marks the field
7958
+ * with field.title(), or names another field through views.title when the heading is
7959
+ * not a text field (a date, a virtual value). A shelf that declares nothing resolves
7960
+ * to 'id' and renders no heading — every field stays in the body with its own label.
7961
+ */
7962
+ function titleKey(m) {
7963
+ if (m.views?.title) return m.views.title;
7964
+ const declared = ownFieldEntries(m.fields).find(([, f]) => f.kind === "title");
7965
+ return declared ? declared[0] : "id";
7966
+ }
7774
7967
  //#endregion
7775
7968
  //#region src/bus_route/index.ts
7776
7969
  /**
@@ -7784,11 +7977,11 @@ var bus_route_default = defineShelf({
7784
7977
  library: "transit",
7785
7978
  label: "transit.bus_route.label",
7786
7979
  icon: "lucide:bus",
7787
- views: { list: { fields: [
7980
+ views: { list: [
7788
7981
  "route_number",
7789
7982
  "from",
7790
7983
  "to"
7791
- ] } },
7984
+ ] },
7792
7985
  fields: [
7793
7986
  field.title({
7794
7987
  key: "name",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-transit",
3
- "version": "2.1.1",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -25,9 +25,9 @@
25
25
  "test": "node --import tsx/esm --test 'src/runtime/*.test.ts'"
26
26
  },
27
27
  "dependencies": {
28
- "@coffer-org/sdk": "^2.1.1",
29
- "@coffer-org/server": "^2.2.2",
30
- "@coffer-org/dav": "^2.1.1"
28
+ "@coffer-org/sdk": "^3.0.0",
29
+ "@coffer-org/server": "^3.0.0",
30
+ "@coffer-org/dav": "^3.0.0"
31
31
  },
32
32
  "coffer": {
33
33
  "schema": "dist/schema.js"