@svgrid/enterprise 3.1.8 → 3.1.10

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 (268) hide show
  1. package/README.md +1 -0
  2. package/dist/SvAlertRuleEditor.svelte +16 -5
  3. package/dist/SvGridAlerts.svelte +8 -0
  4. package/dist/SvGridAlerts.svelte.d.ts +4 -0
  5. package/dist/SvGridBoard.svelte +69 -10
  6. package/dist/SvGridBulkEditDrawer.svelte +13 -5
  7. package/dist/SvGridGallery.svelte +286 -0
  8. package/dist/SvGridGallery.svelte.d.ts +30 -0
  9. package/dist/SvGridScheduler.svelte +33 -11
  10. package/dist/SvGridSelectionBar.svelte +47 -0
  11. package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
  12. package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
  13. package/dist/advanced-filter-enable.d.ts +6 -0
  14. package/dist/advanced-filter-enable.js +8 -0
  15. package/dist/alerts/alert-engine.d.ts +7 -0
  16. package/dist/alerts/alert-engine.js +5 -2
  17. package/dist/alerts/condition-editor.d.ts +26 -0
  18. package/dist/alerts/condition-editor.js +17 -0
  19. package/dist/base/index.d.ts +20 -0
  20. package/dist/base/index.js +20 -0
  21. package/dist/bulk-edit.d.ts +11 -0
  22. package/dist/bulk-edit.js +18 -1
  23. package/dist/cdn/svgrid-enterprise.svelte-external.js +11197 -10639
  24. package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
  25. package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
  26. package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
  27. package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
  28. package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
  29. package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
  30. package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
  31. package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
  32. package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
  33. package/dist/designer/assets/index-BmvG99kR.css +1 -0
  34. package/dist/designer/assets/{index-Obk8sI77.js → index-CJodU3tU.js} +307 -307
  35. package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
  36. package/dist/designer/assets/src-C_U1NKOY.js +58 -0
  37. package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
  38. package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
  39. package/dist/designer/index.html +11 -7
  40. package/dist/expressions/compile.js +10 -0
  41. package/dist/expressions/evaluate.js +18 -0
  42. package/dist/expressions/expression-types.d.ts +10 -0
  43. package/dist/expressions/kinds.d.ts +28 -0
  44. package/dist/expressions/kinds.js +19 -0
  45. package/dist/expressions/parse.js +9 -1
  46. package/dist/fields/AttachmentCell.svelte +242 -0
  47. package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
  48. package/dist/fields/AttachmentDialog.svelte +357 -0
  49. package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
  50. package/dist/fields/AttachmentEditor.svelte +41 -0
  51. package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
  52. package/dist/fields/AttachmentPreview.svelte +72 -0
  53. package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
  54. package/dist/fields/BarcodeCell.svelte +20 -0
  55. package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
  56. package/dist/fields/BaseAutomations.svelte +558 -0
  57. package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
  58. package/dist/fields/BaseChartView.svelte +47 -0
  59. package/dist/fields/BaseChartView.svelte.d.ts +8 -0
  60. package/dist/fields/BaseTableView.svelte +185 -0
  61. package/dist/fields/BaseTableView.svelte.d.ts +37 -0
  62. package/dist/fields/BaseTabs.svelte +308 -0
  63. package/dist/fields/BaseTabs.svelte.d.ts +21 -0
  64. package/dist/fields/ButtonCell.svelte +47 -0
  65. package/dist/fields/ButtonCell.svelte.d.ts +20 -0
  66. package/dist/fields/CheckboxCell.svelte +46 -0
  67. package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
  68. package/dist/fields/DeleteFieldConfirm.svelte +80 -0
  69. package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
  70. package/dist/fields/DeleteRecordConfirm.svelte +48 -0
  71. package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
  72. package/dist/fields/FieldCell.svelte +80 -0
  73. package/dist/fields/FieldCell.svelte.d.ts +9 -0
  74. package/dist/fields/FieldEditor.svelte +321 -0
  75. package/dist/fields/FieldEditor.svelte.d.ts +10 -0
  76. package/dist/fields/FieldInput.svelte +188 -0
  77. package/dist/fields/FieldInput.svelte.d.ts +29 -0
  78. package/dist/fields/FieldsHeaderEnd.svelte +146 -0
  79. package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
  80. package/dist/fields/FootRowAdder.svelte +8 -0
  81. package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
  82. package/dist/fields/GroupHeader.svelte +127 -0
  83. package/dist/fields/GroupHeader.svelte.d.ts +24 -0
  84. package/dist/fields/LinkCell.svelte +46 -0
  85. package/dist/fields/LinkCell.svelte.d.ts +9 -0
  86. package/dist/fields/LinkPickerEditor.svelte +172 -0
  87. package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
  88. package/dist/fields/LinkPreview.svelte +60 -0
  89. package/dist/fields/LinkPreview.svelte.d.ts +7 -0
  90. package/dist/fields/LinkedRecordView.svelte +94 -0
  91. package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
  92. package/dist/fields/LinkedRecordsCell.svelte +59 -0
  93. package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
  94. package/dist/fields/LongTextEditor.svelte +157 -0
  95. package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
  96. package/dist/fields/NumberEditor.svelte +309 -0
  97. package/dist/fields/NumberEditor.svelte.d.ts +12 -0
  98. package/dist/fields/OptionsEditor.svelte +117 -0
  99. package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
  100. package/dist/fields/PercentCell.svelte +21 -0
  101. package/dist/fields/PercentCell.svelte.d.ts +8 -0
  102. package/dist/fields/PropertyInput.svelte +204 -0
  103. package/dist/fields/PropertyInput.svelte.d.ts +11 -0
  104. package/dist/fields/RatingCell.svelte +75 -0
  105. package/dist/fields/RatingCell.svelte.d.ts +16 -0
  106. package/dist/fields/RatingEditor.svelte +82 -0
  107. package/dist/fields/RatingEditor.svelte.d.ts +9 -0
  108. package/dist/fields/RecordView.svelte +310 -0
  109. package/dist/fields/RecordView.svelte.d.ts +11 -0
  110. package/dist/fields/RichTextCell.svelte +30 -0
  111. package/dist/fields/RichTextCell.svelte.d.ts +5 -0
  112. package/dist/fields/RowAdder.svelte +93 -0
  113. package/dist/fields/RowAdder.svelte.d.ts +7 -0
  114. package/dist/fields/RowHeader.svelte +99 -0
  115. package/dist/fields/RowHeader.svelte.d.ts +11 -0
  116. package/dist/fields/SelectCell.svelte +35 -0
  117. package/dist/fields/SelectCell.svelte.d.ts +15 -0
  118. package/dist/fields/SelectEditor.svelte +350 -0
  119. package/dist/fields/SelectEditor.svelte.d.ts +17 -0
  120. package/dist/fields/SummaryCell.svelte +160 -0
  121. package/dist/fields/SummaryCell.svelte.d.ts +11 -0
  122. package/dist/fields/SvBase.svelte +545 -0
  123. package/dist/fields/SvBase.svelte.d.ts +30 -0
  124. package/dist/fields/SvBaseForm.svelte +295 -0
  125. package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
  126. package/dist/fields/SvBaseViewBar.svelte +644 -0
  127. package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
  128. package/dist/fields/UserCell.svelte +39 -0
  129. package/dist/fields/UserCell.svelte.d.ts +9 -0
  130. package/dist/fields/UserFace.svelte +30 -0
  131. package/dist/fields/UserFace.svelte.d.ts +12 -0
  132. package/dist/fields/activity.svelte.d.ts +50 -0
  133. package/dist/fields/activity.svelte.js +110 -0
  134. package/dist/fields/attachments.d.ts +95 -0
  135. package/dist/fields/attachments.js +145 -0
  136. package/dist/fields/automation-outbound.d.ts +46 -0
  137. package/dist/fields/automation-outbound.js +61 -0
  138. package/dist/fields/automation-server.d.ts +53 -0
  139. package/dist/fields/automation-server.js +333 -0
  140. package/dist/fields/automations.svelte.d.ts +208 -0
  141. package/dist/fields/automations.svelte.js +437 -0
  142. package/dist/fields/bar-request.svelte.d.ts +16 -0
  143. package/dist/fields/bar-request.svelte.js +15 -0
  144. package/dist/fields/base-alerts.d.ts +24 -0
  145. package/dist/fields/base-alerts.js +64 -0
  146. package/dist/fields/base-connect.d.ts +82 -0
  147. package/dist/fields/base-connect.js +271 -0
  148. package/dist/fields/base-schema.d.ts +104 -0
  149. package/dist/fields/base-schema.js +266 -0
  150. package/dist/fields/base-sql.d.ts +62 -0
  151. package/dist/fields/base-sql.js +251 -0
  152. package/dist/fields/base-sync.d.ts +142 -0
  153. package/dist/fields/base-sync.js +499 -0
  154. package/dist/fields/base-tables.d.ts +89 -0
  155. package/dist/fields/base-tables.js +349 -0
  156. package/dist/fields/builtin-types.d.ts +13 -0
  157. package/dist/fields/builtin-types.js +773 -0
  158. package/dist/fields/button.d.ts +2 -0
  159. package/dist/fields/button.js +17 -0
  160. package/dist/fields/chart-view.d.ts +35 -0
  161. package/dist/fields/chart-view.js +154 -0
  162. package/dist/fields/column-order.d.ts +6 -0
  163. package/dist/fields/column-order.js +12 -0
  164. package/dist/fields/convert.d.ts +16 -0
  165. package/dist/fields/convert.js +176 -0
  166. package/dist/fields/copy-text.d.ts +13 -0
  167. package/dist/fields/copy-text.js +45 -0
  168. package/dist/fields/editor-request.svelte.d.ts +71 -0
  169. package/dist/fields/editor-request.svelte.js +52 -0
  170. package/dist/fields/field-ops.d.ts +72 -0
  171. package/dist/fields/field-ops.js +334 -0
  172. package/dist/fields/filter-model.d.ts +84 -0
  173. package/dist/fields/filter-model.js +446 -0
  174. package/dist/fields/format.d.ts +109 -0
  175. package/dist/fields/format.js +288 -0
  176. package/dist/fields/formula.d.ts +50 -0
  177. package/dist/fields/formula.js +530 -0
  178. package/dist/fields/grid-access.d.ts +60 -0
  179. package/dist/fields/grid-access.js +120 -0
  180. package/dist/fields/index.d.ts +34 -0
  181. package/dist/fields/index.js +312 -0
  182. package/dist/fields/linked-fields.d.ts +8 -0
  183. package/dist/fields/linked-fields.js +40 -0
  184. package/dist/fields/links.d.ts +10 -0
  185. package/dist/fields/links.js +46 -0
  186. package/dist/fields/name-list.d.ts +16 -0
  187. package/dist/fields/name-list.js +49 -0
  188. package/dist/fields/palette.d.ts +47 -0
  189. package/dist/fields/palette.js +69 -0
  190. package/dist/fields/record-delete.svelte.d.ts +12 -0
  191. package/dist/fields/record-delete.svelte.js +5 -0
  192. package/dist/fields/record-menu.d.ts +9 -0
  193. package/dist/fields/record-menu.js +78 -0
  194. package/dist/fields/registry.svelte.d.ts +176 -0
  195. package/dist/fields/registry.svelte.js +51 -0
  196. package/dist/fields/rich-text.d.ts +23 -0
  197. package/dist/fields/rich-text.js +48 -0
  198. package/dist/fields/row-color-choice.svelte.d.ts +31 -0
  199. package/dist/fields/row-color-choice.svelte.js +25 -0
  200. package/dist/fields/row-colors.d.ts +36 -0
  201. package/dist/fields/row-colors.js +90 -0
  202. package/dist/fields/row-height.d.ts +28 -0
  203. package/dist/fields/row-height.js +50 -0
  204. package/dist/fields/sources.svelte.d.ts +80 -0
  205. package/dist/fields/sources.svelte.js +131 -0
  206. package/dist/fields/summaries.d.ts +18 -0
  207. package/dist/fields/summaries.js +138 -0
  208. package/dist/fields/view-model.d.ts +129 -0
  209. package/dist/fields/view-model.js +492 -0
  210. package/dist/fields/viewbar/ChartPanel.svelte +61 -0
  211. package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
  212. package/dist/fields/viewbar/ColorPanel.svelte +112 -0
  213. package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
  214. package/dist/fields/viewbar/ConditionValue.svelte +153 -0
  215. package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
  216. package/dist/fields/viewbar/FieldIcon.svelte +18 -0
  217. package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
  218. package/dist/fields/viewbar/FilterPanel.svelte +192 -0
  219. package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
  220. package/dist/fields/viewbar/GanttPanel.svelte +60 -0
  221. package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
  222. package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
  223. package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
  224. package/dist/fields/viewbar/SortPanel.svelte +85 -0
  225. package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
  226. package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
  227. package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
  228. package/dist/fields/viewbar/panel-focus.d.ts +3 -0
  229. package/dist/fields/viewbar/panel-focus.js +22 -0
  230. package/dist/fields/viewbar/sort-state.d.ts +20 -0
  231. package/dist/fields/viewbar/sort-state.js +24 -0
  232. package/dist/gallery.d.ts +7 -0
  233. package/dist/gallery.js +36 -0
  234. package/dist/gantt/SvGridGantt.svelte +88 -16
  235. package/dist/gantt/gantt-config.d.ts +28 -0
  236. package/dist/index.d.ts +3 -0
  237. package/dist/index.js +6 -0
  238. package/dist/install.js +3 -0
  239. package/dist/license-core.d.ts +3 -3
  240. package/dist/license.js +1 -0
  241. package/dist/node/studio.js +105 -4
  242. package/dist/scheduler-config.d.ts +11 -0
  243. package/dist/sources/introspect-supabase.js +4 -1
  244. package/dist/sources/rest.js +6 -2
  245. package/dist/studio/ui-components.generated.js +93 -1
  246. package/dist/upgrade-prompt.d.ts +1 -1
  247. package/dist/version.d.ts +2 -2
  248. package/dist/version.js +2 -2
  249. package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
  250. package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
  251. package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
  252. package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
  253. package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
  254. package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
  255. package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
  256. package/dist/wc/sv-sheet-element.js +7556 -7617
  257. package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
  258. package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
  259. package/package.json +14 -3
  260. package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
  261. package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
  262. package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
  263. package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
  264. package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
  265. package/dist/designer/assets/src-DqHCqori.js +0 -58
  266. package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
  267. package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
  268. package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Automations on the server: the ones set to "Runs in: the server".
