@adecore/database 0.0.1 → 0.16.0-beta.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 (400) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -1
  3. package/dist/DatabaseProvider.d.ts +13 -0
  4. package/dist/DatabaseProvider.js +17 -0
  5. package/dist/actions.d.ts +40 -0
  6. package/dist/actions.js +1 -0
  7. package/dist/client/client.d.ts +5 -0
  8. package/dist/client/client.js +245 -0
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +2 -0
  11. package/dist/client/types.d.ts +97 -0
  12. package/dist/client/types.js +13 -0
  13. package/dist/client-context.d.ts +16 -0
  14. package/dist/client-context.js +17 -0
  15. package/dist/connections/ConnectionForm.d.ts +10 -0
  16. package/dist/connections/ConnectionForm.js +145 -0
  17. package/dist/connections/ConnectionManager.d.ts +12 -0
  18. package/dist/connections/ConnectionManager.js +93 -0
  19. package/dist/connections/EngineIcon.d.ts +9 -0
  20. package/dist/connections/EngineIcon.js +7 -0
  21. package/dist/connections/connection-config.d.ts +16 -0
  22. package/dist/connections/connection-config.js +107 -0
  23. package/dist/connections/docker.d.ts +9 -0
  24. package/dist/connections/docker.js +38 -0
  25. package/dist/connections/engine-icons.d.ts +3 -0
  26. package/dist/connections/engine-icons.js +3 -0
  27. package/dist/connections/use-docker-containers.d.ts +17 -0
  28. package/dist/connections/use-docker-containers.js +26 -0
  29. package/dist/console/DestructiveDialog.d.ts +9 -0
  30. package/dist/console/DestructiveDialog.js +9 -0
  31. package/dist/console/HistoryPanel.d.ts +10 -0
  32. package/dist/console/HistoryPanel.js +15 -0
  33. package/dist/console/QueryConsole.d.ts +14 -0
  34. package/dist/console/QueryConsole.js +261 -0
  35. package/dist/console/SchemaPicker.d.ts +11 -0
  36. package/dist/console/SchemaPicker.js +8 -0
  37. package/dist/console/SqlEditor.d.ts +12 -0
  38. package/dist/console/SqlEditor.js +61 -0
  39. package/dist/console/StatementResultView.d.ts +18 -0
  40. package/dist/console/StatementResultView.js +32 -0
  41. package/dist/console/TransactionControls.d.ts +13 -0
  42. package/dist/console/TransactionControls.js +12 -0
  43. package/dist/console/destructive.d.ts +7 -0
  44. package/dist/console/destructive.js +43 -0
  45. package/dist/console/editing.d.ts +12 -0
  46. package/dist/console/editing.js +60 -0
  47. package/dist/console/history.d.ts +13 -0
  48. package/dist/console/history.js +43 -0
  49. package/dist/console/labels.d.ts +1 -0
  50. package/dist/console/labels.js +6 -0
  51. package/dist/console/outcome.d.ts +6 -0
  52. package/dist/console/outcome.js +11 -0
  53. package/dist/console/schemas.d.ts +2 -0
  54. package/dist/console/schemas.js +5 -0
  55. package/dist/console/shortcuts.d.ts +2 -0
  56. package/dist/console/shortcuts.js +3 -0
  57. package/dist/console/useConsoleHistory.d.ts +7 -0
  58. package/dist/console/useConsoleHistory.js +22 -0
  59. package/dist/ddl/alter-mysql.d.ts +4 -0
  60. package/dist/ddl/alter-mysql.js +79 -0
  61. package/dist/ddl/alter-sqlite.d.ts +4 -0
  62. package/dist/ddl/alter-sqlite.js +80 -0
  63. package/dist/ddl/alter.d.ts +4 -0
  64. package/dist/ddl/alter.js +24 -0
  65. package/dist/ddl/column.d.ts +9 -0
  66. package/dist/ddl/column.js +62 -0
  67. package/dist/ddl/create.d.ts +12 -0
  68. package/dist/ddl/create.js +74 -0
  69. package/dist/ddl/dialect.d.ts +12 -0
  70. package/dist/ddl/dialect.js +34 -0
  71. package/dist/ddl/diff.d.ts +34 -0
  72. package/dist/ddl/diff.js +111 -0
  73. package/dist/ddl/draft.d.ts +46 -0
  74. package/dist/ddl/draft.js +70 -0
  75. package/dist/ddl/index.d.ts +10 -0
  76. package/dist/ddl/index.js +10 -0
  77. package/dist/ddl/parse.d.ts +17 -0
  78. package/dist/ddl/parse.js +131 -0
  79. package/dist/ddl/statements.d.ts +6 -0
  80. package/dist/ddl/statements.js +12 -0
  81. package/dist/ddl/types.d.ts +6 -0
  82. package/dist/ddl/types.js +52 -0
  83. package/dist/ddl/validate.d.ts +7 -0
  84. package/dist/ddl/validate.js +38 -0
  85. package/dist/designer/ColumnsEditor.d.ts +2 -0
  86. package/dist/designer/ColumnsEditor.js +24 -0
  87. package/dist/designer/DesignerView.d.ts +25 -0
  88. package/dist/designer/DesignerView.js +33 -0
  89. package/dist/designer/ForeignKeysEditor.d.ts +7 -0
  90. package/dist/designer/ForeignKeysEditor.js +24 -0
  91. package/dist/designer/IndexesEditor.d.ts +2 -0
  92. package/dist/designer/IndexesEditor.js +13 -0
  93. package/dist/designer/NamePicker.d.ts +11 -0
  94. package/dist/designer/NamePicker.js +13 -0
  95. package/dist/designer/OptionsEditor.d.ts +2 -0
  96. package/dist/designer/OptionsEditor.js +17 -0
  97. package/dist/designer/SqlPreview.d.ts +12 -0
  98. package/dist/designer/SqlPreview.js +17 -0
  99. package/dist/designer/TableDesigner.d.ts +10 -0
  100. package/dist/designer/TableDesigner.js +103 -0
  101. package/dist/designer/edit.d.ts +17 -0
  102. package/dist/designer/edit.js +115 -0
  103. package/dist/designer/editor-props.d.ts +10 -0
  104. package/dist/designer/editor-props.js +1 -0
  105. package/dist/designer/plan.d.ts +9 -0
  106. package/dist/designer/plan.js +13 -0
  107. package/dist/designer/recover.d.ts +2 -0
  108. package/dist/designer/recover.js +18 -0
  109. package/dist/designer/useReferenceColumns.d.ts +3 -0
  110. package/dist/designer/useReferenceColumns.js +36 -0
  111. package/dist/explorer/DatabaseExplorer.d.ts +13 -0
  112. package/dist/explorer/DatabaseExplorer.js +264 -0
  113. package/dist/explorer/RowMenu.d.ts +11 -0
  114. package/dist/explorer/RowMenu.js +89 -0
  115. package/dist/explorer/TableDialog.d.ts +15 -0
  116. package/dist/explorer/TableDialog.js +50 -0
  117. package/dist/explorer/expansion.d.ts +7 -0
  118. package/dist/explorer/expansion.js +31 -0
  119. package/dist/explorer/table-statements.d.ts +5 -0
  120. package/dist/explorer/table-statements.js +12 -0
  121. package/dist/explorer/tree.d.ts +125 -0
  122. package/dist/explorer/tree.js +295 -0
  123. package/dist/explorer/use-explorer-loads.d.ts +12 -0
  124. package/dist/explorer/use-explorer-loads.js +109 -0
  125. package/dist/explorer/use-stored-expansion.d.ts +9 -0
  126. package/dist/explorer/use-stored-expansion.js +47 -0
  127. package/dist/grid/CellEditor.d.ts +9 -0
  128. package/dist/grid/CellEditor.js +29 -0
  129. package/dist/grid/CopyAsMenu.d.ts +7 -0
  130. package/dist/grid/CopyAsMenu.js +10 -0
  131. package/dist/grid/DataGrid.d.ts +36 -0
  132. package/dist/grid/DataGrid.js +512 -0
  133. package/dist/grid/EnumPicker.d.ts +17 -0
  134. package/dist/grid/EnumPicker.js +53 -0
  135. package/dist/grid/GridHeaderCell.d.ts +36 -0
  136. package/dist/grid/GridHeaderCell.js +69 -0
  137. package/dist/grid/RecordView.d.ts +20 -0
  138. package/dist/grid/RecordView.js +126 -0
  139. package/dist/grid/ValueDock.d.ts +11 -0
  140. package/dist/grid/ValueDock.js +28 -0
  141. package/dist/grid/aggregates.d.ts +22 -0
  142. package/dist/grid/aggregates.js +62 -0
  143. package/dist/grid/cell-range.d.ts +14 -0
  144. package/dist/grid/cell-range.js +11 -0
  145. package/dist/grid/column-layout.d.ts +18 -0
  146. package/dist/grid/column-layout.js +49 -0
  147. package/dist/grid/column-selection.d.ts +11 -0
  148. package/dist/grid/column-selection.js +28 -0
  149. package/dist/grid/column-view.d.ts +10 -0
  150. package/dist/grid/column-view.js +23 -0
  151. package/dist/grid/copy-formats.d.ts +15 -0
  152. package/dist/grid/copy-formats.js +80 -0
  153. package/dist/grid/display.d.ts +20 -0
  154. package/dist/grid/display.js +81 -0
  155. package/dist/grid/edit-value.d.ts +5 -0
  156. package/dist/grid/edit-value.js +55 -0
  157. package/dist/grid/enum-type.d.ts +7 -0
  158. package/dist/grid/enum-type.js +52 -0
  159. package/dist/grid/focused-value.d.ts +4 -0
  160. package/dist/grid/focused-value.js +19 -0
  161. package/dist/grid/layout.d.ts +22 -0
  162. package/dist/grid/layout.js +74 -0
  163. package/dist/grid/navigation.d.ts +15 -0
  164. package/dist/grid/navigation.js +52 -0
  165. package/dist/grid/row-selection.d.ts +9 -0
  166. package/dist/grid/row-selection.js +24 -0
  167. package/dist/grid/sort.d.ts +7 -0
  168. package/dist/grid/sort.js +7 -0
  169. package/dist/grid/types.d.ts +30 -0
  170. package/dist/grid/types.js +9 -0
  171. package/dist/host/helper-path.d.ts +7 -0
  172. package/dist/host/helper-path.js +15 -0
  173. package/dist/host/host.d.ts +2 -0
  174. package/dist/host/host.js +343 -0
  175. package/dist/host/index.d.ts +5 -0
  176. package/dist/host/index.js +4 -0
  177. package/dist/host/spawn.d.ts +7 -0
  178. package/dist/host/spawn.js +98 -0
  179. package/dist/host/types.d.ts +19 -0
  180. package/dist/host/types.js +1 -0
  181. package/dist/host/validate.d.ts +10 -0
  182. package/dist/host/validate.js +339 -0
  183. package/dist/index.d.ts +14 -0
  184. package/dist/index.js +13 -0
  185. package/dist/locales/en.json +577 -0
  186. package/dist/locales/nl.json +577 -0
  187. package/dist/locales.d.ts +4 -0
  188. package/dist/locales.js +14 -0
  189. package/dist/protocol/connection.d.ts +56 -0
  190. package/dist/protocol/connection.js +1 -0
  191. package/dist/protocol/errors.d.ts +7 -0
  192. package/dist/protocol/errors.js +1 -0
  193. package/dist/protocol/index.d.ts +5 -0
  194. package/dist/protocol/index.js +3 -0
  195. package/dist/protocol/messages.d.ts +250 -0
  196. package/dist/protocol/messages.js +2 -0
  197. package/dist/protocol/schema.d.ts +53 -0
  198. package/dist/protocol/schema.js +1 -0
  199. package/dist/protocol/values.d.ts +21 -0
  200. package/dist/protocol/values.js +15 -0
  201. package/dist/sql-split.d.ts +15 -0
  202. package/dist/sql-split.js +201 -0
  203. package/dist/sql.d.ts +29 -0
  204. package/dist/sql.js +104 -0
  205. package/dist/structure/StructureView.d.ts +14 -0
  206. package/dist/structure/StructureView.js +99 -0
  207. package/dist/structure/structure-text.d.ts +2 -0
  208. package/dist/structure/structure-text.js +2 -0
  209. package/dist/table/CommandField.d.ts +14 -0
  210. package/dist/table/CommandField.js +157 -0
  211. package/dist/table/ImportDialog.d.ts +27 -0
  212. package/dist/table/ImportDialog.js +28 -0
  213. package/dist/table/TableStatusBar.d.ts +19 -0
  214. package/dist/table/TableStatusBar.js +22 -0
  215. package/dist/table/TableToolbar.d.ts +43 -0
  216. package/dist/table/TableToolbar.js +16 -0
  217. package/dist/table/TableView.d.ts +13 -0
  218. package/dist/table/TableView.js +508 -0
  219. package/dist/table/command-field.d.ts +57 -0
  220. package/dist/table/command-field.js +203 -0
  221. package/dist/table/grid-rows.d.ts +23 -0
  222. package/dist/table/grid-rows.js +55 -0
  223. package/dist/table/import-mapping.d.ts +7 -0
  224. package/dist/table/import-mapping.js +28 -0
  225. package/dist/table/layout-store.d.ts +13 -0
  226. package/dist/table/layout-store.js +43 -0
  227. package/dist/table/paging.d.ts +12 -0
  228. package/dist/table/paging.js +20 -0
  229. package/dist/table/pending.d.ts +32 -0
  230. package/dist/table/pending.js +94 -0
  231. package/dist/table/references.d.ts +12 -0
  232. package/dist/table/references.js +21 -0
  233. package/dist/table/useLoaded.d.ts +7 -0
  234. package/dist/table/useLoaded.js +36 -0
  235. package/dist/table/useStoredLayout.d.ts +8 -0
  236. package/dist/table/useStoredLayout.js +39 -0
  237. package/dist/table/useTableTransfer.d.ts +51 -0
  238. package/dist/table/useTableTransfer.js +125 -0
  239. package/dist/testing/fake.d.ts +21 -0
  240. package/dist/testing/fake.js +372 -0
  241. package/dist/testing/index.d.ts +1 -0
  242. package/dist/testing/index.js +1 -0
  243. package/dist/use-popup-press.d.ts +1 -0
  244. package/dist/use-popup-press.js +24 -0
  245. package/dist/use-schema-change.d.ts +1 -0
  246. package/dist/use-schema-change.js +13 -0
  247. package/dist/use-stable-callback.d.ts +1 -0
  248. package/dist/use-stable-callback.js +9 -0
  249. package/dist/value/ValuePanel.d.ts +14 -0
  250. package/dist/value/ValuePanel.js +103 -0
  251. package/dist/value/draft.d.ts +33 -0
  252. package/dist/value/draft.js +44 -0
  253. package/dist/value/hex.d.ts +14 -0
  254. package/dist/value/hex.js +46 -0
  255. package/dist/value/json.d.ts +9 -0
  256. package/dist/value/json.js +71 -0
  257. package/dist/value/views.d.ts +22 -0
  258. package/dist/value/views.js +54 -0
  259. package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
  260. package/dist/workbench/DatabaseWorkbench.js +179 -0
  261. package/dist/workbench/DesignerTab.d.ts +10 -0
  262. package/dist/workbench/DesignerTab.js +22 -0
  263. package/dist/workbench/TableTab.d.ts +10 -0
  264. package/dist/workbench/TableTab.js +8 -0
  265. package/dist/workbench/tabs.d.ts +42 -0
  266. package/dist/workbench/tabs.js +139 -0
  267. package/package.json +79 -3
  268. package/src/DatabaseProvider.tsx +37 -0
  269. package/src/actions.ts +34 -0
  270. package/src/client/client.ts +324 -0
  271. package/src/client/index.ts +18 -0
  272. package/src/client/types.ts +161 -0
  273. package/src/client-context.ts +35 -0
  274. package/src/connections/ConnectionForm.tsx +474 -0
  275. package/src/connections/ConnectionManager.tsx +224 -0
  276. package/src/connections/EngineIcon.tsx +18 -0
  277. package/src/connections/connection-config.ts +143 -0
  278. package/src/connections/docker.ts +50 -0
  279. package/src/connections/engine-icons.ts +5 -0
  280. package/src/connections/use-docker-containers.ts +41 -0
  281. package/src/console/DestructiveDialog.tsx +40 -0
  282. package/src/console/HistoryPanel.tsx +67 -0
  283. package/src/console/QueryConsole.tsx +439 -0
  284. package/src/console/SchemaPicker.tsx +32 -0
  285. package/src/console/SqlEditor.tsx +95 -0
  286. package/src/console/StatementResultView.tsx +176 -0
  287. package/src/console/TransactionControls.tsx +53 -0
  288. package/src/console/destructive.ts +60 -0
  289. package/src/console/editing.ts +77 -0
  290. package/src/console/history.ts +62 -0
  291. package/src/console/labels.ts +7 -0
  292. package/src/console/outcome.ts +14 -0
  293. package/src/console/schemas.ts +7 -0
  294. package/src/console/shortcuts.ts +4 -0
  295. package/src/console/useConsoleHistory.ts +39 -0
  296. package/src/ddl/alter-mysql.ts +90 -0
  297. package/src/ddl/alter-sqlite.ts +93 -0
  298. package/src/ddl/alter.ts +29 -0
  299. package/src/ddl/column.ts +74 -0
  300. package/src/ddl/create.ts +92 -0
  301. package/src/ddl/dialect.ts +53 -0
  302. package/src/ddl/diff.ts +164 -0
  303. package/src/ddl/draft.ts +136 -0
  304. package/src/ddl/index.ts +10 -0
  305. package/src/ddl/parse.ts +161 -0
  306. package/src/ddl/statements.ts +20 -0
  307. package/src/ddl/types.ts +60 -0
  308. package/src/ddl/validate.ts +47 -0
  309. package/src/designer/ColumnsEditor.tsx +204 -0
  310. package/src/designer/DesignerView.tsx +184 -0
  311. package/src/designer/ForeignKeysEditor.tsx +132 -0
  312. package/src/designer/IndexesEditor.tsx +63 -0
  313. package/src/designer/NamePicker.tsx +44 -0
  314. package/src/designer/OptionsEditor.tsx +101 -0
  315. package/src/designer/SqlPreview.tsx +56 -0
  316. package/src/designer/TableDesigner.tsx +186 -0
  317. package/src/designer/edit.ts +137 -0
  318. package/src/designer/editor-props.ts +13 -0
  319. package/src/designer/plan.ts +25 -0
  320. package/src/designer/recover.ts +20 -0
  321. package/src/designer/useReferenceColumns.ts +54 -0
  322. package/src/explorer/DatabaseExplorer.tsx +450 -0
  323. package/src/explorer/RowMenu.tsx +134 -0
  324. package/src/explorer/TableDialog.tsx +80 -0
  325. package/src/explorer/expansion.ts +47 -0
  326. package/src/explorer/table-statements.ts +18 -0
  327. package/src/explorer/tree.ts +453 -0
  328. package/src/explorer/use-explorer-loads.ts +157 -0
  329. package/src/explorer/use-stored-expansion.ts +66 -0
  330. package/src/grid/CellEditor.tsx +53 -0
  331. package/src/grid/CopyAsMenu.tsx +33 -0
  332. package/src/grid/DataGrid.tsx +871 -0
  333. package/src/grid/EnumPicker.tsx +108 -0
  334. package/src/grid/GridHeaderCell.tsx +243 -0
  335. package/src/grid/RecordView.tsx +293 -0
  336. package/src/grid/ValueDock.tsx +52 -0
  337. package/src/grid/aggregates.ts +94 -0
  338. package/src/grid/cell-range.ts +26 -0
  339. package/src/grid/column-layout.ts +71 -0
  340. package/src/grid/column-selection.ts +42 -0
  341. package/src/grid/column-view.ts +35 -0
  342. package/src/grid/copy-formats.ts +108 -0
  343. package/src/grid/display.ts +109 -0
  344. package/src/grid/edit-value.ts +60 -0
  345. package/src/grid/enum-type.ts +62 -0
  346. package/src/grid/focused-value.ts +23 -0
  347. package/src/grid/layout.ts +91 -0
  348. package/src/grid/navigation.ts +74 -0
  349. package/src/grid/row-selection.ts +35 -0
  350. package/src/grid/sort.ts +12 -0
  351. package/src/grid/types.ts +59 -0
  352. package/src/host/helper-path.ts +41 -0
  353. package/src/host/host.ts +456 -0
  354. package/src/host/index.ts +5 -0
  355. package/src/host/spawn.ts +124 -0
  356. package/src/host/types.ts +46 -0
  357. package/src/host/validate.ts +402 -0
  358. package/src/index.ts +37 -0
  359. package/src/locales/en.json +577 -0
  360. package/src/locales/nl.json +577 -0
  361. package/src/locales.ts +18 -0
  362. package/src/protocol/connection.ts +86 -0
  363. package/src/protocol/errors.ts +38 -0
  364. package/src/protocol/index.ts +33 -0
  365. package/src/protocol/messages.ts +168 -0
  366. package/src/protocol/schema.ts +76 -0
  367. package/src/protocol/values.ts +52 -0
  368. package/src/sql-split.ts +220 -0
  369. package/src/sql.ts +152 -0
  370. package/src/structure/StructureView.tsx +269 -0
  371. package/src/structure/structure-text.ts +4 -0
  372. package/src/table/CommandField.tsx +318 -0
  373. package/src/table/ImportDialog.tsx +168 -0
  374. package/src/table/TableStatusBar.tsx +91 -0
  375. package/src/table/TableToolbar.tsx +217 -0
  376. package/src/table/TableView.tsx +880 -0
  377. package/src/table/command-field.ts +285 -0
  378. package/src/table/grid-rows.ts +80 -0
  379. package/src/table/import-mapping.ts +37 -0
  380. package/src/table/layout-store.ts +62 -0
  381. package/src/table/paging.ts +33 -0
  382. package/src/table/pending.ts +143 -0
  383. package/src/table/references.ts +38 -0
  384. package/src/table/useLoaded.ts +58 -0
  385. package/src/table/useStoredLayout.ts +53 -0
  386. package/src/table/useTableTransfer.ts +187 -0
  387. package/src/testing/fake.ts +533 -0
  388. package/src/testing/index.ts +1 -0
  389. package/src/use-popup-press.ts +27 -0
  390. package/src/use-schema-change.ts +19 -0
  391. package/src/use-stable-callback.ts +12 -0
  392. package/src/value/ValuePanel.tsx +254 -0
  393. package/src/value/draft.ts +72 -0
  394. package/src/value/hex.ts +62 -0
  395. package/src/value/json.ts +74 -0
  396. package/src/value/views.ts +68 -0
  397. package/src/workbench/DatabaseWorkbench.tsx +348 -0
  398. package/src/workbench/DesignerTab.tsx +37 -0
  399. package/src/workbench/TableTab.tsx +27 -0
  400. package/src/workbench/tabs.ts +184 -0
