@chelone/core 0.6.0 → 0.7.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.
Files changed (242) hide show
  1. package/dist/assistant/assistant-session.d.ts +6 -0
  2. package/dist/assistant/assistant-session.js +204 -181
  3. package/dist/assistant/echo-transport.js +26 -29
  4. package/dist/assistant/http-transport.d.ts +10 -0
  5. package/dist/assistant/http-transport.js +98 -0
  6. package/dist/assistant/types.d.ts +1 -7
  7. package/dist/auth/access-control.d.ts +13 -5
  8. package/dist/auth/access-control.js +25 -21
  9. package/dist/auth/auth-options.d.ts +21 -17
  10. package/dist/auth/auth-options.js +80 -48
  11. package/dist/auth/permission-matrix.d.ts +12 -0
  12. package/dist/auth/permission-matrix.js +67 -0
  13. package/dist/auth/types.d.ts +14 -4
  14. package/dist/block-editor/block-document.d.ts +60 -0
  15. package/dist/block-editor/block-document.js +143 -0
  16. package/dist/block-editor/data-blocks.d.ts +63 -0
  17. package/dist/block-editor/data-blocks.js +117 -0
  18. package/dist/chart/chart-data.js +46 -44
  19. package/dist/command-menu/hotkey.js +58 -47
  20. package/dist/command-menu/recents.js +8 -11
  21. package/dist/command-menu/registry.d.ts +9 -5
  22. package/dist/command-menu/registry.js +36 -46
  23. package/dist/controller/calendar-dates.js +82 -113
  24. package/dist/controller/calendar-events.js +93 -88
  25. package/dist/controller/client-list-controller.d.ts +2 -2
  26. package/dist/controller/client-list-controller.js +251 -254
  27. package/dist/controller/data-table-columns.d.ts +2 -0
  28. package/dist/controller/data-table-columns.js +50 -56
  29. package/dist/controller/deep-equal.d.ts +1 -0
  30. package/dist/controller/deep-equal.js +14 -0
  31. package/dist/controller/filter-toggle.js +36 -44
  32. package/dist/controller/in-place-editor.d.ts +42 -0
  33. package/dist/controller/in-place-editor.js +42 -0
  34. package/dist/controller/infinite-list-controller.js +219 -189
  35. package/dist/controller/inspector.d.ts +49 -0
  36. package/dist/controller/inspector.js +64 -0
  37. package/dist/controller/job-monitor.d.ts +3 -4
  38. package/dist/controller/job-monitor.js +37 -47
  39. package/dist/controller/kanban-board.d.ts +2 -3
  40. package/dist/controller/kanban-board.js +43 -24
  41. package/dist/controller/list-controller.d.ts +5 -5
  42. package/dist/controller/list-controller.js +444 -400
  43. package/dist/controller/list-params.js +96 -87
  44. package/dist/controller/pivot-grid.js +89 -95
  45. package/dist/controller/prev-next.d.ts +2 -2
  46. package/dist/controller/prev-next.js +74 -54
  47. package/dist/controller/record-controllers.d.ts +14 -6
  48. package/dist/controller/record-controllers.js +288 -257
  49. package/dist/controller/record-diff.d.ts +21 -0
  50. package/dist/controller/record-diff.js +50 -0
  51. package/dist/controller/record-timeline.js +42 -30
  52. package/dist/controller/record-tree.js +57 -47
  53. package/dist/controller/record-workflow.d.ts +38 -16
  54. package/dist/controller/record-workflow.js +85 -64
  55. package/dist/controller/reference-many-controller.js +198 -192
  56. package/dist/controller/reference-one-controller.d.ts +1 -1
  57. package/dist/controller/reference-one-controller.js +63 -56
  58. package/dist/controller/saved-queries.js +104 -91
  59. package/dist/controller/translate-fallback.js +10 -6
  60. package/dist/csv/export.d.ts +2 -1
  61. package/dist/csv/export.js +43 -40
  62. package/dist/csv/import-controller.js +185 -169
  63. package/dist/csv/match-headers.js +29 -22
  64. package/dist/csv/parse-with.js +60 -54
  65. package/dist/csv/run-import.js +69 -72
  66. package/dist/csv/validate-rows.d.ts +1 -1
  67. package/dist/csv/validate-rows.js +65 -52
  68. package/dist/data/cache-ops.d.ts +5 -4
  69. package/dist/data/cache-ops.js +208 -188
  70. package/dist/data/combine-data-providers.js +25 -23
  71. package/dist/data/data-provider-log.d.ts +45 -0
  72. package/dist/data/data-provider-log.js +93 -0
  73. package/dist/data/errors.js +14 -10
  74. package/dist/data/fake-provider.d.ts +6 -3
  75. package/dist/data/fake-provider.js +129 -114
  76. package/dist/data/fetch-utils.js +61 -44
  77. package/dist/data/filter-branch.d.ts +5 -0
  78. package/dist/data/filter-branch.js +10 -0
  79. package/dist/data/get-many-aggregate.js +84 -76
  80. package/dist/data/http-error.js +17 -12
  81. package/dist/data/keys.js +50 -36
  82. package/dist/data/mutation-options.d.ts +14 -9
  83. package/dist/data/mutation-options.js +93 -109
  84. package/dist/data/query-options.js +73 -68
  85. package/dist/data/remove-empty.d.ts +3 -0
  86. package/dist/data/remove-empty.js +11 -0
  87. package/dist/data/undoable.d.ts +2 -2
  88. package/dist/data/undoable.js +190 -174
  89. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  90. package/dist/data/with-lifecycle-callbacks.js +66 -52
  91. package/dist/environment/application-update-store.js +58 -55
  92. package/dist/environment/offline-store.js +21 -21
  93. package/dist/form/array-binding.d.ts +30 -11
  94. package/dist/form/array-binding.js +184 -166
  95. package/dist/form/choice-ids.d.ts +7 -0
  96. package/dist/form/choice-ids.js +11 -0
  97. package/dist/form/choices-controller.d.ts +3 -2
  98. package/dist/form/choices-controller.js +302 -275
  99. package/dist/form/date-format.d.ts +38 -0
  100. package/dist/form/date-format.js +89 -0
  101. package/dist/form/dirty-values.d.ts +2 -2
  102. package/dist/form/dirty-values.js +22 -11
  103. package/dist/form/duration.d.ts +22 -0
  104. package/dist/form/duration.js +49 -0
  105. package/dist/form/edit-create-form.d.ts +12 -5
  106. package/dist/form/edit-create-form.js +243 -188
  107. package/dist/form/event-value.d.ts +6 -0
  108. package/dist/form/event-value.js +17 -0
  109. package/dist/form/field-error.d.ts +14 -0
  110. package/dist/form/field-error.js +28 -0
  111. package/dist/form/field-name.d.ts +20 -0
  112. package/dist/form/field-name.js +6 -0
  113. package/dist/form/filter-form-values.js +63 -49
  114. package/dist/form/form-controller.d.ts +9 -1
  115. package/dist/form/form-controller.js +165 -160
  116. package/dist/form/form-groups.d.ts +4 -3
  117. package/dist/form/form-groups.js +79 -79
  118. package/dist/form/form-handle.d.ts +175 -0
  119. package/dist/form/form-handle.js +68 -0
  120. package/dist/form/input-binding.d.ts +56 -31
  121. package/dist/form/input-binding.js +169 -118
  122. package/dist/form/input-values.d.ts +130 -0
  123. package/dist/form/input-values.js +177 -0
  124. package/dist/form/json-text.js +51 -37
  125. package/dist/form/save-pipeline.d.ts +6 -8
  126. package/dist/form/save-pipeline.js +91 -82
  127. package/dist/form/set-form-values.d.ts +6 -0
  128. package/dist/form/set-form-values.js +10 -0
  129. package/dist/form/source.js +97 -65
  130. package/dist/form/text-of.d.ts +2 -0
  131. package/dist/form/text-of.js +7 -0
  132. package/dist/form/validators.d.ts +58 -19
  133. package/dist/form/validators.js +97 -71
  134. package/dist/geo/bbox.d.ts +5 -4
  135. package/dist/geo/bbox.js +69 -48
  136. package/dist/geo/bounds.d.ts +14 -3
  137. package/dist/geo/bounds.js +98 -68
  138. package/dist/geo/constraints.js +88 -93
  139. package/dist/geo/coordinates.js +31 -23
  140. package/dist/geo/editor.d.ts +13 -13
  141. package/dist/geo/editor.js +176 -153
  142. package/dist/geo/geocoding.d.ts +4 -4
  143. package/dist/geo/geocoding.js +38 -41
  144. package/dist/geo/geojson.d.ts +12 -74
  145. package/dist/geo/geojson.js +52 -40
  146. package/dist/geo/geometry-math.d.ts +4 -4
  147. package/dist/geo/geometry-math.js +100 -93
  148. package/dist/geo/index.d.ts +7 -6
  149. package/dist/geo/map-layers.d.ts +42 -7
  150. package/dist/geo/map-layers.js +88 -62
  151. package/dist/geo/map-style.js +31 -32
  152. package/dist/geo/messages.js +48 -45
  153. package/dist/geo/nominatim.js +88 -80
  154. package/dist/geo/osm-presets.d.ts +4 -3
  155. package/dist/geo/osm-presets.js +108 -86
  156. package/dist/geo/osm-tags.js +14 -12
  157. package/dist/geo/overpass.d.ts +14 -2
  158. package/dist/geo/overpass.js +83 -90
  159. package/dist/geo/shape-value.d.ts +9 -5
  160. package/dist/geo/shape-value.js +169 -146
  161. package/dist/geo/snap-to-roads.d.ts +3 -3
  162. package/dist/geo/snap-to-roads.js +55 -50
  163. package/dist/geo/throttle.js +36 -39
  164. package/dist/guards.d.ts +37 -0
  165. package/dist/guards.js +62 -0
  166. package/dist/i18n/default-messages.js +34 -29
  167. package/dist/i18n/english-messages.js +187 -189
  168. package/dist/i18n/i18n.d.ts +2 -2
  169. package/dist/i18n/i18n.js +47 -60
  170. package/dist/i18n/inflection.d.ts +34 -0
  171. package/dist/i18n/inflection.js +82 -0
  172. package/dist/i18n/messages.d.ts +10 -2
  173. package/dist/i18n/messages.js +17 -4
  174. package/dist/i18n/missing-keys.js +66 -62
  175. package/dist/i18n/translatable.d.ts +16 -1
  176. package/dist/i18n/translatable.js +40 -47
  177. package/dist/index.d.ts +71 -24
  178. package/dist/index.js +40 -288
  179. package/dist/json/deep-equal.d.ts +8 -0
  180. package/dist/json/deep-equal.js +24 -0
  181. package/dist/json/strip-json-comments.js +38 -42
  182. package/dist/noop.d.ts +2 -0
  183. package/dist/noop.js +4 -0
  184. package/dist/notification/error-message.d.ts +2 -0
  185. package/dist/notification/error-message.js +10 -0
  186. package/dist/notification/notification-store.d.ts +3 -2
  187. package/dist/notification/notification-store.js +41 -45
  188. package/dist/onboarding-tour/placement.js +63 -78
  189. package/dist/onboarding-tour/tour-controller.js +46 -54
  190. package/dist/realtime/add-events-for-mutations.js +75 -81
  191. package/dist/realtime/announce-lock.js +16 -13
  192. package/dist/realtime/broadcast-channel-transport.js +65 -59
  193. package/dist/realtime/copy-members.js +22 -14
  194. package/dist/realtime/in-memory-lock-provider.js +40 -48
  195. package/dist/realtime/live.js +95 -101
  196. package/dist/realtime/lock-options.js +16 -16
  197. package/dist/realtime/lock-session.js +91 -78
  198. package/dist/realtime/memory-transport.js +25 -25
  199. package/dist/realtime/presence.js +120 -122
  200. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  201. package/dist/realtime/realtime-data-provider.js +45 -42
  202. package/dist/realtime/reconnect-backoff.js +17 -0
  203. package/dist/realtime/sse-transport.d.ts +31 -0
  204. package/dist/realtime/sse-transport.js +235 -0
  205. package/dist/realtime/status-store.js +23 -28
  206. package/dist/realtime/topic-hub.js +62 -71
  207. package/dist/realtime/topics.js +10 -12
  208. package/dist/realtime/types.js +10 -12
  209. package/dist/realtime/unread-count.js +21 -21
  210. package/dist/realtime/websocket-transport.d.ts +32 -0
  211. package/dist/realtime/websocket-transport.js +306 -0
  212. package/dist/resource/describe-resource.d.ts +2 -0
  213. package/dist/resource/describe-resource.js +232 -219
  214. package/dist/resource/get-at-path.js +29 -14
  215. package/dist/resource/identifier.d.ts +18 -0
  216. package/dist/resource/identifier.js +27 -0
  217. package/dist/resource/record.d.ts +4 -0
  218. package/dist/resource/record.js +10 -0
  219. package/dist/resource/register.d.ts +7 -0
  220. package/dist/resource/registry-store.d.ts +45 -15
  221. package/dist/resource/registry-store.js +67 -60
  222. package/dist/resource/registry.d.ts +10 -0
  223. package/dist/resource/registry.js +8 -4
  224. package/dist/resource/source-path.d.ts +16 -1
  225. package/dist/rich-text/rich-text-extras.d.ts +132 -0
  226. package/dist/rich-text/rich-text-extras.js +179 -0
  227. package/dist/rich-text/rich-text-image.d.ts +124 -0
  228. package/dist/rich-text/rich-text-image.js +163 -0
  229. package/dist/rich-text/rich-text.d.ts +6 -0
  230. package/dist/rich-text/rich-text.js +125 -88
  231. package/dist/router/location.d.ts +22 -4
  232. package/dist/router/location.js +41 -18
  233. package/dist/router/match.js +47 -39
  234. package/dist/router/paths.js +40 -34
  235. package/dist/router/record-from-location.d.ts +1 -1
  236. package/dist/router/record-from-location.js +25 -17
  237. package/dist/store/adapters.js +51 -55
  238. package/dist/store/micro-store.js +57 -59
  239. package/dist/store/shallow-equal.d.ts +12 -0
  240. package/dist/store/shallow-equal.js +20 -0
  241. package/dist/store/store.js +54 -57
  242. package/package.json +13 -11
