@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,203 @@
1
+ import { equalsCondition, findColumn, notEqualsCondition, orderByClause, parseOrderByLoose, quoteIdentifier, sqlLiteral } from '../sql.js';
2
+ /* How many columns the suggestions list for what is typed. */
3
+ export const SUGGESTED_COLUMNS = 4;
4
+ const OR = /\bor\b/i;
5
+ const BARE_NAME = /^[A-Za-z_][\w$]*$/;
6
+ const WORD = /^(`(?:[^`]|``)+`|"(?:[^"]|"")+"|[A-Za-z_][\w$]*)\s*([\s\S]*)$/;
7
+ const COMPARE = /^(==|=|<>|!=|<=|>=|<|>)\s*([\s\S]+)$/;
8
+ const PATTERN = /^((?:NOT\s+)?(?:LIKE|ILIKE|GLOB|REGEXP|RLIKE))\s+([\s\S]+)$/i;
9
+ const MEMBER = /^((?:NOT\s+)?IN)\s*(\([\s\S]+\))$/i;
10
+ const BETWEEN = /^((?:NOT\s+)?BETWEEN)\s+([\s\S]+?)\s+AND\s+([\s\S]+)$/i;
11
+ const IS_TEST = /^IS\s+(?:NOT\s+)?(?:NULL|TRUE|FALSE)$/i;
12
+ /* A name being typed, or one that opened a quote. Whitespace after it means an operator is coming. */
13
+ const NAME_BEING_TYPED = /^\s*(?:[\w$]*|[`"][^`"]*[`"]?)$/;
14
+ export const chipKey = (chip) => (chip.kind === 'sort' ? `sort:${chip.column}` : `${chip.kind}:${chip.kind === 'filter' ? chip.sql : chip.text}`);
15
+ const isFilter = (chip) => chip.kind === 'filter';
16
+ const isSort = (chip) => chip.kind === 'sort';
17
+ /* 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. */
18
+ export const identifierText = (name) => (BARE_NAME.test(name) ? name : `"${name.replaceAll('"', '""')}"`);
19
+ /* The conditions AND-ed together, each in parentheses when it holds an OR and there is more than one. */
20
+ export const chipsToWhere = (chips) => {
21
+ const conditions = chips
22
+ .filter(isFilter)
23
+ .map((chip) => chip.sql.trim())
24
+ .filter((sql) => sql !== '');
25
+ if (conditions.length < 2) {
26
+ return conditions[0] ?? '';
27
+ }
28
+ return conditions.map((sql) => (OR.test(sql) ? `(${sql})` : sql)).join(' AND ');
29
+ };
30
+ /* The sort chips in order, each quoted for the engine; a raw ORDER BY keeps its place among them. */
31
+ export const chipsToOrderBy = (engine, chips) => chips
32
+ .flatMap((chip) => {
33
+ if (chip.kind === 'sort') {
34
+ return [orderByClause(engine, [chip])];
35
+ }
36
+ return chip.kind === 'order' ? [chip.text.trim()] : [];
37
+ })
38
+ .filter((part) => part !== '')
39
+ .join(', ');
40
+ export const sortsOf = (chips) => chips.filter(isSort).map(({ column, direction }) => ({ column, direction }));
41
+ /* The sorts of a header menu replace every sort and raw ORDER BY, keeping the filters where they are. */
42
+ export const withSorts = (chips, sorts) => [
43
+ ...chips.filter(isFilter),
44
+ ...sorts.map((sort) => ({ kind: 'sort', column: sort.column, direction: sort.direction }))
45
+ ];
46
+ /* A column that is not sorted joins as ascending; one that is flips its direction where it stands. */
47
+ export const toggleSort = (chips, column) => {
48
+ if (!chips.some((chip) => isSort(chip) && chip.column === column)) {
49
+ return [...chips, { kind: 'sort', column, direction: 'asc' }];
50
+ }
51
+ return chips.map((chip) => (isSort(chip) && chip.column === column ? { ...chip, direction: chip.direction === 'asc' ? 'desc' : 'asc' } : chip));
52
+ };
53
+ /* The same list when the chip is already in it. */
54
+ export const addChip = (chips, chip) => chips.some((other) => chipKey(other) === chipKey(chip)) ? [...chips] : [...chips, chip];
55
+ export const removeChipAt = (chips, index) => chips.filter((_, at) => at !== index);
56
+ /* Puts the chip where another stood, dropping it if an equal one is already elsewhere in the list. */
57
+ export const replaceChipAt = (chips, index, chip) => {
58
+ const duplicate = chips.some((other, at) => at !== index && chipKey(other) === chipKey(chip));
59
+ return duplicate ? removeChipAt(chips, index) : chips.map((other, at) => (at === index ? chip : other));
60
+ };
61
+ const rawFilter = (text) => ({ kind: 'filter', text, sql: text });
62
+ /* 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. */
63
+ export const filterChipsOf = (where, remembered = []) => {
64
+ const text = where.trim();
65
+ if (text === '') {
66
+ return [];
67
+ }
68
+ const kept = remembered.map((filter) => ({ kind: 'filter', text: filter.text, sql: filter.sql }));
69
+ return chipsToWhere(kept) === text ? kept : [rawFilter(text)];
70
+ };
71
+ /* A plain list of columns becomes sort chips; anything else is one chip that keeps the text. */
72
+ export const sortChipsOf = (engine, orderBy) => {
73
+ const text = orderBy.trim();
74
+ if (text === '') {
75
+ return [];
76
+ }
77
+ const sorts = parseOrderByLoose(engine, text);
78
+ return sorts === null ? [{ kind: 'order', text }] : sorts.map((sort) => ({ kind: 'sort', column: sort.column, direction: sort.direction }));
79
+ };
80
+ export const chipsFromQuery = (engine, { where, orderBy, remembered }) => [
81
+ ...filterChipsOf(where, remembered),
82
+ ...sortChipsOf(engine, orderBy)
83
+ ];
84
+ /* Whether quotes and parentheses close, outside of which an operand would swallow the rest of the text. */
85
+ const isClosed = (operand) => {
86
+ let quote = null;
87
+ let depth = 0;
88
+ for (const character of operand) {
89
+ if (quote !== null) {
90
+ quote = character === quote ? null : quote;
91
+ }
92
+ else if (character === "'" || character === '"' || character === '`') {
93
+ quote = character;
94
+ }
95
+ else if (character === '(') {
96
+ depth++;
97
+ }
98
+ else if (character === ')') {
99
+ depth--;
100
+ if (depth < 0) {
101
+ return false;
102
+ }
103
+ }
104
+ }
105
+ return quote === null && depth === 0;
106
+ };
107
+ const upper = (keyword) => keyword.replace(/\s+/g, ' ').toUpperCase();
108
+ /* The operator and operands of a condition in the form the chip shows, or `null` when the text does not read as one. */
109
+ const readOperation = (rest) => {
110
+ const compare = COMPARE.exec(rest);
111
+ if (compare !== null) {
112
+ const operator = compare[1] === '==' ? '=' : compare[1] === '!=' ? '<>' : compare[1];
113
+ return isClosed(compare[2]) ? `${operator} ${compare[2]}` : null;
114
+ }
115
+ const pattern = PATTERN.exec(rest);
116
+ if (pattern !== null) {
117
+ return isClosed(pattern[2]) ? `${upper(pattern[1])} ${pattern[2]}` : null;
118
+ }
119
+ const member = MEMBER.exec(rest);
120
+ if (member !== null) {
121
+ return isClosed(member[2]) ? `${upper(member[1])} ${member[2]}` : null;
122
+ }
123
+ const between = BETWEEN.exec(rest);
124
+ if (between !== null) {
125
+ return isClosed(between[2]) && isClosed(between[3]) ? `${upper(between[1])} ${between[2]} AND ${between[3]}` : null;
126
+ }
127
+ return IS_TEST.test(rest) ? upper(rest) : null;
128
+ };
129
+ /*
130
+ * The chip a typed text makes when it already reads as a condition: a column of the table, an
131
+ * 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.
132
+ */
133
+ export const parseCondition = (engine, text, columns) => {
134
+ const match = WORD.exec(text.trim());
135
+ if (match === null) {
136
+ return null;
137
+ }
138
+ const column = findColumn(engine, match[1], columns);
139
+ const operation = column === undefined ? null : readOperation(match[2].trim());
140
+ if (column === undefined || operation === null) {
141
+ return null;
142
+ }
143
+ return { kind: 'filter', text: `${match[1]} ${operation}`, sql: `${quoteIdentifier(engine, column)} ${operation}` };
144
+ };
145
+ /* `column = value` or `column <> value` for the cell a menu was opened on; NULL reads as IS NULL and IS NOT NULL. */
146
+ export const cellChip = (engine, column, value, kind, exclude) => {
147
+ const name = identifierText(column);
148
+ const text = value === null ? `${name} IS ${exclude ? 'NOT ' : ''}NULL` : `${name} ${exclude ? '<>' : '='} ${sqlLiteral(engine, value, kind)}`;
149
+ return { kind: 'filter', text, sql: exclude ? notEqualsCondition(engine, column, value, kind) : equalsCondition(engine, column, value, kind) };
150
+ };
151
+ /* Cuts a long text for a label, ending in an ellipsis. */
152
+ export const shorten = (text, limit) => (text.length > limit ? `${text.slice(0, limit - 1)}…` : text);
153
+ /* 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. */
154
+ const rankOf = (name, token) => {
155
+ const lower = name.toLowerCase();
156
+ if (lower === token) {
157
+ return 0;
158
+ }
159
+ if (lower.startsWith(token)) {
160
+ return 1;
161
+ }
162
+ if (lower.split(/[\s_.-]+/).some((word) => word.startsWith(token))) {
163
+ return 2;
164
+ }
165
+ return lower.includes(token) ? 3 : null;
166
+ };
167
+ /* The columns that match a typed name, the closest first and in table order among equals. */
168
+ export const matchColumns = (columns, typed, limit = SUGGESTED_COLUMNS) => {
169
+ const token = typed.trim().replace(/^[`"]/, '').replace(/[`"]$/, '').toLowerCase();
170
+ if (token === '') {
171
+ return [];
172
+ }
173
+ return columns
174
+ .map((column, order) => ({ column, order, rank: rankOf(column.name, token) }))
175
+ .filter((entry) => entry.rank !== null)
176
+ .sort((left, right) => left.rank - right.rank || left.order - right.order)
177
+ .slice(0, limit)
178
+ .map((entry) => entry.column);
179
+ };
180
+ /*
181
+ * What the popup under the field offers for the text: the condition it already is, then for each
182
+ * matching column to filter, sort or jump to it, and last the text as plain SQL. Empty text offers nothing.
183
+ */
184
+ export const suggest = (engine, text, columns, limit = SUGGESTED_COLUMNS) => {
185
+ const typed = text.trim();
186
+ if (typed === '') {
187
+ return [];
188
+ }
189
+ const suggestions = [];
190
+ const condition = parseCondition(engine, typed, columns.map((column) => column.name));
191
+ if (condition !== null) {
192
+ suggestions.push({ kind: 'condition', chip: condition });
193
+ }
194
+ if (NAME_BEING_TYPED.test(text)) {
195
+ for (const column of matchColumns(columns, typed, limit)) {
196
+ suggestions.push({ kind: 'filter', column }, { kind: 'sort', column }, { kind: 'jump', column });
197
+ }
198
+ }
199
+ suggestions.push({ kind: 'sql', chip: rawFilter(typed) });
200
+ return suggestions;
201
+ };
202
+ /* What the input holds after "Filter on <column>" is picked, ready for an operator and a value. */
203
+ export const filterStartText = (column) => `${identifierText(column.name)} `;
@@ -0,0 +1,23 @@
1
+ import { type Shown } from '../grid/display.ts';
2
+ import { type GridColumn, type GridRow } from '../grid/types.ts';
3
+ import type { EditValue, RowsResult, TableStructure } from '../protocol/index.ts';
4
+ import { type PendingChanges } from './pending.ts';
5
+ export type RowRef = {
6
+ readonly kind: 'loaded';
7
+ readonly index: number;
8
+ } | {
9
+ readonly kind: 'inserted';
10
+ readonly id: number;
11
+ };
12
+ export declare const loadedRowKey: (index: number) => string;
13
+ export declare const insertedRowKey: (id: number) => string;
14
+ export declare const parseRowKey: (key: string) => RowRef;
15
+ export declare const buildGridColumns: (loaded: Pick<RowsResult, 'columns'>, structure: TableStructure | null) => GridColumn[];
16
+ export interface GridRowsInput {
17
+ readonly structure: TableStructure | null;
18
+ readonly loaded: Pick<RowsResult, 'columns' | 'rows'>;
19
+ readonly pending: PendingChanges;
20
+ readonly offset: number;
21
+ }
22
+ export declare const buildGridRows: ({ structure, loaded, pending, offset }: GridRowsInput) => GridRow[];
23
+ export declare const cloneValues: (columns: readonly GridColumn[], cells: readonly Shown[]) => Record<string, EditValue>;
@@ -0,0 +1,55 @@
1
+ import { shownOfEdit } from '../grid/display.js';
2
+ import { wholeValueOf } from '../grid/focused-value.js';
3
+ import { gridColumnOf } from '../grid/types.js';
4
+ import { rowKeyOf } from './pending.js';
5
+ export const loadedRowKey = (index) => `row:${index}`;
6
+ export const insertedRowKey = (id) => `new:${id}`;
7
+ export const parseRowKey = (key) => key.startsWith('new:') ? { kind: 'inserted', id: Number(key.slice(4)) } : { kind: 'loaded', index: Number(key.slice(4)) };
8
+ export const buildGridColumns = (loaded, structure) => loaded.columns.map((column) => gridColumnOf(column, structure?.columns.find((info) => info.name === column.name), structure?.primaryKey ?? []));
9
+ /* The loaded rows with their pending edits laid over them, then the rows waiting to be inserted. */
10
+ export const buildGridRows = ({ structure, loaded, pending, offset }) => {
11
+ const rows = loaded.rows.map((cells, index) => {
12
+ const edits = pending.edits[index];
13
+ const edited = new Set();
14
+ const shown = cells.map((cell, column) => {
15
+ const edit = edits?.[loaded.columns[column].name];
16
+ if (edit === undefined) {
17
+ return cell;
18
+ }
19
+ edited.add(column);
20
+ return shownOfEdit(edit);
21
+ });
22
+ return {
23
+ key: loadedRowKey(index),
24
+ number: offset + index + 1,
25
+ cells: shown,
26
+ state: pending.deletes.has(index) ? 'deleted' : undefined,
27
+ edited,
28
+ locked: structure === null || rowKeyOf(structure, loaded, index) === null
29
+ };
30
+ });
31
+ const inserted = pending.inserts.map((row) => ({
32
+ key: insertedRowKey(row.id),
33
+ number: null,
34
+ cells: loaded.columns.map((column) => {
35
+ const value = row.values[column.name];
36
+ return value === undefined ? { kind: 'default' } : shownOfEdit(value);
37
+ }),
38
+ state: 'inserted'
39
+ }));
40
+ return [...rows, ...inserted];
41
+ };
42
+ /*
43
+ * The values a copy of a row starts from: what the row shows, except the columns the server fills in
44
+ * (an auto-increment key, a generated column) and the cells that are only a preview or a DEFAULT.
45
+ */
46
+ export const cloneValues = (columns, cells) => {
47
+ const values = {};
48
+ columns.forEach((column, index) => {
49
+ const value = column.autoIncrement === true || column.readOnly === true ? undefined : wholeValueOf(cells[index] ?? null);
50
+ if (value !== undefined) {
51
+ values[column.name] = value;
52
+ }
53
+ });
54
+ return values;
55
+ };
@@ -0,0 +1,7 @@
1
+ import type { ColumnInfo } from '../protocol/index.ts';
2
+ export type ImportFormat = 'csv' | 'tsv';
3
+ export declare const importableColumns: (columns: readonly ColumnInfo[]) => ColumnInfo[];
4
+ export declare const formatOfPath: (path: string) => ImportFormat;
5
+ export declare const matchColumns: (fileColumns: readonly string[], tableColumns: readonly string[], header: boolean) => (string | null)[];
6
+ export declare const hasMapping: (mapping: readonly (string | null)[]) => boolean;
7
+ export declare const mapColumn: (mapping: readonly (string | null)[], fileColumn: number, tableColumn: string | null) => (string | null)[];
@@ -0,0 +1,28 @@
1
+ /* The columns an import can fill: a generated column is computed by the server. */
2
+ export const importableColumns = (columns) => columns.filter((column) => !column.generated);
3
+ /* CSV or TSV by the extension of the path; CSV when it says neither. */
4
+ export const formatOfPath = (path) => (/\.(tsv|tab)$/i.test(path) ? 'tsv' : 'csv');
5
+ /*
6
+ * The table column each file column goes to, as a name, or `null` for none. Names match without regard
7
+ * to case and a table column takes at most one file column. A file without a header has no names to
8
+ * match, so its columns go to the table's columns in order.
9
+ */
10
+ export const matchColumns = (fileColumns, tableColumns, header) => {
11
+ if (!header) {
12
+ return fileColumns.map((_, index) => tableColumns[index] ?? null);
13
+ }
14
+ const taken = new Set();
15
+ return fileColumns.map((name) => {
16
+ const wanted = name.trim().toLowerCase();
17
+ const match = tableColumns.find((column) => !taken.has(column) && column.toLowerCase() === wanted);
18
+ if (match === undefined) {
19
+ return null;
20
+ }
21
+ taken.add(match);
22
+ return match;
23
+ });
24
+ };
25
+ /* Whether anything is mapped, since an import of nothing is no import. */
26
+ export const hasMapping = (mapping) => mapping.some((column) => column !== null);
27
+ /* Maps a file column to a table column, taking the table column from any other file column that had it. */
28
+ export const mapColumn = (mapping, fileColumn, tableColumn) => mapping.map((current, index) => (index === fileColumn ? tableColumn : tableColumn !== null && current === tableColumn ? null : current));
@@ -0,0 +1,13 @@
1
+ import type { GridLayout } from '../grid/column-layout.ts';
2
+ import type { StoredFilter } from './command-field.ts';
3
+ export declare const LAYOUT_VERSION = 1;
4
+ export interface StoredLayout extends GridLayout {
5
+ readonly pageSize: number | null;
6
+ readonly where: string;
7
+ readonly orderBy: string;
8
+ readonly filters: readonly StoredFilter[];
9
+ }
10
+ export declare const EMPTY_LAYOUT: StoredLayout;
11
+ export declare const layoutStorageKey: (connectionId: string, schema: string, table: string) => string;
12
+ export declare const serializeLayout: (layout: StoredLayout) => string;
13
+ export declare const parseStoredLayout: (text: string | null) => StoredLayout | null;
@@ -0,0 +1,43 @@
1
+ import { PAGE_SIZES } from './paging.js';
2
+ /* 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. */
3
+ export const LAYOUT_VERSION = 1;
4
+ export const EMPTY_LAYOUT = { widths: {}, hidden: [], pinned: [], pageSize: null, where: '', orderBy: '', filters: [] };
5
+ export const layoutStorageKey = (connectionId, schema, table) => `database:table:${connectionId}:${schema}.${table}`;
6
+ const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
7
+ const isStoredFilter = (value) => isRecord(value) && typeof value.text === 'string' && typeof value.sql === 'string' && value.sql.trim() !== '';
8
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ export const serializeLayout = (layout) => JSON.stringify({ version: LAYOUT_VERSION, ...layout });
10
+ /* 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. */
11
+ export const parseStoredLayout = (text) => {
12
+ if (text === null) {
13
+ return null;
14
+ }
15
+ let data;
16
+ try {
17
+ data = JSON.parse(text);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ if (!isRecord(data) || data.version !== LAYOUT_VERSION) {
23
+ return null;
24
+ }
25
+ const widths = {};
26
+ if (isRecord(data.widths)) {
27
+ for (const [name, width] of Object.entries(data.widths)) {
28
+ if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
29
+ widths[name] = width;
30
+ }
31
+ }
32
+ }
33
+ const pageSize = PAGE_SIZES.find((size) => size === data.pageSize) ?? null;
34
+ return {
35
+ widths,
36
+ hidden: isStringList(data.hidden) ? data.hidden : [],
37
+ pinned: isStringList(data.pinned) ? data.pinned : [],
38
+ pageSize,
39
+ where: typeof data.where === 'string' ? data.where : '',
40
+ orderBy: typeof data.orderBy === 'string' ? data.orderBy : '',
41
+ filters: Array.isArray(data.filters) ? data.filters.filter(isStoredFilter).map((filter) => ({ text: filter.text, sql: filter.sql })) : []
42
+ };
43
+ };
@@ -0,0 +1,12 @@
1
+ export declare const PAGE_SIZES: readonly [100, 500, 1000];
2
+ export declare const DEFAULT_PAGE_SIZE = 500;
3
+ export interface PageBounds {
4
+ readonly from: number;
5
+ readonly to: number;
6
+ readonly total: number;
7
+ readonly exact: boolean;
8
+ readonly hasPrevious: boolean;
9
+ readonly hasNext: boolean;
10
+ }
11
+ export declare const pageBounds: (offset: number, rowCount: number, hasMore: boolean, counted: number | null) => PageBounds;
12
+ export declare const lastPageOffset: (total: number, pageSize: number) => number;
@@ -0,0 +1,20 @@
1
+ export const PAGE_SIZES = [100, 500, 1000];
2
+ export const DEFAULT_PAGE_SIZE = 500;
3
+ /*
4
+ * Where a page sits. Without a count, a page that `hasMore` says only that one row exists past it, so
5
+ * the total is that many and the footer writes it as `501+`.
6
+ */
7
+ export const pageBounds = (offset, rowCount, hasMore, counted) => {
8
+ const exact = counted !== null || !hasMore;
9
+ const total = counted ?? (hasMore ? offset + rowCount + 1 : offset + rowCount);
10
+ return {
11
+ from: rowCount === 0 ? 0 : offset + 1,
12
+ to: offset + rowCount,
13
+ total,
14
+ exact,
15
+ hasPrevious: offset > 0,
16
+ hasNext: counted === null ? hasMore : offset + rowCount < counted
17
+ };
18
+ };
19
+ /* Where the last page starts, given how many rows the table has. */
20
+ export const lastPageOffset = (total, pageSize) => (total <= 0 ? 0 : Math.floor((total - 1) / pageSize) * pageSize);
@@ -0,0 +1,32 @@
1
+ import { type Cell, type EditValue, type RowChange, type RowKey, type RowsResult, type TableStructure } from '../protocol/index.ts';
2
+ export interface InsertedRow {
3
+ readonly id: number;
4
+ readonly values: Readonly<Record<string, EditValue>>;
5
+ }
6
+ export interface PendingChanges {
7
+ readonly edits: Readonly<Record<number, Readonly<Record<string, EditValue>>>>;
8
+ readonly inserts: readonly InsertedRow[];
9
+ readonly deletes: ReadonlySet<number>;
10
+ }
11
+ export declare const emptyPending: PendingChanges;
12
+ export declare const isPendingEmpty: (pending: PendingChanges) => boolean;
13
+ export declare const pendingCount: (pending: PendingChanges) => number;
14
+ export declare const valuesEqual: (left: EditValue | undefined, right: EditValue | undefined) => boolean;
15
+ export declare const setEdit: (pending: PendingChanges, row: number, column: string, value: EditValue, original: Cell) => PendingChanges;
16
+ export declare const addInsertWith: (pending: PendingChanges, values: Readonly<Record<string, EditValue>>) => PendingChanges;
17
+ export declare const addInsert: (pending: PendingChanges) => PendingChanges;
18
+ export declare const setInsertValue: (pending: PendingChanges, id: number, column: string, value: EditValue) => PendingChanges;
19
+ export declare const removeInsert: (pending: PendingChanges, id: number) => PendingChanges;
20
+ export declare const markDeleted: (pending: PendingChanges, rows: readonly number[]) => PendingChanges;
21
+ export declare const revertRow: (pending: PendingChanges, row: number) => PendingChanges;
22
+ export interface RowSelectionRefs {
23
+ readonly loaded: readonly number[];
24
+ readonly inserted: readonly number[];
25
+ }
26
+ export declare const revertRows: (pending: PendingChanges, rows: RowSelectionRefs) => PendingChanges;
27
+ export declare const hasRowChanges: (pending: PendingChanges, rows: RowSelectionRefs) => boolean;
28
+ type LoadedRows = Pick<RowsResult, 'columns' | 'rows'>;
29
+ export declare const rowKeyOf: (structure: TableStructure, loaded: LoadedRows, row: number) => RowKey | null;
30
+ export declare const toRowChanges: (structure: TableStructure, loaded: LoadedRows, pending: PendingChanges) => RowChange[];
31
+ export declare const describeKey: (key: RowKey) => string;
32
+ export {};
@@ -0,0 +1,94 @@
1
+ import { valueOfCell } from '../protocol/index.js';
2
+ export const emptyPending = { edits: {}, inserts: [], deletes: new Set() };
3
+ export const isPendingEmpty = (pending) => Object.keys(pending.edits).length === 0 && pending.inserts.length === 0 && pending.deletes.size === 0;
4
+ /* How many row changes a submit sends: an edited row that is also deleted counts once, as the delete. */
5
+ export const pendingCount = (pending) => Object.keys(pending.edits).filter((row) => !pending.deletes.has(Number(row))).length + pending.inserts.length + pending.deletes.size;
6
+ export const valuesEqual = (left, right) => {
7
+ if (left === undefined || right === undefined) {
8
+ return false;
9
+ }
10
+ if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
11
+ return left === right;
12
+ }
13
+ return left.kind === right.kind && (left.kind !== 'binary' || (right.kind === 'binary' && left.hex === right.hex));
14
+ };
15
+ /* Sets a cell of a loaded row; setting it back to what the row held removes the edit. */
16
+ export const setEdit = (pending, row, column, value, original) => {
17
+ const { [row]: current = {}, ...others } = pending.edits;
18
+ const { [column]: _replaced, ...kept } = current;
19
+ const next = valuesEqual(value, valueOfCell(original)) ? kept : { ...kept, [column]: value };
20
+ return { ...pending, edits: Object.keys(next).length === 0 ? others : { ...others, [row]: next } };
21
+ };
22
+ /* A new row to insert, filled in with `values`. */
23
+ export const addInsertWith = (pending, values) => ({
24
+ ...pending,
25
+ inserts: [...pending.inserts, { id: pending.inserts.reduce((highest, row) => Math.max(highest, row.id), 0) + 1, values }]
26
+ });
27
+ export const addInsert = (pending) => addInsertWith(pending, {});
28
+ export const setInsertValue = (pending, id, column, value) => ({
29
+ ...pending,
30
+ inserts: pending.inserts.map((row) => (row.id === id ? { ...row, values: { ...row.values, [column]: value } } : row))
31
+ });
32
+ export const removeInsert = (pending, id) => ({ ...pending, inserts: pending.inserts.filter((row) => row.id !== id) });
33
+ export const markDeleted = (pending, rows) => ({
34
+ ...pending,
35
+ deletes: new Set([...pending.deletes, ...rows])
36
+ });
37
+ /* Takes back every change to one loaded row: its edits and its deletion. */
38
+ export const revertRow = (pending, row) => {
39
+ const { [row]: _dropped, ...edits } = pending.edits;
40
+ const deletes = new Set(pending.deletes);
41
+ deletes.delete(row);
42
+ return { ...pending, edits, deletes };
43
+ };
44
+ /* Takes back every change to the given rows: the edits and deletion marks of the loaded ones, and the inserted ones altogether. */
45
+ export const revertRows = (pending, rows) => {
46
+ const reverted = rows.loaded.reduce(revertRow, pending);
47
+ return { ...reverted, inserts: reverted.inserts.filter((row) => !rows.inserted.includes(row.id)) };
48
+ };
49
+ /* Whether any of the given rows has something to take back. */
50
+ export const hasRowChanges = (pending, rows) => rows.loaded.some((row) => pending.edits[row] !== undefined || pending.deletes.has(row)) || pending.inserts.some((row) => rows.inserted.includes(row.id));
51
+ /*
52
+ * The key that picks out one loaded row, from its own values in the columns of the structure's row
53
+ * key. `null` when the table has no row key, or a key cell is only a preview, in which case the row
54
+ * cannot be told apart safely and stays read only.
55
+ */
56
+ export const rowKeyOf = (structure, loaded, row) => {
57
+ const cells = loaded.rows[row];
58
+ if (structure.rowKey === null || cells === undefined) {
59
+ return null;
60
+ }
61
+ const key = {};
62
+ for (const name of structure.rowKey) {
63
+ const index = loaded.columns.findIndex((column) => column.name === name);
64
+ const value = index < 0 ? undefined : valueOfCell(cells[index]);
65
+ if (value === undefined) {
66
+ return null;
67
+ }
68
+ key[name] = value;
69
+ }
70
+ return key;
71
+ };
72
+ const numeric = (text) => Number(text);
73
+ /* 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. */
74
+ export const toRowChanges = (structure, loaded, pending) => {
75
+ const keyOf = (row) => {
76
+ const key = rowKeyOf(structure, loaded, row);
77
+ if (key === null) {
78
+ throw new Error(`Row ${row + 1} of ${structure.name} has no usable key.`);
79
+ }
80
+ return key;
81
+ };
82
+ const updates = Object.keys(pending.edits)
83
+ .map(numeric)
84
+ .filter((row) => !pending.deletes.has(row))
85
+ .sort((left, right) => left - right)
86
+ .map((row) => ({ kind: 'update', key: keyOf(row), values: pending.edits[row] }));
87
+ const deletes = [...pending.deletes].sort((left, right) => left - right).map((row) => ({ kind: 'delete', key: keyOf(row) }));
88
+ const inserts = pending.inserts.map((row) => ({ kind: 'insert', values: row.values }));
89
+ return [...updates, ...deletes, ...inserts];
90
+ };
91
+ /* A key as a person would write it in a WHERE: `id = 5, tenant = 'a'`. */
92
+ export const describeKey = (key) => Object.entries(key)
93
+ .map(([column, value]) => `${column} = ${value === null ? 'NULL' : typeof value === 'object' ? `0x${value.hex}` : typeof value === 'string' ? `'${value}'` : String(value)}`)
94
+ .join(', ');
@@ -0,0 +1,12 @@
1
+ import type { Shown } from '../grid/display.ts';
2
+ import type { Engine, ForeignKeyInfo, ValueKind } from '../protocol/index.ts';
3
+ export interface Reference {
4
+ readonly schema: string;
5
+ readonly table: string;
6
+ readonly where: string;
7
+ }
8
+ export declare const foreignKeyOf: (foreignKeys: readonly ForeignKeyInfo[], column: string) => ForeignKeyInfo | undefined;
9
+ export declare const referenceOf: (engine: Engine, key: ForeignKeyInfo, columns: readonly {
10
+ readonly name: string;
11
+ readonly kind: ValueKind;
12
+ }[], cells: readonly Shown[]) => Reference | null;
@@ -0,0 +1,21 @@
1
+ import { wholeValueOf } from '../grid/focused-value.js';
2
+ import { matchCondition } from '../sql.js';
3
+ /* The foreign key a column is part of; the first one when a column sits in several. */
4
+ export const foreignKeyOf = (foreignKeys, column) => foreignKeys.find((key) => key.columns.includes(column));
5
+ /*
6
+ * The row a foreign key points to, from the cells of the referencing row. `null` when any column of
7
+ * the key is NULL (it references nothing), is only a preview, or is not among the columns.
8
+ */
9
+ export const referenceOf = (engine, key, columns, cells) => {
10
+ const matches = [];
11
+ for (const [position, name] of key.columns.entries()) {
12
+ const referenced = key.referencedColumns[position];
13
+ const index = columns.findIndex((column) => column.name === name);
14
+ const value = index < 0 ? undefined : wholeValueOf(cells[index] ?? null);
15
+ if (referenced === undefined || value === undefined || value === null) {
16
+ return null;
17
+ }
18
+ matches.push({ column: referenced, value, kind: columns[index].kind });
19
+ }
20
+ return matches.length === 0 ? null : { schema: key.referencedSchema, table: key.referencedTable, where: matchCondition(engine, matches) };
21
+ };
@@ -0,0 +1,7 @@
1
+ export interface Loaded<T> {
2
+ value: T | null;
3
+ loading: boolean;
4
+ error: string | null;
5
+ reload(): void;
6
+ }
7
+ export declare function useLoaded<T>(load: (signal: AbortSignal) => Promise<T>, key: string): Loaded<T>;
@@ -0,0 +1,36 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { messageOf } from '@adecore/ui';
3
+ /*
4
+ * Runs `load` when `key` changes or `reload` is called, and aborts the run that is still going.
5
+ * `key` has to name everything `load` reads, since `load` itself is a new function on every render.
6
+ * A value belongs to the hook's owner, which is remounted for a different subject, so it outlives a key change.
7
+ */
8
+ export function useLoaded(load, key) {
9
+ const loader = useRef(load);
10
+ const [version, setVersion] = useState(0);
11
+ // Loading is not a state of its own: it is that the last answer is not for this key and this version.
12
+ const [state, setState] = useState({ key: '', version: -1, value: null, error: null });
13
+ useEffect(() => {
14
+ loader.current = load;
15
+ });
16
+ useEffect(() => {
17
+ const controller = new AbortController();
18
+ loader.current(controller.signal).then((value) => {
19
+ if (!controller.signal.aborted) {
20
+ setState({ key, version, value, error: null });
21
+ }
22
+ }, (error) => {
23
+ if (!controller.signal.aborted) {
24
+ setState((now) => ({ key, version, value: now.value, error: messageOf(error) }));
25
+ }
26
+ });
27
+ return () => controller.abort();
28
+ }, [key, version]);
29
+ const answered = state.key === key && state.version === version;
30
+ return {
31
+ value: state.value,
32
+ loading: !answered,
33
+ error: answered ? state.error : null,
34
+ reload: () => setVersion((now) => now + 1)
35
+ };
36
+ }
@@ -0,0 +1,8 @@
1
+ import type { DatabaseStorage } from '../actions.ts';
2
+ import { type StoredLayout } from './layout-store.ts';
3
+ export declare const STORE_DELAY_MS = 500;
4
+ export interface StoredLayoutHandle {
5
+ readonly stored: StoredLayout | null;
6
+ remember(change: Partial<StoredLayout>): void;
7
+ }
8
+ export declare function useStoredLayout(storage: DatabaseStorage | undefined, key: string): StoredLayoutHandle;