@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,47 @@
1
+ import { connectionOfKey, isFolderKey } from './tree.ts';
2
+
3
+ const VERSION = 1;
4
+
5
+ export interface Expansion {
6
+ /* Connections, schemas and tables the person opened. */
7
+ readonly expanded: ReadonlySet<string>;
8
+ /* Folders the person closed, since a folder is open until then. */
9
+ readonly collapsed: ReadonlySet<string>;
10
+ }
11
+
12
+ export const expansionKey = (connectionId: string): string => `database:explorer:${connectionId}`;
13
+
14
+ /* Reads what `serializeExpansion` wrote. Anything that does not fit gives nothing open and no folder closed. */
15
+ export const parseExpansion = (raw: string | null, connectionId: string): Expansion => {
16
+ const none: Expansion = { expanded: new Set(), collapsed: new Set() };
17
+ if (raw === null) {
18
+ return none;
19
+ }
20
+ try {
21
+ const parsed: unknown = JSON.parse(raw);
22
+ if (typeof parsed !== 'object' || parsed === null || (parsed as { version?: unknown }).version !== VERSION) {
23
+ return none;
24
+ }
25
+ const { expanded, collapsed } = parsed as { expanded?: unknown; collapsed?: unknown };
26
+ const own = (keys: unknown, folders: boolean): Set<string> =>
27
+ new Set(
28
+ Array.isArray(keys)
29
+ ? keys.filter(
30
+ (key): key is string =>
31
+ typeof key === 'string' && key.length > 2 && isFolderKey(key) === folders && connectionOfKey(key) === connectionId
32
+ )
33
+ : []
34
+ );
35
+ return { expanded: own(expanded, false), collapsed: own(collapsed, true) };
36
+ } catch {
37
+ return none;
38
+ }
39
+ };
40
+
41
+ /* What a connection has open as the text to store, or `null` when everything is as it starts, which removes the key. */
42
+ export const serializeExpansion = (connectionId: string, { expanded, collapsed }: Expansion): string | null => {
43
+ const own = (keys: ReadonlySet<string>): string[] => [...keys].filter((key) => connectionOfKey(key) === connectionId).sort();
44
+ const open = own(expanded);
45
+ const closed = own(collapsed);
46
+ return open.length === 0 && closed.length === 0 ? null : JSON.stringify({ version: VERSION, expanded: open, collapsed: closed });
47
+ };
@@ -0,0 +1,18 @@
1
+ import type { TableRef } from '../client/types.ts';
2
+ import { dropTableSql, dropViewSql, renameTableSql, truncateTableSql, type Dialect } from '../ddl/index.ts';
3
+ import type { TableKind } from '../protocol/index.ts';
4
+
5
+ /* What the explorer's menu changes about a table: its name, its rows, or its existence. */
6
+ export type TableChange = 'rename' | 'truncate' | 'drop';
7
+
8
+ /* The statement for a change; `to` is the new name of a rename. */
9
+ export const statementOf = (dialect: Dialect, change: TableChange, ref: TableRef, kind: TableKind, to = ref.table): string => {
10
+ switch (change) {
11
+ case 'rename':
12
+ return renameTableSql(dialect, ref.schema, ref.table, to);
13
+ case 'truncate':
14
+ return truncateTableSql(dialect, ref.schema, ref.table);
15
+ case 'drop':
16
+ return kind === 'view' ? dropViewSql(dialect, ref.schema, ref.table) : dropTableSql(dialect, ref.schema, ref.table);
17
+ }
18
+ };
@@ -0,0 +1,453 @@
1
+ import type { ExplorerSelection } from '../actions.ts';
2
+ import type { Connection, TableRef } from '../client/types.ts';
3
+ import type { ColumnInfo, Engine, SchemaInfo, TableInfo, TableKind, TableStructure } from '../protocol/index.ts';
4
+ import { qualifiedName } from '../sql.ts';
5
+
6
+ /* Where a lazy list stands: not asked for yet, on its way, failed, or here. */
7
+ export type Load<T> =
8
+ | { readonly status: 'idle' }
9
+ | { readonly status: 'loading' }
10
+ | { readonly status: 'error'; readonly message: string }
11
+ | { readonly status: 'ready'; readonly value: T };
12
+
13
+ export const IDLE: Load<never> = { status: 'idle' };
14
+
15
+ /* What a load fetches: the schemas of a connection, the tables of one schema, or the structure of one table. */
16
+ export interface LoadTarget {
17
+ readonly connectionId: string;
18
+ readonly schema?: string;
19
+ /* Only with `schema`. */
20
+ readonly table?: string;
21
+ }
22
+
23
+ const SEPARATOR = '\u0000';
24
+
25
+ export const connectionKey = (connectionId: string): string => `c:${connectionId}`;
26
+ export const schemaKey = (connectionId: string, schema: string): string => `s:${connectionId}${SEPARATOR}${schema}`;
27
+ export const folderKey = (connectionId: string, schema: string, group: TableKind): string => `f:${connectionId}${SEPARATOR}${schema}${SEPARATOR}${group}`;
28
+ export const tableKey = (ref: TableRef): string => `t:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}`;
29
+ export const isFolderKey = (key: string): boolean => key.startsWith('f:');
30
+ /* The connection a key of a connection, schema, folder or table belongs to. */
31
+ export const connectionOfKey = (key: string): string => {
32
+ const rest = key.slice(2);
33
+ const end = rest.indexOf(SEPARATOR);
34
+ return end === -1 ? rest : rest.slice(0, end);
35
+ };
36
+ export const columnKey = (ref: TableRef, column: string): string => `${tableKey(ref)}${SEPARATOR}${column}`;
37
+
38
+ export const loadKey = (target: LoadTarget): string => {
39
+ if (target.schema === undefined) {
40
+ return connectionKey(target.connectionId);
41
+ }
42
+ return target.table === undefined
43
+ ? schemaKey(target.connectionId, target.schema)
44
+ : tableKey({ connectionId: target.connectionId, schema: target.schema, table: target.table });
45
+ };
46
+
47
+ /* The kinds of table a schema lists, in the order of its folders. */
48
+ export const GROUPS: readonly TableKind[] = ['table', 'view'];
49
+
50
+ interface RowBase {
51
+ readonly key: string;
52
+ /* 1 for a connection. */
53
+ readonly level: number;
54
+ readonly parent: string | null;
55
+ readonly posInSet: number;
56
+ readonly setSize: number;
57
+ /* A row that says it is loading is no stop for the arrow keys. */
58
+ readonly focusable: boolean;
59
+ }
60
+
61
+ export interface ConnectionRow extends RowBase {
62
+ readonly kind: 'connection';
63
+ readonly connection: Connection;
64
+ readonly expanded: boolean;
65
+ /* What the server reported once the person opened the connection; `null` before that. */
66
+ readonly version: string | null;
67
+ /* Only when the system schemas are hidden and there are some: how many show out of how many exist. */
68
+ readonly schemaCount: { readonly shown: number; readonly total: number } | null;
69
+ }
70
+
71
+ export interface SchemaRow extends RowBase {
72
+ readonly kind: 'schema';
73
+ readonly connection: Connection;
74
+ readonly schema: string;
75
+ readonly expanded: boolean;
76
+ }
77
+
78
+ /* The "Tables" or the "Views" of a schema; it exists only when the schema has some. */
79
+ export interface FolderRow extends RowBase {
80
+ readonly kind: 'folder';
81
+ readonly connection: Connection;
82
+ readonly schema: string;
83
+ readonly group: TableKind;
84
+ readonly expanded: boolean;
85
+ /* The tables loaded, or those that match the filter. */
86
+ readonly count: number;
87
+ }
88
+
89
+ export interface TableRow extends RowBase {
90
+ readonly kind: 'table';
91
+ readonly ref: TableRef;
92
+ readonly table: TableInfo;
93
+ readonly expanded: boolean;
94
+ }
95
+
96
+ export interface ColumnRow extends RowBase {
97
+ readonly kind: 'column';
98
+ /* The table the column belongs to, which is what opening it opens. */
99
+ readonly ref: TableRef;
100
+ readonly column: ColumnInfo;
101
+ readonly primaryKey: boolean;
102
+ readonly foreignKey: boolean;
103
+ }
104
+
105
+ export interface LoadingRow extends RowBase {
106
+ readonly kind: 'loading';
107
+ }
108
+
109
+ export interface ErrorRow extends RowBase {
110
+ readonly kind: 'error';
111
+ readonly message: string;
112
+ readonly retry: LoadTarget;
113
+ }
114
+
115
+ /* A connection without schemas, a schema without tables or a table without columns. */
116
+ export interface EmptyRow extends RowBase {
117
+ readonly kind: 'empty';
118
+ readonly of: 'schemas' | 'tables' | 'columns';
119
+ }
120
+
121
+ export type TreeRow = ConnectionRow | SchemaRow | FolderRow | TableRow | ColumnRow | LoadingRow | ErrorRow | EmptyRow;
122
+
123
+ export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow;
124
+
125
+ export const isExpandable = (row: TreeRow): row is ExpandableRow =>
126
+ row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table';
127
+
128
+ /* The table a row stands for, when it stands for one. */
129
+ export const tableOf = (row: TreeRow): TableRef | null => (row.kind === 'table' || row.kind === 'column' ? row.ref : null);
130
+
131
+ /* What picking a row selects: its connection, its schema, or its table (a column stands for its table). Status rows select nothing. */
132
+ export const selectionOf = (row: TreeRow): ExplorerSelection | null => {
133
+ switch (row.kind) {
134
+ case 'connection':
135
+ return { connectionId: row.connection.id };
136
+ case 'schema':
137
+ case 'folder':
138
+ return { connectionId: row.connection.id, schema: row.schema };
139
+ case 'table':
140
+ case 'column':
141
+ return { connectionId: row.ref.connectionId, schema: row.ref.schema, table: row.ref.table };
142
+ default:
143
+ return null;
144
+ }
145
+ };
146
+
147
+ /* The key of the row a selection points at: its table, else its schema, else its connection. */
148
+ export const selectionKey = (selection: ExplorerSelection): string => {
149
+ if (selection.schema === undefined) {
150
+ return connectionKey(selection.connectionId);
151
+ }
152
+ return selection.table === undefined
153
+ ? schemaKey(selection.connectionId, selection.schema)
154
+ : tableKey({ connectionId: selection.connectionId, schema: selection.schema, table: selection.table });
155
+ };
156
+
157
+ /* Whether a row has a menu: the rows of a connection, a schema, a table or a column, and none of the rows that only report. */
158
+ export const hasMenu = (row: TreeRow): boolean =>
159
+ row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table' || row.kind === 'column';
160
+
161
+ /* Only the rows a selection can point at are drawn selected; a folder or a column leaves that to the row above it. */
162
+ export const isSelectable = (row: TreeRow): boolean => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'table';
163
+
164
+ /* The statement behind New console here on a table: every row, as the engine quotes it. */
165
+ export const selectAllSql = (engine: Engine, ref: TableRef): string => `SELECT * FROM ${qualifiedName({ engine, schema: ref.schema, table: ref.table })}`;
166
+
167
+ export interface TreeInput {
168
+ readonly connections: readonly Connection[];
169
+ /* Connections, schemas and tables the person opened. Folders are open unless they are in `collapsed`. */
170
+ readonly expanded: ReadonlySet<string>;
171
+ readonly collapsed: ReadonlySet<string>;
172
+ readonly filter: string;
173
+ readonly showSystemSchemas: boolean;
174
+ schemas(connectionId: string): Load<readonly SchemaInfo[]>;
175
+ tables(connectionId: string, schema: string): Load<readonly TableInfo[]>;
176
+ structures(connectionId: string, schema: string, table: string): Load<TableStructure>;
177
+ versions(connectionId: string): string | null;
178
+ }
179
+
180
+ /* The schemas a list shows. */
181
+ export const visibleSchemas = (schemas: readonly SchemaInfo[], showSystemSchemas: boolean): readonly SchemaInfo[] =>
182
+ showSystemSchemas ? schemas : schemas.filter((schema) => !schema.system);
183
+
184
+ /* One schema is no level worth a row: its folders hang straight under the connection. */
185
+ const soleSchema = (visible: readonly SchemaInfo[]): string | null => (visible.length === 1 ? visible[0]!.name : null);
186
+
187
+ interface Place {
188
+ readonly posInSet: number;
189
+ readonly setSize: number;
190
+ }
191
+
192
+ /* A row before its place among its siblings is known, with the rows under it. */
193
+ interface Draft {
194
+ readonly make: (place: Place) => TreeRow;
195
+ readonly below: readonly TreeRow[];
196
+ }
197
+
198
+ /* Gives the drafts of one parent their place among each other, and lays them out depth first. */
199
+ const settle = (drafts: readonly Draft[]): TreeRow[] => drafts.flatMap((draft, i) => [draft.make({ posInSet: i + 1, setSize: drafts.length }), ...draft.below]);
200
+
201
+ type StatusRow = { kind: 'loading' } | { kind: 'empty'; of: EmptyRow['of'] } | { kind: 'error'; message: string; retry: LoadTarget };
202
+
203
+ const statusDraft = (row: StatusRow, level: number, parent: string, focusable: boolean): Draft => ({
204
+ make: (place) => ({ ...row, key: `${row.kind}:${parent}`, level, parent, focusable, ...place }),
205
+ below: []
206
+ });
207
+
208
+ const matches = (name: string, query: string): boolean => name.toLowerCase().includes(query);
209
+
210
+ /*
211
+ * The rows a tree shows, top to bottom. With a filter only the loaded tables that match stay, under
212
+ * their folder, schema and connection, which then count as open whatever the person left them at.
213
+ */
214
+ export const flattenTree = (input: TreeInput): TreeRow[] => {
215
+ const query = input.filter.trim().toLowerCase();
216
+ const filtering = query !== '';
217
+
218
+ const columnDrafts = (ref: TableRef, level: number, parent: string): Draft[] => {
219
+ const load = input.structures(ref.connectionId, ref.schema, ref.table);
220
+ if (load.status !== 'ready') {
221
+ const row: StatusRow =
222
+ load.status === 'error'
223
+ ? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
224
+ : { kind: 'loading' };
225
+ return [statusDraft(row, level, parent, load.status === 'error')];
226
+ }
227
+ const { columns, primaryKey, foreignKeys } = load.value;
228
+ if (columns.length === 0) {
229
+ return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
230
+ }
231
+ const primary = new Set(primaryKey);
232
+ const foreign = new Set(foreignKeys.flatMap((foreignKey) => foreignKey.columns));
233
+ return columns.map((column): Draft => ({
234
+ make: (place) => ({
235
+ kind: 'column',
236
+ key: columnKey(ref, column.name),
237
+ level,
238
+ parent,
239
+ focusable: true,
240
+ ref,
241
+ column,
242
+ primaryKey: primary.has(column.name),
243
+ foreignKey: foreign.has(column.name),
244
+ ...place
245
+ }),
246
+ below: []
247
+ }));
248
+ };
249
+
250
+ const tableDraft = (ref: TableRef, table: TableInfo, level: number, parent: string): Draft => {
251
+ const key = tableKey(ref);
252
+ const open = input.expanded.has(key);
253
+ return {
254
+ make: (place) => ({ kind: 'table', key, level, parent, focusable: true, ref, table, expanded: open, ...place }),
255
+ below: open ? settle(columnDrafts(ref, level + 1, key)) : []
256
+ };
257
+ };
258
+
259
+ /* The folders under a schema (or under a connection with only that schema), and how many tables they hold. */
260
+ const folderDrafts = (connection: Connection, schema: string, level: number, parent: string): { drafts: Draft[]; tables: number } => {
261
+ const load = input.tables(connection.id, schema);
262
+ if (load.status === 'ready') {
263
+ const shown = filtering ? load.value.filter((table) => matches(table.name, query)) : load.value;
264
+ if (shown.length === 0 && !filtering) {
265
+ return { drafts: [statusDraft({ kind: 'empty', of: 'tables' }, level, parent, true)], tables: 0 };
266
+ }
267
+ const drafts = GROUPS.flatMap((group): Draft[] => {
268
+ const members = shown.filter((table) => table.kind === group);
269
+ if (members.length === 0) {
270
+ return [];
271
+ }
272
+ const key = folderKey(connection.id, schema, group);
273
+ const open = filtering || !input.collapsed.has(key);
274
+ const tables = members.map((table) => tableDraft({ connectionId: connection.id, schema, table: table.name }, table, level + 1, key));
275
+ return [
276
+ {
277
+ make: (place) => ({
278
+ kind: 'folder',
279
+ key,
280
+ level,
281
+ parent,
282
+ focusable: true,
283
+ connection,
284
+ schema,
285
+ group,
286
+ expanded: open,
287
+ count: members.length,
288
+ ...place
289
+ }),
290
+ below: open ? settle(tables) : []
291
+ }
292
+ ];
293
+ });
294
+ return { drafts, tables: shown.length };
295
+ }
296
+ if (filtering) {
297
+ return { drafts: [], tables: 0 };
298
+ }
299
+ const row: StatusRow =
300
+ load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id, schema } } : { kind: 'loading' };
301
+ return { drafts: [statusDraft(row, level, parent, load.status === 'error')], tables: 0 };
302
+ };
303
+
304
+ const schemaDraft = (connection: Connection, schema: string, parent: string): Draft | null => {
305
+ const key = schemaKey(connection.id, schema);
306
+ const { drafts, tables } = folderDrafts(connection, schema, 3, key);
307
+ if (filtering && tables === 0) {
308
+ return null;
309
+ }
310
+ const open = filtering || input.expanded.has(key);
311
+ return {
312
+ make: (place) => ({ kind: 'schema', key, level: 2, parent, focusable: true, connection, schema, expanded: open, ...place }),
313
+ below: open ? settle(drafts) : []
314
+ };
315
+ };
316
+
317
+ const connectionDraft = (connection: Connection): Draft | null => {
318
+ const key = connectionKey(connection.id);
319
+ const load = input.schemas(connection.id);
320
+ const open = filtering ? load.status === 'ready' : input.expanded.has(key);
321
+ let children: Draft[] = [];
322
+ let schemaCount: ConnectionRow['schemaCount'] = null;
323
+ if (load.status === 'ready') {
324
+ const visible = visibleSchemas(load.value, input.showSystemSchemas);
325
+ const sole = soleSchema(visible);
326
+ if (visible.length < load.value.length) {
327
+ schemaCount = { shown: visible.length, total: load.value.length };
328
+ }
329
+ if (sole !== null) {
330
+ children = open ? folderDrafts(connection, sole, 2, key).drafts : [];
331
+ } else if (visible.length === 0) {
332
+ children = open && !filtering ? [statusDraft({ kind: 'empty', of: 'schemas' }, 2, key, true)] : [];
333
+ } else {
334
+ children = visible.flatMap((entry) => schemaDraft(connection, entry.name, key) ?? []);
335
+ }
336
+ } else if (open) {
337
+ const row: StatusRow =
338
+ load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id } } : { kind: 'loading' };
339
+ children = [statusDraft(row, 2, key, load.status === 'error')];
340
+ }
341
+ if (filtering && children.length === 0) {
342
+ return null;
343
+ }
344
+ const version = input.versions(connection.id);
345
+ return {
346
+ make: (place) => ({ kind: 'connection', key, level: 1, parent: null, focusable: true, connection, expanded: open, version, schemaCount, ...place }),
347
+ below: open ? settle(children) : []
348
+ };
349
+ };
350
+
351
+ return settle(input.connections.flatMap((connection) => connectionDraft(connection) ?? []));
352
+ };
353
+
354
+ /* The loads an open node is waiting for and nobody has asked for yet. */
355
+ export const neededLoads = (input: Omit<TreeInput, 'filter'>): LoadTarget[] => {
356
+ const needed: LoadTarget[] = [];
357
+ for (const connection of input.connections) {
358
+ if (!input.expanded.has(connectionKey(connection.id))) {
359
+ continue;
360
+ }
361
+ const load = input.schemas(connection.id);
362
+ if (load.status === 'idle') {
363
+ needed.push({ connectionId: connection.id });
364
+ continue;
365
+ }
366
+ if (load.status !== 'ready') {
367
+ continue;
368
+ }
369
+ const visible = visibleSchemas(load.value, input.showSystemSchemas);
370
+ const sole = soleSchema(visible);
371
+ const wanted = sole !== null ? [sole] : visible.map((schema) => schema.name).filter((name) => input.expanded.has(schemaKey(connection.id, name)));
372
+ for (const schema of wanted) {
373
+ const tables = input.tables(connection.id, schema);
374
+ if (tables.status === 'idle') {
375
+ needed.push({ connectionId: connection.id, schema });
376
+ continue;
377
+ }
378
+ if (tables.status !== 'ready') {
379
+ continue;
380
+ }
381
+ for (const table of tables.value) {
382
+ if (
383
+ input.expanded.has(tableKey({ connectionId: connection.id, schema, table: table.name })) &&
384
+ input.structures(connection.id, schema, table.name).status === 'idle'
385
+ ) {
386
+ needed.push({ connectionId: connection.id, schema, table: table.name });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ return needed;
392
+ };
393
+
394
+ export type TreeAction =
395
+ | { readonly type: 'focus'; readonly key: string }
396
+ | { readonly type: 'expand'; readonly key: string }
397
+ | { readonly type: 'collapse'; readonly key: string }
398
+ /* Enter or Space: opens a table (or the table of a column), toggles a node, retries a failed load. */
399
+ | { readonly type: 'activate'; readonly key: string };
400
+
401
+ /* What a key does to the row that has the focus, or `null` when it does nothing there. */
402
+ export const navigate = (rows: readonly TreeRow[], activeKey: string | null, pressed: string): TreeAction | null => {
403
+ const stops = rows.filter((row) => row.focusable);
404
+ if (stops.length === 0) {
405
+ return null;
406
+ }
407
+ const at = stops.findIndex((row) => row.key === activeKey);
408
+ const row = at === -1 ? null : stops[at]!;
409
+ switch (pressed) {
410
+ case 'ArrowDown':
411
+ return { type: 'focus', key: stops[Math.min(at + 1, stops.length - 1)]!.key };
412
+ case 'ArrowUp':
413
+ return { type: 'focus', key: stops[Math.max(at - 1, 0)]!.key };
414
+ case 'Home':
415
+ return { type: 'focus', key: stops[0]!.key };
416
+ case 'End':
417
+ return { type: 'focus', key: stops[stops.length - 1]!.key };
418
+ case 'ArrowRight': {
419
+ if (row === null || !isExpandable(row)) {
420
+ return null;
421
+ }
422
+ if (!row.expanded) {
423
+ return { type: 'expand', key: row.key };
424
+ }
425
+ const child = stops.find((candidate) => candidate.parent === row.key);
426
+ return child === undefined ? null : { type: 'focus', key: child.key };
427
+ }
428
+ case 'ArrowLeft': {
429
+ if (row === null) {
430
+ return null;
431
+ }
432
+ if (isExpandable(row) && row.expanded) {
433
+ return { type: 'collapse', key: row.key };
434
+ }
435
+ return row.parent === null ? null : { type: 'focus', key: row.parent };
436
+ }
437
+ case 'Enter':
438
+ case ' ':
439
+ return row === null ? null : { type: 'activate', key: row.key };
440
+ default:
441
+ return null;
442
+ }
443
+ };
444
+
445
+ /* The row that takes the tab stop: the one with the focus, else the selected table, else the first. */
446
+ export const tabStop = (rows: readonly TreeRow[], activeKey: string | null, selectedKey: string | null): string | null => {
447
+ for (const key of [activeKey, selectedKey]) {
448
+ if (key !== null && rows.some((row) => row.key === key && row.focusable)) {
449
+ return key;
450
+ }
451
+ }
452
+ return rows.find((row) => row.focusable)?.key ?? null;
453
+ };
@@ -0,0 +1,157 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { messageOf } from '@adecore/ui';
3
+ import { useDatabaseClient } from '../client-context.ts';
4
+ import type { Connection, SchemaChange } from '../client/types.ts';
5
+ import type { SchemaInfo, TableInfo, TableStructure } from '../protocol/index.ts';
6
+ import { IDLE, loadKey, type Load, type LoadTarget } from './tree.ts';
7
+
8
+ type Loaded = readonly SchemaInfo[] | readonly TableInfo[] | TableStructure;
9
+
10
+ interface Entry {
11
+ readonly target: LoadTarget;
12
+ readonly load: Load<Loaded>;
13
+ }
14
+
15
+ interface Loads {
16
+ readonly entries: ReadonlyMap<string, Entry>;
17
+ /* The version each server reported, by connection. */
18
+ readonly versions: ReadonlyMap<string, string>;
19
+ }
20
+
21
+ const EMPTY: Loads = { entries: new Map(), versions: new Map() };
22
+
23
+ export interface ExplorerLoads {
24
+ schemas(connectionId: string): Load<readonly SchemaInfo[]>;
25
+ tables(connectionId: string, schema: string): Load<readonly TableInfo[]>;
26
+ structures(connectionId: string, schema: string, table: string): Load<TableStructure>;
27
+ /* The server version of a connection, once its schemas have loaded; `null` before. */
28
+ versions(connectionId: string): string | null;
29
+ /* Starts a load nobody has started yet; a no-op for one that is running or done. */
30
+ start(target: LoadTarget): void;
31
+ /* Forgets what was loaded under a connection, under one schema, or for one table, so an open node asks again. */
32
+ reset(target: LoadTarget): void;
33
+ }
34
+
35
+ /* Whether a load belongs to what a reset names: the same connection, and the same schema or table when the reset names one. */
36
+ const within = (reset: LoadTarget, target: LoadTarget): boolean =>
37
+ reset.connectionId === target.connectionId &&
38
+ (reset.schema === undefined || reset.schema === target.schema) &&
39
+ (reset.table === undefined || reset.table === target.table);
40
+
41
+ /* A change to a schema also changes the schema list, and one without a schema changes every list of the connection. */
42
+ const touches = (change: SchemaChange, target: LoadTarget): boolean =>
43
+ change.connectionId === target.connectionId && (change.schema === undefined || target.schema === undefined || change.schema === target.schema);
44
+
45
+ /* The schemas, tables and column lists the explorer has asked a session for, by connection, schema and table. */
46
+ export function useExplorerLoads(connections: readonly Connection[]): ExplorerLoads {
47
+ const client = useDatabaseClient();
48
+ const [loads, setLoads] = useState<Loads>(EMPTY);
49
+ // Written before the state is, so a strict-mode second effect run does not fetch twice.
50
+ const started = useRef(new Map<string, LoadTarget>());
51
+ const configs = useRef(new Map<string, Connection['config']>());
52
+
53
+ const store = (target: LoadTarget, load: Load<Loaded>): void => {
54
+ setLoads((current) => ({ ...current, entries: new Map(current.entries).set(loadKey(target), { target, load }) }));
55
+ };
56
+
57
+ const reset = (target: LoadTarget): void => {
58
+ for (const [key, began] of [...started.current]) {
59
+ if (within(target, began)) {
60
+ started.current.delete(key);
61
+ }
62
+ }
63
+ setLoads((current) => ({
64
+ entries: new Map([...current.entries].filter(([, entry]) => !within(target, entry.target))),
65
+ versions: target.schema === undefined ? new Map([...current.versions].filter(([id]) => id !== target.connectionId)) : current.versions
66
+ }));
67
+ };
68
+
69
+ /* A silent load keeps showing what it replaces, so a reload does not blink the tree. */
70
+ const run = (target: LoadTarget, connection: Connection, silent: boolean): void => {
71
+ if (!silent) {
72
+ store(target, { status: 'loading' });
73
+ }
74
+ const session = client.session(connection);
75
+ const request: Promise<Loaded> =
76
+ target.schema === undefined
77
+ ? session.schemas()
78
+ : target.table === undefined
79
+ ? session.tables(target.schema)
80
+ : session.structure(target.schema, target.table);
81
+ request.then(
82
+ (value) => {
83
+ store(target, { status: 'ready', value });
84
+ if (target.schema === undefined) {
85
+ // Chained after the schemas so the version never opens a session by itself.
86
+ session.server().then(
87
+ (info) => setLoads((current) => ({ ...current, versions: new Map(current.versions).set(connection.id, info.version) })),
88
+ () => undefined
89
+ );
90
+ }
91
+ },
92
+ (e: unknown) => store(target, { status: 'error', message: messageOf(e) })
93
+ );
94
+ };
95
+
96
+ const start = (target: LoadTarget): void => {
97
+ const key = loadKey(target);
98
+ if (started.current.has(key)) {
99
+ return;
100
+ }
101
+ const connection = connections.find((entry) => entry.id === target.connectionId);
102
+ if (connection === undefined) {
103
+ return;
104
+ }
105
+ started.current.set(key, target);
106
+ run(target, connection, false);
107
+ };
108
+
109
+ /* Lists load again in place and column lists are dropped, since the table they describe may be gone; an open table asks again by itself. */
110
+ const reload = (change: SchemaChange): void => {
111
+ const connection = connections.find((entry) => entry.id === change.connectionId);
112
+ const dropped: LoadTarget[] = [];
113
+ for (const [key, began] of [...started.current]) {
114
+ if (!touches(change, began)) {
115
+ continue;
116
+ }
117
+ if (began.table !== undefined) {
118
+ started.current.delete(key);
119
+ dropped.push(began);
120
+ } else if (connection !== undefined) {
121
+ run(began, connection, true);
122
+ }
123
+ }
124
+ if (dropped.length > 0) {
125
+ const gone = new Set(dropped.map(loadKey));
126
+ setLoads((current) => ({ ...current, entries: new Map([...current.entries].filter(([key]) => !gone.has(key))) }));
127
+ }
128
+ };
129
+
130
+ useEffect(() => client.onSchemaChange(reload));
131
+
132
+ // A connection that was edited or removed is a different session, so what it listed is stale.
133
+ useEffect(() => {
134
+ const known = configs.current;
135
+ for (const [id, config] of known) {
136
+ const now = connections.find((entry) => entry.id === id);
137
+ if (now === undefined || now.config !== config) {
138
+ known.delete(id);
139
+ reset({ connectionId: id });
140
+ }
141
+ }
142
+ for (const connection of connections) {
143
+ known.set(connection.id, connection.config);
144
+ }
145
+ });
146
+
147
+ const loadOf = <T extends Loaded>(target: LoadTarget): Load<T> => (loads.entries.get(loadKey(target))?.load as Load<T> | undefined) ?? IDLE;
148
+
149
+ return {
150
+ schemas: (connectionId) => loadOf<readonly SchemaInfo[]>({ connectionId }),
151
+ tables: (connectionId, schema) => loadOf<readonly TableInfo[]>({ connectionId, schema }),
152
+ structures: (connectionId, schema, table) => loadOf<TableStructure>({ connectionId, schema, table }),
153
+ versions: (connectionId) => loads.versions.get(connectionId) ?? null,
154
+ start,
155
+ reset
156
+ };
157
+ }