@@ -1,36 +1,48 @@
1
1
  import { getAtPath } from "../resource/get-at-path.js";
2
2
  import { parseCalendarDate } from "./calendar-dates.js";
3
+ // src/controller/record-timeline.ts
4
+ /**
5
+ * Pure rules for a record timeline. No react, no vue: both RecordTimeline
6
+ * adapters read the same entries from the same records and print the same
7
+ * relative time text.
8
+ */
9
+ /**
10
+ * One entry per record, in list order: the data provider's sort decides the
11
+ * order, and the records are not changed.
12
+ */
3
13
  function toTimelineEntries(records, options) {
4
- const { messageSource, timestampSource, userSource, iconSource } = options;
5
- return records.map((record) => {
6
- const message = getAtPath(record, messageSource);
7
- const user = userSource === void 0 ? void 0 : getAtPath(record, userSource);
8
- const icon = iconSource === void 0 ? void 0 : getAtPath(record, iconSource);
9
- return {
10
- id: record.id,
11
- record,
12
- message: message == null ? "" : String(message),
13
- date: parseCalendarDate(getAtPath(record, timestampSource)),
14
- user: user == null || user === "" ? void 0 : String(user),
15
- icon: icon == null || icon === "" ? void 0 : String(icon)
16
- };
17
- });
14
+ const { messageSource, timestampSource, userSource, iconSource } = options;
15
+ return records.map((record) => {
16
+ const message = getAtPath(record, messageSource);
17
+ const user = userSource === void 0 ? void 0 : getAtPath(record, userSource);
18
+ const icon = iconSource === void 0 ? void 0 : getAtPath(record, iconSource);
19
+ return {
20
+ id: record.id,
21
+ record,
22
+ message: message == null ? "" : String(message),
23
+ date: parseCalendarDate(getAtPath(record, timestampSource)),
24
+ user: user == null || user === "" ? void 0 : String(user),
25
+ icon: icon == null || icon === "" ? void 0 : String(icon)
26
+ };
27
+ });
18
28
  }
