@svgrid/enterprise 3.1.8 → 3.1.9

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-Obk8sI77.js → index-BPU2KNc5.js} +307 -307
  34. package/dist/designer/assets/index-BmvG99kR.css +1 -0
  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,271 @@
1
+ import { introspectDatabase, listDatabaseTables } from '../studio/introspect-db.js';
2
+ import { createSqlDataSource } from '../sveltekit/sql-source.js';
3
+ import { createKitDataSource, createKitHandlers } from '../sveltekit/transport.js';
4
+ import { createSupabaseDataSource } from '../sources/supabase.js';
5
+ import { createSupabaseRealtime } from '../sources/realtime-supabase.js';
6
+ import { introspectSupabaseTable, listSupabaseTables } from '../sources/introspect-supabase.js';
7
+ import { applyBaseComments, baseSpecFromSchemas, baseTablesFromSpec, withInverses } from './base-schema.js';
8
+ import { SQL_COMMENT_PREFIX } from './base-sql.js';
9
+ const SQL_DIALECT = {
10
+ postgres: { placeholders: '$', ilike: true },
11
+ supabase: { placeholders: '$', ilike: true },
12
+ mysql: { quote: '`', placeholders: '?' },
13
+ sqlite: { placeholders: '?' },
14
+ mssql: { placeholders: '@' },
15
+ };
16
+ const quote = (dialect, name) => dialect === 'mysql' ? `\`${name.replace(/`/g, '``')}\`` : dialect === 'mssql' ? `[${name.replace(/]/g, ']]')}]` : `"${name.replace(/"/g, '""')}"`;
17
+ const param = (dialect, i) => (dialect === 'postgres' || dialect === 'supabase' ? `$${i}` : dialect === 'mssql' ? `@p${i}` : '?');
18
+ /** The values each text column repeats, most used first (at most 13 per column). */
19
+ async function readValues(o, dialect, schemas) {
20
+ const out = {};
21
+ for (const s of schemas) {
22
+ out[s.name] = {};
23
+ for (const f of s.fields) {
24
+ if (f.type !== 'text' || f.primaryKey)
25
+ continue;
26
+ const c = quote(dialect, f.field);
27
+ const top = dialect === 'mssql' ? 'TOP 13 ' : '';
28
+ const limit = dialect === 'mssql' ? '' : ' LIMIT 13';
29
+ try {
30
+ const rows = await o.execute(`SELECT ${top}${c} AS v, COUNT(*) AS n FROM ${quote(dialect, s.name)} WHERE ${c} IS NOT NULL GROUP BY ${c} ORDER BY COUNT(*) DESC${limit}`, []);
31
+ out[s.name][f.field] = rows.map((r) => ({ value: String(r.v), count: Number(r.n) }));
32
+ }
33
+ catch {
34
+ // A column it cannot group (a long text type): no choices for it.
35
+ }
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ /** The svgrid: comments baseToSql left on the tables and columns (Postgres). */
41
+ async function readComments(o, dialect) {
42
+ const out = { tables: new Map(), columns: new Map(), junctions: new Map() };
43
+ if (dialect !== 'postgres' && dialect !== 'supabase')
44
+ return out;
45
+ let rows = [];
46
+ try {
47
+ rows = await o.execute(`select c.relname as tbl, a.attname as col, d.description as text
48
+ from pg_description d
49
+ join pg_class c on c.oid = d.objoid
50
+ join pg_namespace n on n.oid = c.relnamespace
51
+ left join pg_attribute a on a.attrelid = c.oid and a.attnum = d.objsubid and d.objsubid > 0
52
+ where n.nspname = 'public' and d.description like 'svgrid:%'`, []);
53
+ }
54
+ catch {
55
+ return out;
56
+ }
57
+ for (const r of rows)
58
+ addComment(out, String(r.tbl), r.col == null ? null : String(r.col), String(r.text));
59
+ return out;
60
+ }
61
+ /** One svgrid: comment, on a table (`col` null) or a column. */
62
+ function addComment(out, table, col, text) {
63
+ if (!text.startsWith(SQL_COMMENT_PREFIX))
64
+ return;
65
+ let value;
66
+ try {
67
+ // PostgREST adds notes after the comment, on lines of their own.
68
+ value = JSON.parse(text.slice(SQL_COMMENT_PREFIX.length).split('\n')[0]);
69
+ }
70
+ catch {
71
+ return;
72
+ }
73
+ if (col != null)
74
+ out.columns.set(`${table}.${col}`, value);
75
+ else if (value?.junction)
76
+ out.junctions.set(table, value.junction);
77
+ else if (value?.v === 1)
78
+ out.tables.set(table, value);
79
+ }
80
+ /** The svgrid: comments, as Supabase's API description (PostgREST's OpenAPI) shows them. */
81
+ async function readSupabaseComments(o) {
82
+ const out = { tables: new Map(), columns: new Map(), junctions: new Map() };
83
+ try {
84
+ const res = await (o.fetch ?? globalThis.fetch)(`${o.url.replace(/\/+$/, '')}/rest/v1/`, {
85
+ headers: { apikey: o.key, Authorization: `Bearer ${o.key}`, Accept: 'application/json' },
86
+ });
87
+ if (!res.ok)
88
+ return out;
89
+ const spec = (await res.json());
90
+ const defs = spec?.definitions ?? spec?.components?.schemas ?? {};
91
+ for (const [table, def] of Object.entries(defs)) {
92
+ if (typeof def?.description === 'string')
93
+ addComment(out, table, null, def.description);
94
+ for (const [col, prop] of Object.entries(def?.properties ?? {})) {
95
+ if (typeof prop?.description === 'string')
96
+ addComment(out, table, col, prop.description);
97
+ }
98
+ }
99
+ }
100
+ catch {
101
+ // No API description (blocked, or a key that cannot read it): the base from the columns.
102
+ }
103
+ return out;
104
+ }
105
+ /** A base saved with baseToSql: the fields, views and automations it had, its tabs in their order. */
106
+ function withComments(specs, comments, given) {
107
+ const out = withInverses(applyBaseComments(specs, comments));
108
+ if (!given && out.every((s) => comments.tables.get(s.id)?.position != null))
109
+ out.sort((a, b) => comments.tables.get(a.id).position - comments.tables.get(b.id).position);
110
+ return out;
111
+ }
112
+ const hasComments = (c) => c.tables.size > 0 || c.columns.size > 0;
113
+ /** Whether the database fills in a table's key: a numeric key, or one with a default. */
114
+ async function makesKeys(o, dialect, s) {
115
+ const pk = s.fields.find((f) => f.primaryKey);
116
+ if (!pk)
117
+ return false;
118
+ if (pk.type === 'number')
119
+ return true;
120
+ if (dialect !== 'postgres' && dialect !== 'supabase' && dialect !== 'mysql')
121
+ return false;
122
+ try {
123
+ const rows = await o.execute(`SELECT column_default AS d FROM information_schema.columns WHERE table_name = ${param(dialect, 1)} AND column_name = ${param(dialect, 2)}`, [s.name, pk.field]);
124
+ return rows.some((r) => r.d != null && String(r.d) !== '');
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ async function readDatabase(o) {
131
+ const dialect = o.dialect ?? 'postgres';
132
+ const names = o.tables ?? (await listDatabaseTables(dialect, o.execute));
133
+ const list = [];
134
+ for (const table of names)
135
+ list.push(await introspectDatabase({ dialect, table, execute: o.execute }));
136
+ // A junction table the list leaves out still carries a link to many.
137
+ const values = o.choices === false ? {} : await readValues(o, dialect, list);
138
+ const keys = new Map();
139
+ for (const s of list)
140
+ keys.set(s.name, await makesKeys(o, dialect, s));
141
+ const comments = await readComments(o, dialect);
142
+ const kept = hasComments(comments);
143
+ let specs = baseSpecFromSchemas(list, { values, serverIds: (s) => keys.get(s.name) ?? false, order: o.tables ? 'given' : 'links', inverses: !kept });
144
+ if (kept)
145
+ specs = withComments(specs, comments, !!o.tables);
146
+ return { specs, schemas: new Map(list.map((s) => [s.name, s])) };
147
+ }
148
+ /** The tables of a database, described as a base: plain JSON (what an endpoint sends). */
149
+ export async function describeDatabase(options) {
150
+ return (await readDatabase(options)).specs;
151
+ }
152
+ /**
153
+ * A base on a SQL database, from its tables as they are.
154
+ *
155
+ * ```ts
156
+ * const tables = await baseFromDatabase({ execute: (sql, params) => db.query(sql, params).then((r) => r.rows) })
157
+ * ```
158
+ */
159
+ export async function baseFromDatabase(options) {
160
+ const dialect = options.dialect ?? 'postgres';
161
+ const { specs, schemas } = await readDatabase(options);
162
+ return baseTablesFromSpec(specs, (table) => createSqlDataSource({ schema: schemas.get(table), table, dialect: SQL_DIALECT[dialect], execute: options.execute }));
163
+ }
164
+ /**
165
+ * The server half of `baseFromEndpoint`, as a SvelteKit route:
166
+ *
167
+ * ```ts
168
+ * // src/routes/api/base/+server.ts
169
+ * export const { GET, POST } = createBaseEndpoint({ execute: (sql, params) => pool.query(sql, params).then((r) => r.rows) })
170
+ * ```
171
+ *
172
+ * `GET` answers with the tables described as a base; `POST ?table=tasks`
173
+ * reads and writes that table. Only the tables it described are reachable.
174
+ */
175
+ export function createBaseEndpoint(options) {
176
+ const dialect = options.dialect ?? 'postgres';
177
+ let read = null;
178
+ const load = () => (read ?? (read = readDatabase(options).catch((e) => {
179
+ read = null;
180
+ throw e;
181
+ })));
182
+ const handlers = new Map();
183
+ const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
184
+ return {
185
+ async GET() {
186
+ return json((await load()).specs);
187
+ },
188
+ async POST(event) {
189
+ const { schemas } = await load();
190
+ const table = new URL(event.request.url).searchParams.get('table') ?? '';
191
+ const schema = schemas.get(table);
192
+ if (!schema)
193
+ return json({ error: `No table "${table}"` }, 404);
194
+ let h = handlers.get(table);
195
+ if (!h) {
196
+ h = createKitHandlers({
197
+ schema,
198
+ source: createSqlDataSource({ schema, table, dialect: SQL_DIALECT[dialect], execute: options.execute }),
199
+ ...(options.authorize ? { authorize: options.authorize(table) } : {}),
200
+ ...(options.audit ? { audit: options.audit(table) } : {}),
201
+ });
202
+ handlers.set(table, h);
203
+ }
204
+ return h.POST(event);
205
+ },
206
+ /** Read the database's tables again (after a migration). */
207
+ refresh() {
208
+ read = null;
209
+ handlers.clear();
210
+ },
211
+ };
212
+ }
213
+ /**
214
+ * A base on your server's `createBaseEndpoint`.
215
+ *
216
+ * ```svelte
217
+ * {#await baseFromEndpoint('/api/base') then tables}<SvBase {tables} editable />{/await}
218
+ * ```
219
+ */
220
+ export async function baseFromEndpoint(url, options = {}) {
221
+ const doFetch = options.fetch ?? globalThis.fetch;
222
+ const res = await doFetch(url);
223
+ if (!res.ok)
224
+ throw new Error(`${url} answered ${res.status}`);
225
+ const specs = (await res.json());
226
+ const at = (table) => `${url}${url.includes('?') ? '&' : '?'}table=${encodeURIComponent(table)}`;
227
+ return baseTablesFromSpec(specs, (table) => createKitDataSource({ endpoint: at(table), ...(options.fetch ? { fetch: options.fetch } : {}) }));
228
+ }
229
+ /**
230
+ * A base on a Supabase project.
231
+ *
232
+ * ```ts
233
+ * const tables = await baseFromSupabase({ client: supabase, url: PUBLIC_SUPABASE_URL, key: PUBLIC_SUPABASE_ANON_KEY })
234
+ * ```
235
+ */
236
+ export async function baseFromSupabase(options) {
237
+ const names = options.tables ?? (await listSupabaseTables(options.url, options.key, options.fetch));
238
+ const schemas = [];
239
+ for (const table of names) {
240
+ const s = await introspectSupabaseTable({ url: options.url, key: options.key, table, ...(options.fetch ? { fetch: options.fetch } : {}) });
241
+ if (s)
242
+ schemas.push(s);
243
+ }
244
+ const values = {};
245
+ if (options.choices !== false) {
246
+ for (const s of schemas) {
247
+ const text = s.fields.filter((f) => f.type === 'text' && !f.primaryKey).map((f) => f.field);
248
+ if (!text.length)
249
+ continue;
250
+ const { data } = (await options.client.from(s.name).select(text.join(',')).range(0, 999));
251
+ values[s.name] = {};
252
+ for (const f of text) {
253
+ const counts = new Map();
254
+ for (const r of data ?? [])
255
+ if (r[f] != null && r[f] !== '')
256
+ counts.set(String(r[f]), (counts.get(String(r[f])) ?? 0) + 1);
257
+ values[s.name][f] = [...counts].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count);
258
+ }
259
+ }
260
+ }
261
+ const byName = new Map(schemas.map((s) => [s.name, s]));
262
+ const comments = await readSupabaseComments(options);
263
+ const kept = hasComments(comments);
264
+ // Supabase makes the keys: its source inserts without the page's id.
265
+ let specs = baseSpecFromSchemas(schemas, { values, serverIds: () => true, order: options.tables ? 'given' : 'links', inverses: !kept });
266
+ if (kept)
267
+ specs = withComments(specs, comments, !!options.tables);
268
+ return baseTablesFromSpec(specs, (table) => createSupabaseDataSource({ client: options.client, table, schema: byName.get(table) }), (s) => options.realtime === false
269
+ ? {}
270
+ : { realtime: (onChange) => createSupabaseRealtime({ client: options.client, table: s.id, onChange }) });
271
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Base tables from a database's own description: the `EntitySchema` that
3
+ * `introspectSupabaseTable`, `introspectDatabase` or `introspectDrizzle`
4
+ * read.
5
+ *
6
+ * Column types become field types, helped by the column's name (`email`,
7
+ * `phone`, `price`, `notes`, `rating`); an enum, or a text column that keeps
8
+ * repeating a handful of values, becomes a single select with colored
9
+ * options; a foreign key becomes a link to the table it names, with a field
10
+ * in that table that links back; a table that only pairs two others
11
+ * (`task_tags`) becomes a link to many records instead of a tab of its own.
12
+ * Each table gets views that fit its fields: a grid, a Kanban board by its
13
+ * first select, a calendar by its first date.
14
+ *
15
+ * The result is first a `BaseTableSpec`: plain JSON, so a server can send it
16
+ * to the page. `baseTablesFromSpec` then gives each table its source.
17
+ */
18
+ import type { ServerDataSource } from '@svgrid/grid';
19
+ import type { EntitySchema } from '../schema.js';
20
+ import { type BaseTable } from './base-tables.js';
21
+ import { type BaseSourceOptions, type ForeignKeyLink } from './base-sync.js';
22
+ import type { BaseJunctionComment, BaseTableComment } from './base-sql.js';
23
+ type Def = Record<string, any>;
24
+ /** How a link field is kept, by table name rather than by source: JSON-safe. */
25
+ export type BaseLinkSpec = {
26
+ column: string;
27
+ } | {
28
+ through: string;
29
+ from: string;
30
+ to: string;
31
+ idField?: string;
32
+ };
33
+ /** A base table as plain data: what `describeDatabase` reads and an endpoint sends. */
34
+ export type BaseTableSpec = Omit<BaseTable, 'source' | 'data'> & {
35
+ /** How its link fields are stored. */
36
+ links?: Record<string, BaseLinkSpec>;
37
+ /** The record's key column. */
38
+ idField?: string;
39
+ /** The database makes the key of a new record. */
40
+ serverIds?: boolean;
41
+ /** Column names that differ from field names: `{ fixedOn: 'fixed_on' }`. */
42
+ rename?: Record<string, string>;
43
+ };
44
+ /** Values a text column holds, most used first, for turning it into a select. */
45
+ export type ColumnValues = Record<string, Record<string, Array<{
46
+ value: string;
47
+ count: number;
48
+ }>>>;
49
+ /** One table's fields, its primary field and how its foreign keys are stored. */
50
+ export declare function baseFieldsFromSchema(entity: EntitySchema): {
51
+ columns: Def[];
52
+ primaryField: string;
53
+ idField: string;
54
+ links: Record<string, ForeignKeyLink>;
55
+ };
56
+ export type DescribeOptions = {
57
+ /** Values of text columns, to make the ones that repeat a few values single selects. */
58
+ values?: ColumnValues;
59
+ /** The database makes the key of each table's new records (by table). */
60
+ serverIds?: (table: EntitySchema) => boolean;
61
+ /** Add a Kanban and a calendar view where the fields allow (default true). */
62
+ views?: boolean;
63
+ /** Add the field that links back for each link (default true; false when the database says which they are). */
64
+ inverses?: boolean;
65
+ /** 'links': the tables that link to the most others first (when nobody chose an order). */
66
+ order?: 'given' | 'links';
67
+ };
68
+ /**
69
+ * Base tables as plain data from database descriptions: fields, links (a
70
+ * junction table becomes a link to many), choices, the fields that link
71
+ * back, and views.
72
+ */
73
+ export declare function baseSpecFromSchemas(entities: ReadonlyArray<EntitySchema | null | undefined>, options?: DescribeOptions): BaseTableSpec[];
74
+ /** Every link gets the field that links back in the other table, unless it has one. */
75
+ export declare function withInverses(specs: BaseTableSpec[]): BaseTableSpec[];
76
+ /** What a database's svgrid: comments say (see baseToSql), by SQL table and column. */
77
+ export type BaseComments = {
78
+ tables: Map<string, BaseTableComment>;
79
+ columns: Map<string, Def>;
80
+ junctions: Map<string, BaseJunctionComment['junction']>;
81
+ };
82
+ /**
83
+ * Lay what the comments keep over the base read from the columns: each
84
+ * field as it was (its type, options, format, name), the fields with no
85
+ * column (formulas, rollups, buttons, the fields that link back), a link to
86
+ * many under its own name, and the table's name, views and automations.
87
+ */
88
+ export declare function applyBaseComments(specs: BaseTableSpec[], comments: BaseComments): BaseTableSpec[];
89
+ /**
90
+ * Base tables from their specs: each gets a source from `sourceFor(table)`
91
+ * (also called for a junction table, by its name).
92
+ */
93
+ export declare function baseTablesFromSpec(specs: ReadonlyArray<BaseTableSpec>, sourceFor: (table: string, spec?: BaseTableSpec) => ServerDataSource<any>, options?: (spec: BaseTableSpec) => Omit<BaseSourceOptions, 'links' | 'idField' | 'serverIds'>): BaseTable[];
94
+ /**
95
+ * Base tables for a set of database tables, each on its own source, with a
96
+ * field that links back for every foreign key between them.
97
+ *
98
+ * ```ts
99
+ * const schemas = await Promise.all(['projects', 'tasks'].map((table) => introspectSupabaseTable({ url, key, table })))
100
+ * tables = baseTablesFromSchemas(schemas, (s) => createSupabaseDataSource({ client, table: s.name, schema: s }))
101
+ * ```
102
+ */
103
+ export declare function baseTablesFromSchemas(entities: ReadonlyArray<EntitySchema | null | undefined>, sourceFor: (entity: EntitySchema) => ServerDataSource<any>, options?: (entity: EntitySchema) => Omit<BaseSourceOptions, 'links' | 'idField'>): BaseTable[];
104
+ export {};
@@ -0,0 +1,266 @@
1
+ import { addInverse, colId } from './base-tables.js';
2
+ import { baseSource } from './base-sync.js';
3
+ import { BRIGHT } from './palette.js';
4
+ const title = (s) => s.replace(/[_-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').trim().replace(/^\w/, (c) => c.toUpperCase());
5
+ /** `project_id` -> `project`: the link field's name for a foreign key column. */
6
+ const linkName = (column) => column.replace(/_?id$/i, '') || column;
7
+ const MONEY = /(^|_)(price|amount|cost|budget|revenue|salary|total|fee|value|spend|mrr|arr)($|_)/i;
8
+ const LONG = /(^|_)(notes?|description|body|comment|comments|summary|details|bio|content|message)($|_)/i;
9
+ function fieldOf(f) {
10
+ const header = f.label ?? title(f.field);
11
+ const base = { id: f.field, field: f.field, header };
12
+ const editor = f.input?.editorType;
13
+ switch (f.type) {
14
+ case 'number':
15
+ if (editor === 'rating')
16
+ return { ...base, fieldType: 'rating', fieldProps: { max: 5 } };
17
+ if (MONEY.test(f.field))
18
+ return { ...base, fieldType: 'number', fieldProps: { format: 'currency', symbol: '$', precision: 0, summary: 'sum' } };
19
+ return { ...base, fieldType: 'number' };
20
+ case 'boolean':
21
+ return { ...base, fieldType: 'checkbox' };
22
+ case 'date':
23
+ case 'dateString':
24
+ return { ...base, fieldType: 'date', fieldProps: { dateFormat: 'friendly' } };
25
+ case 'datetime':
26
+ return { ...base, fieldType: 'date', fieldProps: { dateFormat: 'friendly', time: true } };
27
+ case 'enum':
28
+ return { ...base, fieldType: 'singleSelect', fieldProps: { options: (f.options ?? []).map((o, i) => ({ value: String(o.value), color: o.color ?? BRIGHT[i % BRIGHT.length].value })) } };
29
+ case 'json':
30
+ return { ...base, fieldType: 'longText' };
31
+ default:
32
+ if (f.format === 'email')
33
+ return { ...base, fieldType: 'email' };
34
+ if (f.format === 'url')
35
+ return { ...base, fieldType: 'url' };
36
+ if (editor === 'phone')
37
+ return { ...base, fieldType: 'phone' };
38
+ if (LONG.test(f.field))
39
+ return { ...base, fieldType: 'longText' };
40
+ return { ...base, fieldType: 'text' };
41
+ }
42
+ }
43
+ /** A width that shows a value of the type whole: a friendly date, an amount, an address. */
44
+ function widthFor(d) {
45
+ switch (d.fieldType) {
46
+ case 'date':
47
+ return d.fieldProps?.time ? 210 : 170;
48
+ case 'longText':
49
+ return 240;
50
+ case 'email':
51
+ case 'url':
52
+ return 200;
53
+ case 'link':
54
+ return 180;
55
+ case 'checkbox':
56
+ return 90;
57
+ case 'rating':
58
+ return 120;
59
+ case 'number':
60
+ return Math.max(110, String(d.header).length * 9 + 40);
61
+ default:
62
+ return Math.max(130, Math.min(200, String(d.header).length * 9 + 60));
63
+ }
64
+ }
65
+ const PRIMARY = /^(name|title|subject|label|full_?name|display_?name|summary|headline)$/i;
66
+ /** One table's fields, its primary field and how its foreign keys are stored. */
67
+ export function baseFieldsFromSchema(entity) {
68
+ const idField = entity.idField ?? entity.fields.find((f) => f.primaryKey)?.field ?? 'id';
69
+ const columns = [];
70
+ const links = {};
71
+ for (const f of entity.fields) {
72
+ if (f.field === idField)
73
+ continue;
74
+ if (f.type === 'relation' && f.relation) {
75
+ const column = f.relation.foreignKey ?? f.field;
76
+ const name = linkName(f.field);
77
+ links[name] = { column };
78
+ columns.push({ id: name, field: name, header: f.label ? f.label.replace(/\s*id$/i, '') : title(name), fieldType: 'link', fieldProps: { table: f.relation.entity, multiple: false } });
79
+ }
80
+ else
81
+ columns.push(fieldOf(f));
82
+ }
83
+ const primary = columns.find((d) => PRIMARY.test(String(d.field))) ?? columns.find((d) => d.fieldType === 'text') ?? columns[0];
84
+ // The primary field leads.
85
+ if (primary && columns[0] !== primary)
86
+ columns.splice(0, 0, ...columns.splice(columns.indexOf(primary), 1));
87
+ for (const d of columns)
88
+ d.width ?? (d.width = d === primary ? 220 : widthFor(d));
89
+ return { columns, primaryField: primary ? String(primary.field) : idField, idField, links };
90
+ }
91
+ /** A table that only pairs two others: its key, two foreign keys, and at most a timestamp. */
92
+ function isJunction(e) {
93
+ const rest = e.fields.filter((f) => !f.primaryKey && !/^(created|updated)_?(at|on)$/i.test(f.field));
94
+ const rels = rest.filter((f) => f.type === 'relation' && f.relation);
95
+ return rest.length === 2 && rels.length === 2 ? { from: rels[0], to: rels[1] } : null;
96
+ }
97
+ /** Whether a column's values read as a choice: a few values, each used more than once. */
98
+ function asChoices(values) {
99
+ if (!values?.length || values.length > 12)
100
+ return null;
101
+ const used = values.reduce((n, v) => n + v.count, 0);
102
+ if (used < values.length * 2 || values.some((v) => v.value.length > 40))
103
+ return null;
104
+ return values.map((v) => v.value);
105
+ }
106
+ /**
107
+ * Base tables as plain data from database descriptions: fields, links (a
108
+ * junction table becomes a link to many), choices, the fields that link
109
+ * back, and views.
110
+ */
111
+ export function baseSpecFromSchemas(entities, options = {}) {
112
+ const all = entities.filter((e) => !!e);
113
+ const junctions = new Map(all.map((e) => [e.name, isJunction(e)]).filter((x) => !!x[1]));
114
+ const names = new Set(all.filter((e) => !junctions.has(e.name)).map((e) => e.name));
115
+ let specs = all
116
+ .filter((e) => !junctions.has(e.name))
117
+ .map((e) => {
118
+ const { columns, primaryField, idField, links } = baseFieldsFromSchema(e);
119
+ // A foreign key to a table outside the set stays a plain value.
120
+ let kept = columns.map((d) => (d.fieldType === 'link' && !names.has(d.fieldProps.table) ? { ...d, field: links[d.field].column, id: links[d.field].column, header: title(links[d.field].column), fieldType: 'text', fieldProps: undefined } : d));
121
+ // A text column that repeats a handful of values: a single select.
122
+ kept = kept.map((d) => {
123
+ const choices = d.fieldType === 'text' && d.field !== primaryField ? asChoices(options.values?.[e.name]?.[String(d.field)]) : null;
124
+ return choices ? { ...d, width: 140, fieldType: 'singleSelect', fieldProps: { options: choices.map((value, i) => ({ value, color: BRIGHT[i % BRIGHT.length].value })) } } : d;
125
+ });
126
+ const keptLinks = Object.fromEntries(Object.entries(links).filter(([field]) => kept.some((d) => d.field === field)));
127
+ return {
128
+ id: e.name,
129
+ name: e.label && e.label !== e.name ? e.label : title(e.name),
130
+ primaryField,
131
+ columns: kept,
132
+ idField,
133
+ serverIds: options.serverIds?.(e) ?? false,
134
+ links: keptLinks,
135
+ };
136
+ });
137
+ // A junction table: a link to many on its first table, through it.
138
+ for (const [jname, j] of junctions) {
139
+ const fromTable = j.from.relation.entity;
140
+ const toTable = j.to.relation.entity;
141
+ const owner = specs.find((s) => s.id === fromTable);
142
+ if (!owner || !names.has(toTable))
143
+ continue;
144
+ const field = owner.columns.some((d) => d.field === toTable) ? `${toTable}_links` : toTable;
145
+ const target = specs.find((s) => s.id === toTable);
146
+ owner.columns = [...owner.columns, { id: field, field, header: target?.name ?? title(toTable), width: 220, fieldType: 'link', fieldProps: { table: toTable, multiple: true } }];
147
+ owner.links = { ...owner.links, [field]: { through: jname, from: j.from.relation.foreignKey ?? j.from.field, to: j.to.relation.foreignKey ?? j.to.field } };
148
+ }
149
+ // Every link gets the field that links back in the other table.
150
+ if (options.inverses !== false)
151
+ specs = withInverses(specs);
152
+ if (options.views !== false)
153
+ specs = specs.map((s) => ({ ...s, views: s.views ?? viewsFor(s) }));
154
+ if (options.order === 'links') {
155
+ // The table that points at the most others leads: orders before customers.
156
+ const out = (s) => Object.keys(s.links ?? {}).length;
157
+ specs = specs.map((s, i) => [s, i]).sort((a, b) => out(b[0]) - out(a[0]) || a[1] - b[1]).map(([s]) => s);
158
+ }
159
+ return specs;
160
+ }
161
+ /** Every link gets the field that links back in the other table, unless it has one. */
162
+ export function withInverses(specs) {
163
+ let tables = specs.map((x) => ({ ...x, data: [] }));
164
+ for (const t of [...tables])
165
+ for (const d of t.columns)
166
+ if (d.fieldType === 'link' && d.fieldProps?.table !== t.id && !d.fieldProps?.inverse)
167
+ tables = addInverse(tables, t.id, colId(d));
168
+ return tables.map(({ data: _data, ...rest }) => rest);
169
+ }
170
+ /**
171
+ * Lay what the comments keep over the base read from the columns: each
172
+ * field as it was (its type, options, format, name), the fields with no
173
+ * column (formulas, rollups, buttons, the fields that link back), a link to
174
+ * many under its own name, and the table's name, views and automations.
175
+ */
176
+ export function applyBaseComments(specs, comments) {
177
+ return specs.map((s0) => {
178
+ const s = { ...s0, columns: [...s0.columns], links: { ...(s0.links ?? {}) }, rename: { ...(s0.rename ?? {}) } };
179
+ s.columns = s.columns.map((d) => {
180
+ const field = String(d.field);
181
+ const link = s.links[field];
182
+ if (link && 'through' in link) {
183
+ const j = comments.junctions.get(link.through);
184
+ if (!j)
185
+ return d;
186
+ delete s.links[field];
187
+ s.links[j.field] = link;
188
+ return { ...d, id: j.field, field: j.field };
189
+ }
190
+ const sqlColumn = link ? link.column : field;
191
+ const def = comments.columns.get(`${s.id}.${sqlColumn}`);
192
+ if (!def)
193
+ return d;
194
+ const name = String(def.field);
195
+ if (link) {
196
+ delete s.links[field];
197
+ s.links[name] = { column: sqlColumn };
198
+ }
199
+ else if (name !== sqlColumn)
200
+ s.rename[name] = sqlColumn;
201
+ return { ...def, id: name, field: name };
202
+ });
203
+ const c = comments.tables.get(s.id);
204
+ if (!c)
205
+ return s;
206
+ // Fields with no column, and a link to many as it was defined.
207
+ for (const e of c.extra) {
208
+ const at = s.columns.findIndex((d) => String(d.field) === String(e.field));
209
+ if (at >= 0)
210
+ s.columns[at] = { ...e, id: e.field };
211
+ else
212
+ s.columns.push({ ...e, id: e.field });
213
+ }
214
+ const order = new Map(c.order.map((f, i) => [f, i]));
215
+ s.columns.sort((a, b) => (order.get(String(a.field)) ?? 999) - (order.get(String(b.field)) ?? 999));
216
+ return {
217
+ ...s,
218
+ name: c.name,
219
+ ...(c.primaryField ? { primaryField: c.primaryField } : {}),
220
+ ...(c.views ? { views: c.views } : {}),
221
+ ...(c.automations ? { automations: c.automations } : {}),
222
+ };
223
+ });
224
+ }
225
+ /** A grid colored by the first select, a board by it, a calendar by the first date. */
226
+ function viewsFor(s) {
227
+ const select = s.columns.find((d) => d.fieldType === 'singleSelect');
228
+ const date = s.columns.find((d) => d.fieldType === 'date');
229
+ const views = [{ id: 'grid', name: `All ${s.name.toLowerCase()}`, type: 'grid', ...(select ? { color: { mode: 'field', field: String(select.field) } } : {}) }];
230
+ if (select)
231
+ views.push({ id: 'board', name: `By ${String(select.header).toLowerCase()}`, type: 'kanban', stackBy: String(select.field) });
232
+ if (date)
233
+ views.push({ id: 'calendar', name: `${date.header} calendar`, type: 'calendar', dateField: String(date.field), ...(select ? { color: { mode: 'field', field: String(select.field) } } : {}) });
234
+ return views;
235
+ }
236
+ /**
237
+ * Base tables from their specs: each gets a source from `sourceFor(table)`
238
+ * (also called for a junction table, by its name).
239
+ */
240
+ export function baseTablesFromSpec(specs, sourceFor, options = () => ({})) {
241
+ return specs.map((s) => {
242
+ const { links = {}, idField, serverIds, rename, ...table } = s;
243
+ const resolved = {};
244
+ for (const [field, l] of Object.entries(links))
245
+ resolved[field] = 'through' in l ? { through: sourceFor(l.through), from: l.from, to: l.to, ...(l.idField ? { idField: l.idField } : {}) } : l;
246
+ return { ...table, data: [], source: baseSource(sourceFor(s.id, s), { ...options(s), links: resolved, idField, serverIds, ...(rename && Object.keys(rename).length ? { columns: rename } : {}) }) };
247
+ });
248
+ }
249
+ /**
250
+ * Base tables for a set of database tables, each on its own source, with a
251
+ * field that links back for every foreign key between them.
252
+ *
253
+ * ```ts
254
+ * const schemas = await Promise.all(['projects', 'tasks'].map((table) => introspectSupabaseTable({ url, key, table })))
255
+ * tables = baseTablesFromSchemas(schemas, (s) => createSupabaseDataSource({ client, table: s.name, schema: s }))
256
+ * ```
257
+ */
258
+ export function baseTablesFromSchemas(entities, sourceFor, options = () => ({})) {
259
+ const list = entities.filter((e) => !!e);
260
+ const byName = new Map(list.map((e) => [e.name, e]));
261
+ const specs = baseSpecFromSchemas(list, { views: false, serverIds: (e) => !!options(e).serverIds });
262
+ return baseTablesFromSpec(specs.map((s) => ({ ...s, views: s.views ?? [{ id: 'grid', name: 'Grid view', type: 'grid' }] })), (table) => sourceFor(byName.get(table)), (s) => {
263
+ const { serverIds: _s, ...rest } = options(byName.get(s.id));
264
+ return rest;
265
+ });
266
+ }