@@ -0,0 +1,285 @@
1
+ import type { Engine, ValueKind } from '../protocol/index.ts';
2
+ import {
3
+ equalsCondition,
4
+ findColumn,
5
+ notEqualsCondition,
6
+ orderByClause,
7
+ parseOrderByLoose,
8
+ quoteIdentifier,
9
+ sqlLiteral,
10
+ type ColumnSort,
11
+ type LiteralValue,
12
+ type SortDirection
13
+ } from '../sql.ts';
14
+
15
+ /* A condition of the WHERE: `text` is what the chip shows and what editing it starts from, `sql` what goes to the server. */
16
+ export interface FilterChip {
17
+ readonly kind: 'filter';
18
+ readonly text: string;
19
+ readonly sql: string;
20
+ }
21
+
22
+ export interface SortChip {
23
+ readonly kind: 'sort';
24
+ readonly column: string;
25
+ readonly direction: SortDirection;
26
+ }
27
+
28
+ /* An ORDER BY that is more than a list of columns, such as one with `NULLS LAST`, kept as it was written. */
29
+ export interface OrderChip {
30
+ readonly kind: 'order';
31
+ readonly text: string;
32
+ }
33
+
34
+ export type Chip = FilterChip | SortChip | OrderChip;
35
+
36
+ export type StoredFilter = Pick<FilterChip, 'text' | 'sql'>;
37
+
38
+ export interface CommandColumn {
39
+ readonly name: string;
40
+ readonly type: string;
41
+ }
42
+
43
+ export type Suggestion =
44
+ | { readonly kind: 'condition'; readonly chip: FilterChip }
45
+ | { readonly kind: 'filter' | 'sort' | 'jump'; readonly column: CommandColumn }
46
+ | { readonly kind: 'sql'; readonly chip: FilterChip };
47
+
48
+ /* How many columns the suggestions list for what is typed. */
49
+ export const SUGGESTED_COLUMNS = 4;
50
+
51
+ const OR = /\bor\b/i;
52
+ const BARE_NAME = /^[A-Za-z_][\w$]*$/;
53
+ const WORD = /^(`(?:[^`]|``)+`|"(?:[^"]|"")+"|[A-Za-z_][\w$]*)\s*([\s\S]*)$/;
54
+ const COMPARE = /^(==|=|<>|!=|<=|>=|<|>)\s*([\s\S]+)$/;
55
+ const PATTERN = /^((?:NOT\s+)?(?:LIKE|ILIKE|GLOB|REGEXP|RLIKE))\s+([\s\S]+)$/i;
56
+ const MEMBER = /^((?:NOT\s+)?IN)\s*(\([\s\S]+\))$/i;
57
+ const BETWEEN = /^((?:NOT\s+)?BETWEEN)\s+([\s\S]+?)\s+AND\s+([\s\S]+)$/i;
58
+ const IS_TEST = /^IS\s+(?:NOT\s+)?(?:NULL|TRUE|FALSE)$/i;
59
+ /* A name being typed, or one that opened a quote. Whitespace after it means an operator is coming. */
60
+ const NAME_BEING_TYPED = /^\s*(?:[\w$]*|[`"][^`"]*[`"]?)$/;
61
+
62
+ export const chipKey = (chip: Chip): string => (chip.kind === 'sort' ? `sort:${chip.column}` : `${chip.kind}:${chip.kind === 'filter' ? chip.sql : chip.text}`);
63
+
64
+ const isFilter = (chip: Chip): chip is FilterChip => chip.kind === 'filter';
65
+
66
+ const isSort = (chip: Chip): chip is SortChip => chip.kind === 'sort';
67
+
68
+ /* A name as typed in SQL: bare when it can be, else in double quotes. The quotes are only for the text of a chip; `sql` quotes per engine. */
69
+ export const identifierText = (name: string): string => (BARE_NAME.test(name) ? name : `"${name.replaceAll('"', '""')}"`);
70
+
71
+ /* The conditions AND-ed together, each in parentheses when it holds an OR and there is more than one. */
72
+ export const chipsToWhere = (chips: readonly Chip[]): string => {
73
+ const conditions = chips
74
+ .filter(isFilter)
75
+ .map((chip) => chip.sql.trim())
76
+ .filter((sql) => sql !== '');
77
+ if (conditions.length < 2) {
78
+ return conditions[0] ?? '';
79
+ }
80
+ return conditions.map((sql) => (OR.test(sql) ? `(${sql})` : sql)).join(' AND ');
81
+ };
82
+
83
+ /* The sort chips in order, each quoted for the engine; a raw ORDER BY keeps its place among them. */
84
+ export const chipsToOrderBy = (engine: Engine, chips: readonly Chip[]): string =>
85
+ chips
86
+ .flatMap((chip) => {
87
+ if (chip.kind === 'sort') {
88
+ return [orderByClause(engine, [chip])];
89
+ }
90
+ return chip.kind === 'order' ? [chip.text.trim()] : [];
91
+ })
92
+ .filter((part) => part !== '')
93
+ .join(', ');
94
+
95
+ export const sortsOf = (chips: readonly Chip[]): ColumnSort[] => chips.filter(isSort).map(({ column, direction }) => ({ column, direction }));
96
+
97
+ /* The sorts of a header menu replace every sort and raw ORDER BY, keeping the filters where they are. */
98
+ export const withSorts = (chips: readonly Chip[], sorts: readonly ColumnSort[]): Chip[] => [
99
+ ...chips.filter(isFilter),
100
+ ...sorts.map((sort): Chip => ({ kind: 'sort', column: sort.column, direction: sort.direction }))
101
+ ];
102
+
103
+ /* A column that is not sorted joins as ascending; one that is flips its direction where it stands. */
104
+ export const toggleSort = (chips: readonly Chip[], column: string): Chip[] => {
105
+ if (!chips.some((chip) => isSort(chip) && chip.column === column)) {
106
+ return [...chips, { kind: 'sort', column, direction: 'asc' }];
107
+ }
108
+ return chips.map((chip) => (isSort(chip) && chip.column === column ? { ...chip, direction: chip.direction === 'asc' ? 'desc' : 'asc' } : chip));
109
+ };
110
+
111
+ /* The same list when the chip is already in it. */
112
+ export const addChip = (chips: readonly Chip[], chip: Chip): Chip[] =>
113
+ chips.some((other) => chipKey(other) === chipKey(chip)) ? [...chips] : [...chips, chip];
114
+
115
+ export const removeChipAt = (chips: readonly Chip[], index: number): Chip[] => chips.filter((_, at) => at !== index);
116
+
117
+ /* Puts the chip where another stood, dropping it if an equal one is already elsewhere in the list. */
118
+ export const replaceChipAt = (chips: readonly Chip[], index: number, chip: Chip): Chip[] => {
119
+ const duplicate = chips.some((other, at) => at !== index && chipKey(other) === chipKey(chip));
120
+ return duplicate ? removeChipAt(chips, index) : chips.map((other, at) => (at === index ? chip : other));
121
+ };
122
+
123
+ const rawFilter = (text: string): FilterChip => ({ kind: 'filter', text, sql: text });
124
+
125
+ /* The WHERE of a layout or of a jump as chips: the remembered ones when they still say that text, else the text as one raw chip. */
126
+ export const filterChipsOf = (where: string, remembered: readonly StoredFilter[] = []): Chip[] => {
127
+ const text = where.trim();
128
+ if (text === '') {
129
+ return [];
130
+ }
131
+ const kept = remembered.map((filter): FilterChip => ({ kind: 'filter', text: filter.text, sql: filter.sql }));
132
+ return chipsToWhere(kept) === text ? kept : [rawFilter(text)];
133
+ };
134
+
135
+ /* A plain list of columns becomes sort chips; anything else is one chip that keeps the text. */
136
+ export const sortChipsOf = (engine: Engine, orderBy: string): Chip[] => {
137
+ const text = orderBy.trim();
138
+ if (text === '') {
139
+ return [];
140
+ }
141
+ const sorts = parseOrderByLoose(engine, text);
142
+ return sorts === null ? [{ kind: 'order', text }] : sorts.map((sort): Chip => ({ kind: 'sort', column: sort.column, direction: sort.direction }));
143
+ };
144
+
145
+ export interface ChipSource {
146
+ readonly where: string;
147
+ readonly orderBy: string;
148
+ readonly remembered?: readonly StoredFilter[];
149
+ }
150
+
151
+ export const chipsFromQuery = (engine: Engine, { where, orderBy, remembered }: ChipSource): Chip[] => [
152
+ ...filterChipsOf(where, remembered),
153
+ ...sortChipsOf(engine, orderBy)
154
+ ];
155
+
156
+ /* Whether quotes and parentheses close, outside of which an operand would swallow the rest of the text. */
157
+ const isClosed = (operand: string): boolean => {
158
+ let quote: string | null = null;
159
+ let depth = 0;
160
+ for (const character of operand) {
161
+ if (quote !== null) {
162
+ quote = character === quote ? null : quote;
163
+ } else if (character === "'" || character === '"' || character === '`') {
164
+ quote = character;
165
+ } else if (character === '(') {
166
+ depth++;
167
+ } else if (character === ')') {
168
+ depth--;
169
+ if (depth < 0) {
170
+ return false;
171
+ }
172
+ }
173
+ }
174
+ return quote === null && depth === 0;
175
+ };
176
+
177
+ const upper = (keyword: string): string => keyword.replace(/\s+/g, ' ').toUpperCase();
178
+
179
+ /* The operator and operands of a condition in the form the chip shows, or `null` when the text does not read as one. */
180
+ const readOperation = (rest: string): string | null => {
181
+ const compare = COMPARE.exec(rest);
182
+ if (compare !== null) {
183
+ const operator = compare[1] === '==' ? '=' : compare[1] === '!=' ? '<>' : compare[1];
184
+ return isClosed(compare[2]!) ? `${operator} ${compare[2]}` : null;
185
+ }
186
+ const pattern = PATTERN.exec(rest);
187
+ if (pattern !== null) {
188
+ return isClosed(pattern[2]!) ? `${upper(pattern[1]!)} ${pattern[2]}` : null;
189
+ }
190
+ const member = MEMBER.exec(rest);
191
+ if (member !== null) {
192
+ return isClosed(member[2]!) ? `${upper(member[1]!)} ${member[2]}` : null;
193
+ }
194
+ const between = BETWEEN.exec(rest);
195
+ if (between !== null) {
196
+ return isClosed(between[2]!) && isClosed(between[3]!) ? `${upper(between[1]!)} ${between[2]} AND ${between[3]}` : null;
197
+ }
198
+ return IS_TEST.test(rest) ? upper(rest) : null;
199
+ };
200
+
201
+ /*
202
+ * The chip a typed text makes when it already reads as a condition: a column of the table, an
203
+ * operator and what it needs after it. A text that is anything else stays for the person to finish or to hand over as SQL.
204
+ */
205
+ export const parseCondition = (engine: Engine, text: string, columns: readonly string[]): FilterChip | null => {
206
+ const match = WORD.exec(text.trim());
207
+ if (match === null) {
208
+ return null;
209
+ }
210
+ const column = findColumn(engine, match[1]!, columns);
211
+ const operation = column === undefined ? null : readOperation(match[2]!.trim());
212
+ if (column === undefined || operation === null) {
213
+ return null;
214
+ }
215
+ return { kind: 'filter', text: `${match[1]} ${operation}`, sql: `${quoteIdentifier(engine, column)} ${operation}` };
216
+ };
217
+
218
+ /* `column = value` or `column <> value` for the cell a menu was opened on; NULL reads as IS NULL and IS NOT NULL. */
219
+ export const cellChip = (engine: Engine, column: string, value: LiteralValue, kind: ValueKind | undefined, exclude: boolean): FilterChip => {
220
+ const name = identifierText(column);
221
+ const text = value === null ? `${name} IS ${exclude ? 'NOT ' : ''}NULL` : `${name} ${exclude ? '<>' : '='} ${sqlLiteral(engine, value, kind)}`;
222
+ return { kind: 'filter', text, sql: exclude ? notEqualsCondition(engine, column, value, kind) : equalsCondition(engine, column, value, kind) };
223
+ };
224
+
225
+ /* Cuts a long text for a label, ending in an ellipsis. */
226
+ export const shorten = (text: string, limit: number): string => (text.length > limit ? `${text.slice(0, limit - 1)}…` : text);
227
+
228
+ /* How well a name matches what is typed: 0 is the name itself, then its start, a word of it, anywhere in it; `null` is no match. */
229
+ const rankOf = (name: string, token: string): number | null => {
230
+ const lower = name.toLowerCase();
231
+ if (lower === token) {
232
+ return 0;
233
+ }
234
+ if (lower.startsWith(token)) {
235
+ return 1;
236
+ }
237
+ if (lower.split(/[\s_.-]+/).some((word) => word.startsWith(token))) {
238
+ return 2;
239
+ }
240
+ return lower.includes(token) ? 3 : null;
241
+ };
242
+
243
+ /* The columns that match a typed name, the closest first and in table order among equals. */
244
+ export const matchColumns = (columns: readonly CommandColumn[], typed: string, limit = SUGGESTED_COLUMNS): CommandColumn[] => {
245
+ const token = typed.trim().replace(/^[`"]/, '').replace(/[`"]$/, '').toLowerCase();
246
+ if (token === '') {
247
+ return [];
248
+ }
249
+ return columns
250
+ .map((column, order) => ({ column, order, rank: rankOf(column.name, token) }))
251
+ .filter((entry): entry is { column: CommandColumn; order: number; rank: number } => entry.rank !== null)
252
+ .sort((left, right) => left.rank - right.rank || left.order - right.order)
253
+ .slice(0, limit)
254
+ .map((entry) => entry.column);
255
+ };
256
+
257
+ /*
258
+ * What the popup under the field offers for the text: the condition it already is, then for each
259
+ * matching column to filter, sort or jump to it, and last the text as plain SQL. Empty text offers nothing.
260
+ */
261
+ export const suggest = (engine: Engine, text: string, columns: readonly CommandColumn[], limit = SUGGESTED_COLUMNS): Suggestion[] => {
262
+ const typed = text.trim();
263
+ if (typed === '') {
264
+ return [];
265
+ }
266
+ const suggestions: Suggestion[] = [];
267
+ const condition = parseCondition(
268
+ engine,
269
+ typed,
270
+ columns.map((column) => column.name)
271
+ );
272
+ if (condition !== null) {
273
+ suggestions.push({ kind: 'condition', chip: condition });
274
+ }
275
+ if (NAME_BEING_TYPED.test(text)) {
276
+ for (const column of matchColumns(columns, typed, limit)) {
277
+ suggestions.push({ kind: 'filter', column }, { kind: 'sort', column }, { kind: 'jump', column });
278
+ }
279
+ }
280
+ suggestions.push({ kind: 'sql', chip: rawFilter(typed) });
281
+ return suggestions;
282
+ };
283
+
284
+ /* What the input holds after "Filter on <column>" is picked, ready for an operator and a value. */
285
+ export const filterStartText = (column: CommandColumn): string => `${identifierText(column.name)} `;
@@ -0,0 +1,80 @@
1
+ import { shownOfEdit, type Shown } from '../grid/display.ts';
2
+ import { wholeValueOf } from '../grid/focused-value.ts';
3
+ import { gridColumnOf, type GridColumn, type GridRow } from '../grid/types.ts';
4
+ import type { EditValue, RowsResult, TableStructure } from '../protocol/index.ts';
5
+ import { rowKeyOf, type PendingChanges } from './pending.ts';
6
+
7
+ export type RowRef = { readonly kind: 'loaded'; readonly index: number } | { readonly kind: 'inserted'; readonly id: number };
8
+
9
+ export const loadedRowKey = (index: number): string => `row:${index}`;
10
+
11
+ export const insertedRowKey = (id: number): string => `new:${id}`;
12
+
13
+ export const parseRowKey = (key: string): RowRef =>
14
+ key.startsWith('new:') ? { kind: 'inserted', id: Number(key.slice(4)) } : { kind: 'loaded', index: Number(key.slice(4)) };
15
+
16
+ export const buildGridColumns = (loaded: Pick<RowsResult, 'columns'>, structure: TableStructure | null): GridColumn[] =>
17
+ loaded.columns.map((column) =>
18
+ gridColumnOf(
19
+ column,
20
+ structure?.columns.find((info) => info.name === column.name),
21
+ structure?.primaryKey ?? []
22
+ )
23
+ );
24
+
25
+ export interface GridRowsInput {
26
+ readonly structure: TableStructure | null;
27
+ readonly loaded: Pick<RowsResult, 'columns' | 'rows'>;
28
+ readonly pending: PendingChanges;
29
+ /* Rows before this page, so the gutter counts from the first row of the table. */
30
+ readonly offset: number;
31
+ }
32
+
33
+ /* The loaded rows with their pending edits laid over them, then the rows waiting to be inserted. */
34
+ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsInput): GridRow[] => {
35
+ const rows = loaded.rows.map((cells, index): GridRow => {
36
+ const edits = pending.edits[index];
37
+ const edited = new Set<number>();
38
+ const shown = cells.map((cell, column): Shown => {
39
+ const edit = edits?.[loaded.columns[column]!.name];
40
+ if (edit === undefined) {
41
+ return cell;
42
+ }
43
+ edited.add(column);
44
+ return shownOfEdit(edit);
45
+ });
46
+ return {
47
+ key: loadedRowKey(index),
48
+ number: offset + index + 1,
49
+ cells: shown,
50
+ state: pending.deletes.has(index) ? 'deleted' : undefined,
51
+ edited,
52
+ locked: structure === null || rowKeyOf(structure, loaded, index) === null
53
+ };
54
+ });
55
+ const inserted = pending.inserts.map((row): GridRow => ({
56
+ key: insertedRowKey(row.id),
57
+ number: null,
58
+ cells: loaded.columns.map((column): Shown => {
59
+ const value = row.values[column.name];
60
+ return value === undefined ? { kind: 'default' } : shownOfEdit(value);
61
+ }),
62
+ state: 'inserted'
63
+ }));
64
+ return [...rows, ...inserted];
65
+ };
66
+
67
+ /*
68
+ * The values a copy of a row starts from: what the row shows, except the columns the server fills in
69
+ * (an auto-increment key, a generated column) and the cells that are only a preview or a DEFAULT.
70
+ */
71
+ export const cloneValues = (columns: readonly GridColumn[], cells: readonly Shown[]): Record<string, EditValue> => {
72
+ const values: Record<string, EditValue> = {};
73
+ columns.forEach((column, index) => {
74
+ const value = column.autoIncrement === true || column.readOnly === true ? undefined : wholeValueOf(cells[index] ?? null);
75
+ if (value !== undefined) {
76
+ values[column.name] = value;
77
+ }
78
+ });
79
+ return values;
80
+ };
@@ -0,0 +1,37 @@
1
+ import type { ColumnInfo } from '../protocol/index.ts';
2
+
3
+ export type ImportFormat = 'csv' | 'tsv';
4
+
5
+ /* The columns an import can fill: a generated column is computed by the server. */
6
+ export const importableColumns = (columns: readonly ColumnInfo[]): ColumnInfo[] => columns.filter((column) => !column.generated);
7
+
8
+ /* CSV or TSV by the extension of the path; CSV when it says neither. */
9
+ export const formatOfPath = (path: string): ImportFormat => (/\.(tsv|tab)$/i.test(path) ? 'tsv' : 'csv');
10
+
11
+ /*
12
+ * The table column each file column goes to, as a name, or `null` for none. Names match without regard
13
+ * to case and a table column takes at most one file column. A file without a header has no names to
14
+ * match, so its columns go to the table's columns in order.
15
+ */
16
+ export const matchColumns = (fileColumns: readonly string[], tableColumns: readonly string[], header: boolean): (string | null)[] => {
17
+ if (!header) {
18
+ return fileColumns.map((_, index) => tableColumns[index] ?? null);
19
+ }
20
+ const taken = new Set<string>();
21
+ return fileColumns.map((name) => {
22
+ const wanted = name.trim().toLowerCase();
23
+ const match = tableColumns.find((column) => !taken.has(column) && column.toLowerCase() === wanted);
24
+ if (match === undefined) {
25
+ return null;
26
+ }
27
+ taken.add(match);
28
+ return match;
29
+ });
30
+ };
31
+
32
+ /* Whether anything is mapped, since an import of nothing is no import. */
33
+ export const hasMapping = (mapping: readonly (string | null)[]): boolean => mapping.some((column) => column !== null);
34
+
35
+ /* Maps a file column to a table column, taking the table column from any other file column that had it. */
36
+ export const mapColumn = (mapping: readonly (string | null)[], fileColumn: number, tableColumn: string | null): (string | null)[] =>
37
+ mapping.map((current, index) => (index === fileColumn ? tableColumn : tableColumn !== null && current === tableColumn ? null : current));
@@ -0,0 +1,62 @@
1
+ import type { GridLayout } from '../grid/column-layout.ts';
2
+ import type { StoredFilter } from './command-field.ts';
3
+ import { PAGE_SIZES } from './paging.ts';
4
+
5
+ /* Bump when the shape changes; a stored value of another version is ignored rather than migrated. `filters` was added alongside `where`, which still says the same thing for a value without it. */
6
+ export const LAYOUT_VERSION = 1;
7
+
8
+ /* What is remembered of one table's view between visits. */
9
+ export interface StoredLayout extends GridLayout {
10
+ readonly pageSize: number | null;
11
+ readonly where: string;
12
+ readonly orderBy: string;
13
+ /* The filter chips as typed, which `where` alone would only give back as one raw condition. */
14
+ readonly filters: readonly StoredFilter[];
15
+ }
16
+
17
+ export const EMPTY_LAYOUT: StoredLayout = { widths: {}, hidden: [], pinned: [], pageSize: null, where: '', orderBy: '', filters: [] };
18
+
19
+ export const layoutStorageKey = (connectionId: string, schema: string, table: string): string => `database:table:${connectionId}:${schema}.${table}`;
20
+
21
+ const isStringList = (value: unknown): value is string[] => Array.isArray(value) && value.every((item) => typeof item === 'string');
22
+
23
+ const isStoredFilter = (value: unknown): value is StoredFilter =>
24
+ isRecord(value) && typeof value.text === 'string' && typeof value.sql === 'string' && value.sql.trim() !== '';
25
+
26
+ const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value);
27
+
28
+ export const serializeLayout = (layout: StoredLayout): string => JSON.stringify({ version: LAYOUT_VERSION, ...layout });
29
+
30
+ /* The layout a stored text holds, or `null` when it is missing, broken or from another version. A field that is off is dropped alone. */
31
+ export const parseStoredLayout = (text: string | null): StoredLayout | null => {
32
+ if (text === null) {
33
+ return null;
34
+ }
35
+ let data: unknown;
36
+ try {
37
+ data = JSON.parse(text);
38
+ } catch {
39
+ return null;
40
+ }
41
+ if (!isRecord(data) || data.version !== LAYOUT_VERSION) {
42
+ return null;
43
+ }
44
+ const widths: Record<string, number> = {};
45
+ if (isRecord(data.widths)) {
46
+ for (const [name, width] of Object.entries(data.widths)) {
47
+ if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
48
+ widths[name] = width;
49
+ }
50
+ }
51
+ }
52
+ const pageSize = PAGE_SIZES.find((size) => size === data.pageSize) ?? null;
53
+ return {
54
+ widths,
55
+ hidden: isStringList(data.hidden) ? data.hidden : [],
56
+ pinned: isStringList(data.pinned) ? data.pinned : [],
57
+ pageSize,
58
+ where: typeof data.where === 'string' ? data.where : '',
59
+ orderBy: typeof data.orderBy === 'string' ? data.orderBy : '',
60
+ filters: Array.isArray(data.filters) ? data.filters.filter(isStoredFilter).map((filter) => ({ text: filter.text, sql: filter.sql })) : []
61
+ };
62
+ };
@@ -0,0 +1,33 @@
1
+ export const PAGE_SIZES = [100, 500, 1000] as const;
2
+ export const DEFAULT_PAGE_SIZE = 500;
3
+
4
+ export interface PageBounds {
5
+ /* One-based number of the first and last row on the page; both 0 on an empty page. */
6
+ readonly from: number;
7
+ readonly to: number;
8
+ /* The number of rows in the table when known, else the least it can be (`exact` is then false). */
9
+ readonly total: number;
10
+ readonly exact: boolean;
11
+ readonly hasPrevious: boolean;
12
+ readonly hasNext: boolean;
13
+ }
14
+
15
+ /*
16
+ * Where a page sits. Without a count, a page that `hasMore` says only that one row exists past it, so
17
+ * the total is that many and the footer writes it as `501+`.
18
+ */
19
+ export const pageBounds = (offset: number, rowCount: number, hasMore: boolean, counted: number | null): PageBounds => {
20
+ const exact = counted !== null || !hasMore;
21
+ const total = counted ?? (hasMore ? offset + rowCount + 1 : offset + rowCount);
22
+ return {
23
+ from: rowCount === 0 ? 0 : offset + 1,
24
+ to: offset + rowCount,
25
+ total,
26
+ exact,
27
+ hasPrevious: offset > 0,
28
+ hasNext: counted === null ? hasMore : offset + rowCount < counted
29
+ };
30
+ };
31
+
32
+ /* Where the last page starts, given how many rows the table has. */
33
+ export const lastPageOffset = (total: number, pageSize: number): number => (total <= 0 ? 0 : Math.floor((total - 1) / pageSize) * pageSize);
@@ -0,0 +1,143 @@
1
+ import { valueOfCell, type Cell, type EditValue, type RowChange, type RowKey, type RowsResult, type TableStructure, type Value } from '../protocol/index.ts';
2
+
3
+ export interface InsertedRow {
4
+ /* Unique among the inserted rows; the grid keys the row by it. */
5
+ readonly id: number;
6
+ /* The columns a person filled in; the others take their default. */
7
+ readonly values: Readonly<Record<string, EditValue>>;
8
+ }
9
+
10
+ /*
11
+ * What a person changed on one page of a table and has not submitted yet. Rows are named by their
12
+ * index in the page as loaded, so a pending set belongs to that page and is dropped with it.
13
+ */
14
+ export interface PendingChanges {
15
+ readonly edits: Readonly<Record<number, Readonly<Record<string, EditValue>>>>;
16
+ readonly inserts: readonly InsertedRow[];
17
+ readonly deletes: ReadonlySet<number>;
18
+ }
19
+
20
+ export const emptyPending: PendingChanges = { edits: {}, inserts: [], deletes: new Set() };
21
+
22
+ export const isPendingEmpty = (pending: PendingChanges): boolean =>
23
+ Object.keys(pending.edits).length === 0 && pending.inserts.length === 0 && pending.deletes.size === 0;
24
+
25
+ /* How many row changes a submit sends: an edited row that is also deleted counts once, as the delete. */
26
+ export const pendingCount = (pending: PendingChanges): number =>
27
+ Object.keys(pending.edits).filter((row) => !pending.deletes.has(Number(row))).length + pending.inserts.length + pending.deletes.size;
28
+
29
+ export const valuesEqual = (left: EditValue | undefined, right: EditValue | undefined): boolean => {
30
+ if (left === undefined || right === undefined) {
31
+ return false;
32
+ }
33
+ if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
34
+ return left === right;
35
+ }
36
+ return left.kind === right.kind && (left.kind !== 'binary' || (right.kind === 'binary' && left.hex === right.hex));
37
+ };
38
+
39
+ /* Sets a cell of a loaded row; setting it back to what the row held removes the edit. */
40
+ export const setEdit = (pending: PendingChanges, row: number, column: string, value: EditValue, original: Cell): PendingChanges => {
41
+ const { [row]: current = {}, ...others } = pending.edits;
42
+ const { [column]: _replaced, ...kept } = current;
43
+ const next = valuesEqual(value, valueOfCell(original)) ? kept : { ...kept, [column]: value };
44
+ return { ...pending, edits: Object.keys(next).length === 0 ? others : { ...others, [row]: next } };
45
+ };
46
+
47
+ /* A new row to insert, filled in with `values`. */
48
+ export const addInsertWith = (pending: PendingChanges, values: Readonly<Record<string, EditValue>>): PendingChanges => ({
49
+ ...pending,
50
+ inserts: [...pending.inserts, { id: pending.inserts.reduce((highest, row) => Math.max(highest, row.id), 0) + 1, values }]
51
+ });
52
+
53
+ export const addInsert = (pending: PendingChanges): PendingChanges => addInsertWith(pending, {});
54
+
55
+ export const setInsertValue = (pending: PendingChanges, id: number, column: string, value: EditValue): PendingChanges => ({
56
+ ...pending,
57
+ inserts: pending.inserts.map((row) => (row.id === id ? { ...row, values: { ...row.values, [column]: value } } : row))
58
+ });
59
+
60
+ export const removeInsert = (pending: PendingChanges, id: number): PendingChanges => ({ ...pending, inserts: pending.inserts.filter((row) => row.id !== id) });
61
+
62
+ export const markDeleted = (pending: PendingChanges, rows: readonly number[]): PendingChanges => ({
63
+ ...pending,
64
+ deletes: new Set([...pending.deletes, ...rows])
65
+ });
66
+
67
+ /* Takes back every change to one loaded row: its edits and its deletion. */
68
+ export const revertRow = (pending: PendingChanges, row: number): PendingChanges => {
69
+ const { [row]: _dropped, ...edits } = pending.edits;
70
+ const deletes = new Set(pending.deletes);
71
+ deletes.delete(row);
72
+ return { ...pending, edits, deletes };
73
+ };
74
+
75
+ /* Rows of a page, by the index of the loaded ones and the id of the inserted ones. */
76
+ export interface RowSelectionRefs {
77
+ readonly loaded: readonly number[];
78
+ readonly inserted: readonly number[];
79
+ }
80
+
81
+ /* Takes back every change to the given rows: the edits and deletion marks of the loaded ones, and the inserted ones altogether. */
82
+ export const revertRows = (pending: PendingChanges, rows: RowSelectionRefs): PendingChanges => {
83
+ const reverted = rows.loaded.reduce(revertRow, pending);
84
+ return { ...reverted, inserts: reverted.inserts.filter((row) => !rows.inserted.includes(row.id)) };
85
+ };
86
+
87
+ /* Whether any of the given rows has something to take back. */
88
+ export const hasRowChanges = (pending: PendingChanges, rows: RowSelectionRefs): boolean =>
89
+ rows.loaded.some((row) => pending.edits[row] !== undefined || pending.deletes.has(row)) || pending.inserts.some((row) => rows.inserted.includes(row.id));
90
+
91
+ type LoadedRows = Pick<RowsResult, 'columns' | 'rows'>;
92
+
93
+ /*
94
+ * The key that picks out one loaded row, from its own values in the columns of the structure's row
95
+ * key. `null` when the table has no row key, or a key cell is only a preview, in which case the row
96
+ * cannot be told apart safely and stays read only.
97
+ */
98
+ export const rowKeyOf = (structure: TableStructure, loaded: LoadedRows, row: number): RowKey | null => {
99
+ const cells = loaded.rows[row];
100
+ if (structure.rowKey === null || cells === undefined) {
101
+ return null;
102
+ }
103
+ const key: Record<string, Value> = {};
104
+ for (const name of structure.rowKey) {
105
+ const index = loaded.columns.findIndex((column) => column.name === name);
106
+ const value = index < 0 ? undefined : valueOfCell(cells[index] as Cell);
107
+ if (value === undefined) {
108
+ return null;
109
+ }
110
+ key[name] = value;
111
+ }
112
+ return key;
113
+ };
114
+
115
+ const numeric = (text: string): number => Number(text);
116
+
117
+ /* Updates, then deletes, then inserts, all computed against the page as loaded. Throws for a change on a row without a usable key, which the grid never lets a person make. */
118
+ export const toRowChanges = (structure: TableStructure, loaded: LoadedRows, pending: PendingChanges): RowChange[] => {
119
+ const keyOf = (row: number): RowKey => {
120
+ const key = rowKeyOf(structure, loaded, row);
121
+ if (key === null) {
122
+ throw new Error(`Row ${row + 1} of ${structure.name} has no usable key.`);
123
+ }
124
+ return key;
125
+ };
126
+ const updates = Object.keys(pending.edits)
127
+ .map(numeric)
128
+ .filter((row) => !pending.deletes.has(row))
129
+ .sort((left, right) => left - right)
130
+ .map((row): RowChange => ({ kind: 'update', key: keyOf(row), values: pending.edits[row]! }));
131
+ const deletes = [...pending.deletes].sort((left, right) => left - right).map((row): RowChange => ({ kind: 'delete', key: keyOf(row) }));
132
+ const inserts = pending.inserts.map((row): RowChange => ({ kind: 'insert', values: row.values }));
133
+ return [...updates, ...deletes, ...inserts];
134
+ };
135
+
136
+ /* A key as a person would write it in a WHERE: `id = 5, tenant = 'a'`. */
137
+ export const describeKey = (key: RowKey): string =>
138
+ Object.entries(key)
139
+ .map(
140
+ ([column, value]) =>
141
+ `${column} = ${value === null ? 'NULL' : typeof value === 'object' ? `0x${value.hex}` : typeof value === 'string' ? `'${value}'` : String(value)}`
142
+ )
143
+ .join(', ');