19
- const MINUTE = 60;
20
- const HOUR = 3600;
21
- const DAY = 86400;
22
- const MONTH = DAY * 30;
29
+ var MINUTE = 60;
30
+ var HOUR = 3600;
31
+ var DAY = 86400;
32
+ var MONTH = DAY * 30;
33
+ /**
34
+ * "2 hours ago" or "in 3 days", from the distance between `date` and `now`
35
+ * in whole seconds, minutes, hours, days or 30-day months. It takes `now` so
36
+ * a caller decides what the present is.
37
+ */
23
38
  function formatRelativeTime(date, now, locale) {
24
- const delta = (date.getTime() - now.getTime()) / 1e3;
25
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
26
- const distance = Math.abs(delta);
27
- if (distance < MINUTE) return rtf.format(Math.round(delta), "second");
28
- if (distance < HOUR) return rtf.format(Math.round(delta / MINUTE), "minute");
29
- if (distance < DAY) return rtf.format(Math.round(delta / HOUR), "hour");
30
- if (distance < MONTH) return rtf.format(Math.round(delta / DAY), "day");
31
- return rtf.format(Math.round(delta / MONTH), "month");
39
+ const delta = (date.getTime() - now.getTime()) / 1e3;
40
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
41
+ const distance = Math.abs(delta);
42
+ if (distance < MINUTE) return rtf.format(Math.round(delta), "second");
43
+ if (distance < HOUR) return rtf.format(Math.round(delta / MINUTE), "minute");
44
+ if (distance < DAY) return rtf.format(Math.round(delta / HOUR), "hour");
45
+ if (distance < MONTH) return rtf.format(Math.round(delta / DAY), "day");
46
+ return rtf.format(Math.round(delta / MONTH), "month");
32
47
  }