3
+ *
4
+ * The browser runner runs an automation in the page of whoever made the
5
+ * change. That cannot send an email, keep an API key, notice a change made by
6
+ * another app, or run at 9:00 when nobody has the base open. This runner can:
7
+ * it lives in your server (a SvelteKit route, a worker process), reads and
8
+ * writes the tables through the same `baseSource`s, and hears of changes in
9
+ * one of three ways:
10
+ *
11
+ * - `recordChanged(table, { before, after })`: a change with both sides, as
12
+ * a Supabase database webhook or a Postgres trigger sends it;
13
+ * - `audit(table)`: the `audit` hook of `createKitHandlers`, after every
14
+ * write through your endpoint (the "before" is what it read last);
15
+ * - `tableChanged(table)`: read the table again and compare.
16
+ *
17
+ * `tick()` runs the scheduled ones; call it once a minute (a cron job, a
18
+ * `setInterval`). A change it makes is kept as what it read, so the echo of
19
+ * its own write (the webhook firing for it) is recognized and ignored, and
20
+ * automations setting each other off stop at `MAX_DEPTH`.
21
+ */
22
+ import { cronMatches } from '../scheduling.js';
23
+ import { primaryOf, rowIdOf, syncAllInverses } from './base-tables.js';
24
+ import { newRecordId } from './field-ops.js';
25
+ import { registerLinkedTable } from './sources.svelte.js';
26
+ import { MAX_DEPTH, automationsFor, fieldValueFrom, fillTokens, matches, outbound, runAppAction, tableFields, } from './automations.svelte.js';
27
+ import { callWebhook, sendEmailAction } from './automation-outbound.js';
28
+ let runSeq = 0;
29
+ const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
30
+ export function createAutomationServer(options) {
31
+ const now = options.now ?? (() => new Date());
32
+ const keep = options.keepRuns ?? 200;
33
+ let tables = options.tables.map((t) => ({ ...t, data: [] }));
34
+ let primed = null;
35
+ let chain = Promise.resolve();
36
+ const runs = [];
37
+ const lastFired = new Map();
38
+ const secret = (name) => (typeof options.secrets === 'function' ? options.secrets(name) : options.secrets?.[name]);
39
+ const tableOf = (id) => tables.find((t) => t.id === id);
40
+ const sourceOf = (t) => {
41
+ if (!t.source)
42
+ throw new Error(`Table ${t.name} has no source`);
43
+ return t.source;
44
+ };
45
+ function setData(tableId, change) {
46
+ tables = tables.map((t) => (t.id === tableId ? { ...t, data: change(t.data) } : t));
47
+ // The fields that link back are worked out from the other side.
48
+ for (const t of tables)
49
+ tables = syncAllInverses(tables, t.id);
50
+ }
51
+ /** Read every table once, so later changes have a "before" and links have names. */
52
+ function prime() {
53
+ primed ?? (primed = (async () => {
54
+ for (const t of tables) {
55
+ const rows = await sourceOf(t).load();
56
+ setData(t.id, () => rows);
57
+ }
58
+ for (const t of tables) {
59
+ const id = t.id;
60
+ registerLinkedTable({
61
+ id,
62
+ name: t.name,
63
+ rows: () => tableOf(id)?.data ?? [],
64
+ getRowId: (r) => rowIdOf(tableOf(id) ?? t, r),
65
+ primaryField: primaryOf(t),
66
+ fields: t.columns.filter((d) => d.field).map((d) => ({ field: String(d.field), header: String(d.header ?? d.field), fieldType: d.fieldType, fieldProps: d.fieldProps })),
67
+ });
68
+ }
69
+ })());
70
+ return primed;
71
+ }
72
+ /** Column ids of the fields that differ between two versions of a record. */
73
+ function changedFields(t, before, after) {
74
+ const out = [];
75
+ for (const c of tableFields(t).values())
76
+ if (!same(before[c.field], after[c.field]))
77
+ out.push(c.id);
78
+ return out;
79
+ }
80
+ // ---- What it hears ---------------------------------------------------------------
81
+ /**
82
+ * A record changed: rows as the database has them (`db: false` for rows
83
+ * already in the base's shape). No `after` is a delete; no `before` is
84
+ * read from what it last read.
85
+ */
86
+ async function recordChanged(tableId, change, opts = {}) {
87
+ await prime();
88
+ const t = tableOf(tableId);
89
+ if (!t)
90
+ throw new Error(`No table "${tableId}"`);
91
+ const asPage = (r) => (r ? (opts.db === false ? r : sourceOf(t).toPage(r)) : null);
92
+ const after = asPage(change.after);
93
+ const before = asPage(change.before);
94
+ if (!after) {
95
+ const id = String(before?.id ?? '');
96
+ setData(tableId, (rows) => rows.filter((r) => rowIdOf(t, r) !== id));
97
+ return [];
98
+ }
99
+ const id = String(after.id);
100
+ const known = t.data.find((r) => rowIdOf(t, r) === id);
101
+ // A junction link is not in a database row: keep the one it read.
102
+ const merged = { ...(known ?? {}), ...after };
103
+ // The echo of a change it made, or one it already heard: nothing new.
104
+ if (known && same(known, merged))
105
+ return [];
106
+ const prev = before ? { ...(known ?? {}), ...before } : known;
107
+ setData(tableId, (rows) => (known ? rows.map((r) => (r === known ? merged : r)) : [...rows, merged]));
108
+ const record = tableOf(tableId).data.find((r) => rowIdOf(t, r) === id);
109
+ if (!prev)
110
+ return handle({ type: 'created', table: tableId, record });
111
+ const fields = changedFields(t, prev, record);
112
+ return fields.length ? handle({ type: 'updated', table: tableId, record, before: prev, fields }) : [];
113
+ }
114
+ /** Read a table again and run what its changes set off. */
115
+ async function tableChanged(tableId) {
116
+ await prime();
117
+ const t = tableOf(tableId);
118
+ if (!t)
119
+ throw new Error(`No table "${tableId}"`);
120
+ const fresh = await sourceOf(t).load();
121
+ const old = new Map(t.data.map((r) => [rowIdOf(t, r), r]));
122
+ setData(tableId, () => fresh);
123
+ const out = [];
124
+ for (const r of tableOf(tableId).data) {
125
+ const prev = old.get(rowIdOf(t, r));
126
+ if (!prev)
127
+ out.push(...(await handle({ type: 'created', table: tableId, record: r })));
128
+ else {
129
+ const fields = changedFields(t, prev, r);
130
+ if (fields.length)
131
+ out.push(...(await handle({ type: 'updated', table: tableId, record: r, before: prev, fields })));
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+ /** The `audit` hook of `createKitHandlers` for a table: every write through the endpoint. */
137
+ function audit(tableId) {
138
+ return async (entry) => {
139
+ if (entry.action === 'delete') {
140
+ await recordChanged(tableId, { before: { id: entry.id }, after: null }, { db: false });
141
+ return;
142
+ }
143
+ // A bulk update has no single row: read the table.
144
+ if (!entry.result) {
145
+ await tableChanged(tableId);
146
+ return;
147
+ }
148
+ await recordChanged(tableId, { after: entry.result });
149
+ };
150
+ }
151
+ async function formSubmitted(tableId, recordId, view) {
152
+ await prime();
153
+ const record = await recordById(tableId, recordId);
154
+ return record ? handle({ type: 'form', table: tableId, record, view }) : [];
155
+ }
156
+ async function buttonPressed(tableId, recordId, field) {
157
+ await prime();
158
+ const record = await recordById(tableId, recordId);
159
+ return record ? handle({ type: 'button', table: tableId, record, field }) : [];
160
+ }
161
+ async function recordById(tableId, id) {
162
+ const t = tableOf(tableId);
163
+ if (!t)
164
+ return null;
165
+ let hit = t.data.find((r) => rowIdOf(t, r) === id);
166
+ if (!hit) {
167
+ await tableChanged(tableId);
168
+ hit = tableOf(tableId).data.find((r) => rowIdOf(t, r) === id);
169
+ }
170
+ return hit ?? null;
171
+ }
172
+ /** Run the scheduled automations due at this minute (call it once a minute). */
173
+ async function tick(at = now()) {
174
+ await prime();
175
+ const minute = `${at.getFullYear()}-${at.getMonth()}-${at.getDate()} ${at.getHours()}:${at.getMinutes()}`;
176
+ const out = [];
177
+ for (const t of tables) {
178
+ for (const a of t.automations ?? []) {
179
+ if (!a.enabled || a.runOn !== 'server' || a.trigger.type !== 'scheduled')
180
+ continue;
181
+ const key = `${t.id}:${a.id}`;
182
+ if (lastFired.get(key) === minute || !cronMatches(a.trigger.cron, at))
183
+ continue;
184
+ lastFired.set(key, minute);
185
+ if (!a.condition)
186
+ out.push(await queue(a, t.id, null, 0));
187
+ else
188
+ for (const r of tableOf(t.id).data)
189
+ if (matches(t, a.condition, r))
190
+ out.push(await queue(a, t.id, rowIdOf(t, r), 0));
191
+ }
192
+ }
193
+ return out;
194
+ }
195
+ // ---- Running ------------------------------------------------------------------------
196
+ async function handle(e) {
197
+ const t = tableOf(e.table);
198
+ if (!t)
199
+ return [];
200
+ const depth = 'depth' in e ? (e.depth ?? 0) : 0;
201
+ const out = [];
202
+ for (const a of automationsFor(t, e, 'server'))
203
+ out.push(await queue(a, t.id, rowIdOf(t, e.record), depth));
204
+ return out;
205
+ }
206
+ /** One run at a time, in the order they were set off. */
207
+ function queue(a, tableId, recordId, depth) {
208
+ const next = chain.then(() => runOne(a, tableId, recordId, depth));
209
+ chain = next.catch(() => undefined);
210
+ return next;
211
+ }
212
+ async function runOne(a, tableId, recordId, depth) {
213
+ const t = tableOf(tableId);
214
+ const rec = recordId == null ? null : (t.data.find((r) => rowIdOf(t, r) === recordId) ?? null);
215
+ const run = { id: ++runSeq, automationId: a.id, name: a.name, table: t.name, record: rec ? String(rec[primaryOf(t)] ?? recordId) : null, at: now().getTime(), status: 'ok', steps: [] };
216
+ if (depth >= MAX_DEPTH) {
217
+ run.status = 'stopped';
218
+ run.steps.push({ action: 'Stopped', detail: `Automations set each other off ${MAX_DEPTH} times in a row; this one did not run, so they cannot loop.` });
219
+ }
220
+ else {
221
+ const follow = [];
222
+ for (const action of a.actions) {
223
+ try {
224
+ const step = await act(a, action, tableId, recordId, depth, follow);
225
+ if (step)
226
+ run.steps.push(step);
227
+ }
228
+ catch (e) {
229
+ run.status = 'error';
230
+ run.steps.push({ action: action.type, detail: '', error: e instanceof Error ? e.message : String(e) });
231
+ break;
232
+ }
233
+ }
234
+ remember(run);
235
+ // What its actions changed can set off others, after this run is kept.
236
+ for (const f of follow)
237
+ void f();
238
+ return run;
239
+ }
240
+ remember(run);
241
+ return run;
242
+ }
243
+ function remember(run) {
244
+ runs.unshift(run);
245
+ if (runs.length > keep)
246
+ runs.length = keep;
247
+ options.onRun?.(run);
248
+ }
249
+ async function act(a, action, tableId, recordId, depth, follow) {
250
+ const t = tableOf(tableId);
251
+ const rec = recordId == null ? null : (t.data.find((r) => rowIdOf(t, r) === recordId) ?? null);
252
+ const ctx = { tables, from: t, record: rec, now: now() };
253
+ switch (action.type) {
254
+ case 'notify': {
255
+ const message = fillTokens(t, rec, action.message);
256
+ options.notify?.({ message, severity: action.severity ?? 'info', automation: a.name, table: t.name, record: rec });
257
+ return { action: 'Notice', detail: message };
258
+ }
259
+ case 'highlight':
260
+ return null;
261
+ case 'updateRecord': {
262
+ if (!rec || recordId == null)
263
+ return { action: 'Update record', detail: 'No record to update' };
264
+ const fields = tableFields(t);
265
+ const patch = {};
266
+ for (const [id, spec] of Object.entries(action.values)) {
267
+ const c = fields.get(id);
268
+ if (c)
269
+ patch[c.field] = fieldValueFrom(t, c.def, spec, ctx);
270
+ }
271
+ const after = { ...rec, ...patch };
272
+ const changed = Object.keys(patch).filter((f) => !same(rec[f], after[f]));
273
+ if (!changed.length)
274
+ return { action: 'Update record', detail: 'Already set' };
275
+ const only = {};
276
+ for (const f of changed)
277
+ only[f] = after[f];
278
+ await sourceOf(t).update(recordId, only, rec);
279
+ setData(tableId, (rows) => rows.map((r) => (r === rec ? after : r)));
280
+ const ids = changed.map((f) => [...fields.values()].find((c) => c.field === f)?.id ?? f);
281
+ follow.push(() => handle({ type: 'updated', table: tableId, record: tableOf(tableId).data.find((r) => rowIdOf(t, r) === recordId), before: rec, fields: ids, depth: depth + 1 }));
282
+ return { action: 'Update record', detail: changed.map((f) => `${[...fields.values()].find((c) => c.field === f)?.header ?? f}: ${String(after[f] ?? '')}`).join(', ') };
283
+ }
284
+ case 'createRecord': {
285
+ const target = tableOf(action.table);
286
+ if (!target)
287
+ throw new Error(`No table "${action.table}"`);
288
+ const fields = tableFields(target);
289
+ const row = { id: newRecordId() };
290
+ for (const [id, spec] of Object.entries(action.values)) {
291
+ const c = fields.get(id);
292
+ if (c)
293
+ row[c.field] = fieldValueFrom(target, c.def, spec, ctx);
294
+ }
295
+ if (action.linkField && rec) {
296
+ const c = fields.get(action.linkField);
297
+ if (c)
298
+ row[c.field] = [rowIdOf(t, rec)];
299
+ }
300
+ const stored = await sourceOf(target).insert(row);
301
+ const made = { ...row, id: String(stored.id ?? row.id) };
302
+ setData(target.id, (rows) => [...rows, made]);
303
+ follow.push(() => handle({ type: 'created', table: target.id, record: made, depth: depth + 1 }));
304
+ return { action: 'Create record', detail: `${String(made[primaryOf(target)] ?? '') || 'a record'} in ${target.name}` };
305
+ }
306
+ case 'run':
307
+ return runAppAction(a, action, t, rec);
308
+ case 'webhook':
309
+ return { action: 'Webhook', detail: await callWebhook(action, outbound(a, t, rec, { secret, fetch: options.fetch })) };
310
+ case 'email':
311
+ return { action: 'Email', detail: await sendEmailAction(action, outbound(a, t, rec), options.sendEmail) };
312
+ }
313
+ }
314
+ return {
315
+ prime,
316
+ recordChanged,
317
+ tableChanged,
318
+ audit,
319
+ formSubmitted,
320
+ buttonPressed,
321
+ tick,
322
+ /** The latest runs, newest first: hand them to `<SvBase serverRuns>` from an endpoint. */
323
+ get runs() {
324
+ return runs;
325
+ },
326
+ /** Resolves when every queued run has finished. */
327
+ settled: () => chain.then(() => undefined),
328
+ /** The tables as it last read them. */
329
+ get tables() {
330
+ return tables;
331
+ },
332
+ };
333
+ }
@@ -0,0 +1,208 @@
1
+ import { type BaseTable } from './base-tables.js';
2
+ import { type FieldFilter } from './filter-model.js';
3
+ import type { FieldColumn } from './grid-access.js';
4
+ import { type EmailAction, type OutboundContext, type WebhookAction } from './automation-outbound.js';
5
+ type Row = Record<string, unknown>;
6
+ type Def = Record<string, any>;
7
+ export type AutomationTrigger =
8
+ /** A record is added: "+ Add row", a form, a board lane, another automation. */
9
+ {
10
+ type: 'recordCreated';
11
+ }
12
+ /** A record changes; with `fields`, only when one of them does. */
13
+ | {
14
+ type: 'recordUpdated';
15
+ fields?: string[];
16
+ }
17
+ /** A record starts matching the condition (it did not before the change). */
18
+ | {
19
+ type: 'recordMatches';
20
+ }
21
+ /** A form view sends a record; with `view`, only that form. */
22
+ | {
23
+ type: 'formSubmitted';
24
+ view?: string;
25
+ }
26
+ /** A button field is pressed on a record. */
27
+ | {
28
+ type: 'button';
29
+ field: string;
30
+ }
31
+ /** On a schedule: once for each record matching the condition then, or once with no record. */
32
+ | {
33
+ type: 'scheduled';
34
+ cron: string;
35
+ };
36
+ export type AutomationAction =
37
+ /** Set fields of the record that set it off. */
38
+ {
39
+ type: 'updateRecord';
40
+ values: Record<string, unknown>;
41
+ }
42
+ /** Add a record to a table; `linkField` there links it to the record that set it off. */
43
+ | {
44
+ type: 'createRecord';
45
+ table: string;
46
+ values: Record<string, unknown>;
47
+ linkField?: string;
48
+ }
49
+ /** A notice: `{Field name}` is that field of the record. */
50
+ | {
51
+ type: 'notify';
52
+ message: string;
53
+ severity?: 'info' | 'success' | 'warning' | 'error';
54
+ }
55
+ /** While the record matches the condition, color these fields (or the whole row). */
56
+ | {
57
+ type: 'highlight';
58
+ fields?: string[];
59
+ background: string;
60
+ color?: string;
61
+ }
62
+ /** A function the app registered with `registerAutomationAction`. */
63
+ | {
64
+ type: 'run';
65
+ action: string;
66
+ options?: Record<string, unknown>;
67
+ }
68
+ /** Call a URL with the record (a chat webhook, your API). */
69
+ | WebhookAction
70
+ /** Send an email (the server runner's `sendEmail`). */
71
+ | EmailAction;
72
+ export type BaseAutomation = {
73
+ id: string;
74
+ name: string;
75
+ enabled: boolean;
76
+ /**
77
+ * Where it runs: in the browser of the person who made the change
78
+ * (default), or on the server with `createAutomationServer`, which hears of
79
+ * changes from the database and can read secrets and send email.
80
+ */
81
+ runOn?: 'browser' | 'server';
82
+ trigger: AutomationTrigger;
83
+ /** Only records that match (the view bar's filter). */
84
+ condition?: FieldFilter;
85
+ actions: AutomationAction[];
86
+ };
87
+ /**
88
+ * Field values in `updateRecord` / `createRecord` are written as the field
89
+ * shows them: an option's name, a person's name, a linked record's name, a
90
+ * number, `true`. Text may name fields: `{Task} follow-up`. Dates also take
91
+ * `today`, `now`, `today+7`, `today-1`. `{record}` in a link field is the
92
+ * record that set the automation off. An empty string clears the field.
93
+ */
94
+ export type AutomationValue = unknown;
95
+ export type AutomationActionContext = {
96
+ automation: BaseAutomation;
97
+ table: BaseTable;
98
+ /** The record that set it off; null for a scheduled run with no condition. */
99
+ record: Row | null;
100
+ /** A field of the record as its cell shows it. */
101
+ text: (fieldName: string) => string;
102
+ options: Record<string, unknown>;
103
+ };
104
+ export type RegisteredAutomationAction = {
105
+ /** How the editor names it: "Post to Slack". */
106
+ label: string;
107
+ run: (ctx: AutomationActionContext) => unknown | Promise<unknown>;
108
+ };
109
+ /**
110
+ * Add an action automations can run: a webhook, an email, a message to chat.
111
+ * It runs in the page; keep secrets on your server and call it from here.
112
+ */
113
+ export declare function registerAutomationAction(id: string, action: RegisteredAutomationAction): void;
114
+ export declare function listAutomationActions(): Array<{
115
+ id: string;
116
+ label: string;
117
+ }>;
118
+ /** A table's fields as filled in (formulas, lookups, formats), by column id. */
119
+ export declare function tableFields(t: BaseTable): Map<string, FieldColumn>;
120
+ /** `{Field name}` tokens of a template, filled from a record. */
121
+ export declare function fillTokens(t: BaseTable, record: Row | null, template: string): string;
122
+ /** Whether a record matches a condition (no condition matches every record). */
123
+ export declare function matches(t: BaseTable, condition: FieldFilter | undefined, record: Row): boolean;
124
+ /** What a value written in an action becomes in a field of `t`. */
125
+ export declare function fieldValueFrom(t: BaseTable, def: Def, spec: unknown, ctx: {
126
+ tables: BaseTable[];
127
+ from: BaseTable;
128
+ record: Row | null;
129
+ now: Date;
130
+ }): unknown;
131
+ export type AutomationStep = {
132
+ action: string;
133
+ detail: string;
134
+ error?: string;
135
+ };
136
+ export type AutomationRun = {
137
+ id: number;
138
+ automationId: string;
139
+ name: string;
140
+ table: string;
141
+ record: string | null;
142
+ at: number;
143
+ status: 'ok' | 'error' | 'stopped';
144
+ steps: AutomationStep[];
145
+ };
146
+ /** The longest chain of automations setting each other off. */
147
+ export declare const MAX_DEPTH = 4;
148
+ export type AutomationEvent = {
149
+ type: 'created';
150
+ table: string;
151
+ record: Row;
152
+ depth?: number;
153
+ } | {
154
+ type: 'updated';
155
+ table: string;
156
+ record: Row;
157
+ before: Row;
158
+ fields: string[];
159
+ depth?: number;
160
+ } | {
161
+ type: 'form';
162
+ table: string;
163
+ record: Row;
164
+ view?: string;
165
+ } | {
166
+ type: 'button';
167
+ table: string;
168
+ record: Row;
169
+ field: string;
170
+ };
171
+ export type AutomationRunnerOptions = {
172
+ getTables: () => BaseTable[] | undefined;
173
+ /**
174
+ * Put changed tables in the page (and, through a table's source, in the
175
+ * database). `depth` is how deep in a chain of automations the change was
176
+ * made: changes it causes in other tables (through links) carry it on.
177
+ * `wrote` is the table it changed itself; it reports that change on its own.
178
+ */
179
+ apply: (tables: BaseTable[], depth?: number, wrote?: string) => void;
180
+ now?: () => Date;
181
+ /** The base's own object (its grids' `baseScope`), so its records' activity stays its own. */
182
+ scope?: object;
183
+ };
184
+ export declare function createAutomationRunner(options: AutomationRunnerOptions): {
185
+ handle: (e: AutomationEvent) => Promise<void>;
186
+ schedule: () => void;
187
+ runScheduled: (key: string) => Promise<void>;
188
+ /** Run one automation now for a record (or none), as its trigger would. */
189
+ /** Run one automation now for a record (or none), as its trigger would; a record that does not match its condition is reported, not run. */
190
+ runNow: (a: BaseAutomation, tableId: string, recordId: string | null) => Promise<void | undefined>;
191
+ readonly runs: AutomationRun[];
192
+ clearRuns: () => never[];
193
+ settled: () => Promise<void>;
194
+ stop: () => void | undefined;
195
+ };
196
+ export type AutomationRunner = ReturnType<typeof createAutomationRunner>;
197
+ /** Which of a table's automations an event sets off, for the runner on one side. */
198
+ export declare function automationsFor(t: BaseTable, e: AutomationEvent, side: 'browser' | 'server'): BaseAutomation[];
199
+ /** What a webhook or an email reads of the record. */
200
+ export declare function outbound(a: BaseAutomation, t: BaseTable, rec: Row | null, extra?: Partial<OutboundContext>): OutboundContext;
201
+ /** An action the app registered, run on a record. */
202
+ export declare function runAppAction(a: BaseAutomation, action: Extract<AutomationAction, {
203
+ type: 'run';
204
+ }>, t: BaseTable, rec: Row | null): Promise<AutomationStep>;
205
+ export declare function actionLabel(a: AutomationAction): string;
206
+ /** "When a record matches: Status is Blocked", for the list of automations. */
207
+ export declare function triggerText(t: BaseTable | undefined, a: BaseAutomation): string;
208
+ export {};