33
- export {
34
- formatRelativeTime,
35
- toTimelineEntries
36
- };
48
+ export { formatRelativeTime, toTimelineEntries };
@@ -1,50 +1,60 @@
1
1
  import { getAtPath } from "../resource/get-at-path.js";
2
+ // src/controller/record-tree.ts
3
+ /**
4
+ * Pure rule for nesting a flat record list into a tree from a parent field.
5
+ * No react, no vue: both TreeList adapters build the same tree from the same
6
+ * records, and no record disappears when the data holds a parent cycle.
7
+ */
8
+ /**
9
+ * Roots and siblings keep list order, so the data provider's sort decides
10
+ * the order. A record is a root when its parent value is null, undefined or
11
+ * `""`, names itself, or names no record in the list (its parent may be on
12
+ * another page). A cycle keeps its first record in list order as a root, with
13
+ * the rest beneath it. A later record that repeats an id is left out.
14
+ */
2
15
  function buildRecordTree(records, parentSource) {
3
- const nodes = /* @__PURE__ */ new Map();
4
- const order = /* @__PURE__ */ new Map();
5
- for (const record of records) {
6
- const id = String(record.id);
7
- if (nodes.has(id)) continue;
8
- order.set(id, nodes.size);
9
- nodes.set(id, { record, children: [] });
10
- }
11
- const parents = /* @__PURE__ */ new Map();
12
- for (const [id, node] of nodes) {
13
- const value = getAtPath(node.record, parentSource);
14
- const parent = value == null || value === "" ? null : String(value);
15
- parents.set(
16
- id,
17
- parent !== null && parent !== id && nodes.has(parent) ? parent : null
18
- );
19
- }
20
- const settled = /* @__PURE__ */ new Set();
21
- for (const start of nodes.keys()) {
22
- const path = [];
23
- const onPath = /* @__PURE__ */ new Set();
24
- let current = start;
25
- while (current !== null && !settled.has(current)) {
26
- if (onPath.has(current)) {
27
- const cycle = path.slice(path.indexOf(current));
28
- const first = cycle.reduce(
29
- (a, b) => order.get(a) < order.get(b) ? a : b
30
- );
31
- parents.set(first, null);
32
- break;
33
- }
34
- path.push(current);
35
- onPath.add(current);
36
- current = parents.get(current) ?? null;
37
- }
38
- for (const id of path) settled.add(id);
39
- }
40
- const roots = [];
41
- for (const [id, node] of nodes) {
42
- const parent = parents.get(id) ?? null;
43
- const siblings = parent === null ? roots : nodes.get(parent).children;
44
- siblings.push(node);
45
- }
46
- return roots;
16
+ const nodes = /* @__PURE__ */ new Map();
17
+ const order = /* @__PURE__ */ new Map();
18
+ for (const record of records) {
19
+ const id = String(record.id);
20
+ if (nodes.has(id)) continue;
21
+ order.set(id, nodes.size);
22
+ nodes.set(id, {
23
+ record,
24
+ children: []
25
+ });
26
+ }
27
+ const parents = /* @__PURE__ */ new Map();
28
+ for (const [id, node] of nodes) {
29
+ const value = getAtPath(node.record, parentSource);
30
+ const parent = value == null || value === "" ? null : String(value);
31
+ parents.set(id, parent !== null && parent !== id && nodes.has(parent) ? parent : null);
32
+ }
33
+ const settled = /* @__PURE__ */ new Set();
34
+ for (const start of nodes.keys()) {
35
+ const path = [];
36
+ const onPath = /* @__PURE__ */ new Set();
37
+ let current = start;
38
+ while (current !== null && !settled.has(current)) {
39
+ if (onPath.has(current)) {
40
+ const cycle = path.slice(path.indexOf(current));
41
+ const rank = (id) => order.get(id) ?? Number.POSITIVE_INFINITY;
42
+ const first = cycle.reduce((a, b) => rank(a) < rank(b) ? a : b);
43
+ parents.set(first, null);
44
+ break;
45
+ }
46
+ path.push(current);
47
+ onPath.add(current);
48
+ current = parents.get(current) ?? null;
49
+ }
50
+ for (const id of path) settled.add(id);
51
+ }
52
+ const roots = [];
53
+ for (const [id, node] of nodes) {
54
+ const parent = parents.get(id) ?? null;
55
+ const parentNode = parent === null ? void 0 : nodes.get(parent);
56
+ (parentNode ? parentNode.children : roots).push(node);
57
+ }
58
+ return roots;
47
59
  }
48
- export {
49
- buildRecordTree
50
- };
60
+ export { buildRecordTree };
@@ -1,20 +1,19 @@
1
1
  /**
2
- * Pure rules behind an approval button and a status-transition button, so
3
- * every adapter decides the same way.
2
+ * Pure rules behind an approval button, a rejection button and a
3
+ * status-transition button, so every adapter decides the same way.
4
4
  *
5
5
  * These are UI rules, not security ones: the approvers list is a field on the
6
6
  * record that the caller writes back whole, so two approvers acting at once
7
7
  * can overwrite each other. The server has to enforce the count and who may
8
- * approve.
8
+ * approve or reject.
9
9
  */
10
- import type { SourceKey } from "../resource/source-path.js";
11
10
  type Id = string | number;
12
- export interface ApprovalOptions<TRecord = Record<string, unknown>> {
11
+ export interface ApprovalOptions<TRecord extends object> {
13
12
  /**
14
13
  * Record field holding the approver list, a top-level key: it is read as
15
14
  * `record[approverSource]` and written back whole. Default `"approvers"`.
16
15
  */
17
- approverSource?: SourceKey<TRecord>;
16
+ approverSource?: keyof TRecord & string;
18
17
  /** Approvals needed. Floored, minimum 1; `NaN` reads as 2. Default 2. */
19
18
  required?: number;
20
19
  /** The current user. No identity means nobody can approve. */
@@ -23,21 +22,37 @@ export interface ApprovalOptions<TRecord = Record<string, unknown>> {
23
22
  * Record field `buildApprovalUpdate` writes the status to, a top-level key.
24
23
  * Default `"status"`.
25
24
  */
26
- statusSource?: SourceKey<TRecord>;
25
+ statusSource?: keyof TRecord & string;
27
26
  /** Status written while approvals are short of `required`. Default `"pending"`. */
28
- pendingValue?: unknown;
27
+ pendingValue?: string;
29
28
  /** Status written once approvals reach `required`. Default `"approved"`. */
30
- approvedValue?: unknown;
29
+ approvedValue?: string;
31
30
  }
32
31
  /**
33
32
  * What `buildApprovalUpdate` takes. `statusSource` is a write target, and a
34
33
  * record often has no status field until its first approval, so it takes any
35
34
  * string; `approverSource` is read, so it stays a key of the record.
36
35
  */
37
- export type ApprovalUpdateOptions<TRecord = Record<string, unknown>> = Omit<ApprovalOptions<TRecord>, "statusSource"> & {
36
+ export type ApprovalUpdateOptions<TRecord extends object> = Omit<ApprovalOptions<TRecord>, "statusSource"> & {
38
37
  /** Record field the status is written to. Default `"status"`. */
39
38
  statusSource?: string;
40
39
  };
40
+ export interface RejectionOptions {
41
+ /** The current user. No identity means nobody can reject. */
42
+ identityId?: Id | null;
43
+ /** Record field the status is written to and read from. Default `"status"`. */
44
+ statusSource?: string;
45
+ /** Status written on rejection. Default `"rejected"`. */
46
+ rejectedValue?: unknown;
47
+ /** Record field the reason is written to. Default `"rejectionNote"`. */
48
+ noteSource?: string;
49
+ /** Record field the rejecter's id is written to. Default `"rejectedBy"`. */
50
+ rejectedBySource?: string;
51
+ /** Why the record is rejected. Trimmed; an empty one is written as `null`. */
52
+ reason?: string;
53
+ /** Refuse a rejection whose trimmed reason is empty. Default `false`. */
54
+ requireReason?: boolean;
55
+ }
41
56
  export interface ApprovalState {
42
57
  /** Distinct approver ids, as strings. */
43
58
  approvers: string[];
@@ -47,24 +62,31 @@ export interface ApprovalState {
47
62
  isComplete: boolean;
48
63
  canApprove: boolean;
49
64
  }
50
- export interface TransitionOptions<TRecord = Record<string, unknown>> {
65
+ export interface TransitionOptions<TRecord extends object> {
51
66
  /** Record field holding the status, a top-level key. Default `"status"`. */
52
- source?: SourceKey<TRecord>;
67
+ source?: keyof TRecord & string;
53
68
  /** Next states by current state. */
54
69
  transitions: Record<string, readonly string[]>;
55
70
  /** Predicates keyed `"<from>-><to>"`. A transition with no guard is allowed. */
56
- guards?: Record<string, (record: Record<string, unknown>) => boolean>;
71
+ guards?: Record<string, (record: TRecord) => boolean>;
57
72
  }
58
73
  export interface AllowedTransitions {
59
74
  current: string;
60
75
  allowed: string[];
61
76
  }
62
- export declare function getApprovalState<TRecord = Record<string, unknown>>(record: TRecord & object, options?: ApprovalOptions<TRecord>): ApprovalState;
77
+ export declare function getApprovalState<TRecord extends object>(record: TRecord & object, options?: ApprovalOptions<TRecord>): ApprovalState;
63
78
  /**
64
79
  * The patch that records `identityId`'s approval, or `null` when they cannot
65
80
  * approve. Stored items stay as they were and the identity is appended in its
66
81
  * own type.
67
82
  */
68
- export declare function buildApprovalUpdate<TRecord = Record<string, unknown>>(record: TRecord & object, options?: ApprovalUpdateOptions<TRecord>): Record<string, unknown> | null;
69
- export declare function getAllowedTransitions<TRecord = Record<string, unknown>>(record: TRecord & object, options: TransitionOptions<TRecord>): AllowedTransitions;
83
+ export declare function buildApprovalUpdate<TRecord extends object>(record: TRecord & object, options?: ApprovalUpdateOptions<TRecord>): Record<string, unknown> | null;
84
+ /**
85
+ * The patch that records `identityId`'s rejection, or `null` when they cannot
86
+ * reject: no identity, a blank reason while `requireReason` is set, or a record
87
+ * whose status already equals `rejectedValue`. All three sources are write
88
+ * targets, so they take any string.
89
+ */
90
+ export declare function buildRejectionUpdate<TRecord extends object>(record: TRecord & object, options?: RejectionOptions): Record<string, unknown> | null;
91
+ export declare function getAllowedTransitions<TRecord extends object>(record: TRecord & object, options: TransitionOptions<TRecord>): AllowedTransitions;
70
92
  export {};
@@ -1,74 +1,95 @@
1
- const hasOwn = (obj, key) => Object.hasOwn(obj, key);
1
+ // src/controller/record-workflow.ts
2
+ var hasOwn = (obj, key) => Object.hasOwn(obj, key);
2
3
  function normalizeRequired(required) {
3
- if (required === void 0 || Number.isNaN(required)) return 2;
4
- return Math.max(1, Math.floor(required));
4
+ if (required === void 0 || Number.isNaN(required)) return 2;
5
+ return Math.max(1, Math.floor(required));
5
6
  }
6
7
  function storedItems(record, source) {
7
- const value = record[source];
8
- return Array.isArray(value) ? value : [];
8
+ const value = record[source];
9
+ return Array.isArray(value) ? value : [];
10
+ }
11
+ /**
12
+ * The approver list: `approverSource` when given, else the `"approvers"` field
13
+ * if the record has one. The default is a literal the record's type may not
14
+ * name, so it is read only after an `in` check.
15
+ */
16
+ function approverItems(record, approverSource) {
17
+ if (approverSource !== void 0) return storedItems(record, approverSource);
18
+ return "approvers" in record ? storedItems(record, "approvers") : [];
9
19
  }
10
20
  function getApprovalState(record, options = {}) {
11
- const { approverSource = "approvers", identityId } = options;
12
- const required = normalizeRequired(options.required);
13
- const approvers = [
14
- ...new Set(storedItems(record, approverSource).map((item) => String(item)))
15
- ];
16
- const count = approvers.length;
17
- const hasIdentity = identityId !== void 0 && identityId !== null;
18
- const hasApproved = hasIdentity && approvers.includes(String(identityId));
19
- const isComplete = count >= required;
20
- return {
21
- approvers,
22
- count,
23
- required,
24
- hasApproved,
25
- isComplete,
26
- canApprove: hasIdentity && !hasApproved && !isComplete
27
- };
21
+ const { approverSource, identityId } = options;
22
+ const required = normalizeRequired(options.required);
23
+ const approvers = [...new Set(approverItems(record, approverSource).map((item) => String(item)))];
24
+ const count = approvers.length;
25
+ const hasIdentity = identityId !== void 0 && identityId !== null;
26
+ const hasApproved = hasIdentity && approvers.includes(String(identityId));
27
+ const isComplete = count >= required;
28
+ return {
29
+ approvers,
30
+ count,
31
+ required,
32
+ hasApproved,
33
+ isComplete,
34
+ canApprove: hasIdentity && !hasApproved && !isComplete
35
+ };
28
36
  }
37
+ /**
38
+ * The patch that records `identityId`'s approval, or `null` when they cannot
39
+ * approve. Stored items stay as they were and the identity is appended in its
40
+ * own type.
41
+ */
29
42
  function buildApprovalUpdate(record, options = {}) {
30
- const {
31
- approverSource = "approvers",
32
- statusSource = "status",
33
- identityId,
34
- pendingValue = "pending",
35
- approvedValue = "approved"
36
- } = options;
37
- const state = getApprovalState(record, {
38
- approverSource: options.approverSource,
39
- required: options.required,
40
- identityId
41
- });
42
- if (!state.canApprove || identityId === void 0 || identityId === null) {
43
- return null;
44
- }
45
- return {
46
- [approverSource]: [...storedItems(record, approverSource), identityId],
47
- [statusSource]: state.count + 1 >= state.required ? approvedValue : pendingValue
48
- };
43
+ const { approverSource, statusSource = "status", identityId, pendingValue = "pending", approvedValue = "approved" } = options;
44
+ const state = getApprovalState(record, {
45
+ approverSource: options.approverSource,
46
+ required: options.required,
47
+ identityId
48
+ });
49
+ if (!state.canApprove || identityId === void 0 || identityId === null) return null;
50
+ return {
51
+ [approverSource ?? "approvers"]: [...approverItems(record, approverSource), identityId],
52
+ [statusSource]: state.count + 1 >= state.required ? approvedValue : pendingValue
53
+ };
54
+ }
55
+ /**
56
+ * The patch that records `identityId`'s rejection, or `null` when they cannot
57
+ * reject: no identity, a blank reason while `requireReason` is set, or a record
58
+ * whose status already equals `rejectedValue`. All three sources are write
59
+ * targets, so they take any string.
60
+ */
61
+ function buildRejectionUpdate(record, options = {}) {
62
+ const { identityId, statusSource = "status", rejectedValue = "rejected", noteSource = "rejectionNote", rejectedBySource = "rejectedBy", requireReason = false } = options;
63
+ const reason = options.reason?.trim() ?? "";
64
+ if (identityId === void 0 || identityId === null) return null;
65
+ if (requireReason && reason === "") return null;
66
+ if (Reflect.get(record, statusSource) === rejectedValue) return null;
67
+ return {
68
+ [statusSource]: rejectedValue,
69
+ [rejectedBySource]: identityId,
70
+ [noteSource]: reason === "" ? null : reason
71
+ };
49
72
  }
50
73
  function getAllowedTransitions(record, options) {
51
- const { source = "status", transitions, guards } = options;
52
- const current = String(record[source] ?? "");
53
- const next = hasOwn(transitions, current) ? transitions[current] : void 0;
54
- const allowed = [];
55
- for (const to of Array.isArray(next) ? next : []) {
56
- if (allowed.includes(to)) continue;
57
- const key = `${current}->${to}`;
58
- const guard = guards && hasOwn(guards, key) ? guards[key] : void 0;
59
- if (guard) {
60
- try {
61
- if (!guard(record)) continue;
62
- } catch {
63
- continue;
64
- }
65
- }
66
- allowed.push(to);
67
- }
68
- return { current, allowed };
74
+ const { source, transitions, guards } = options;
75
+ const value = source !== void 0 ? record[source] : "status" in record ? record.status : void 0;
76
+ const current = String(value ?? "");
77
+ const next = hasOwn(transitions, current) ? transitions[current] : void 0;
78
+ const allowed = [];
79
+ for (const to of Array.isArray(next) ? next : []) {
80
+ if (allowed.includes(to)) continue;
81
+ const key = `${current}->${to}`;
82
+ const guard = guards && hasOwn(guards, key) ? guards[key] : void 0;
83
+ if (guard) try {
84
+ if (!guard(record)) continue;
85
+ } catch {
86
+ continue;
87
+ }
88
+ allowed.push(to);
89
+ }
90
+ return {
91
+ current,
92
+ allowed
93
+ };
69
94
  }
70
- export {
71
- buildApprovalUpdate,
72
- getAllowedTransitions,
73
- getApprovalState
74
- };
95
+ export { buildApprovalUpdate, buildRejectionUpdate, getAllowedTransitions, getApprovalState };