sqlite-hub 0.5.0 → 0.6.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 (36) hide show
  1. package/README.md +2 -2
  2. package/changelog.md +8 -0
  3. package/frontend/assets/mockups/connections.png +0 -0
  4. package/frontend/assets/mockups/data.png +0 -0
  5. package/frontend/assets/mockups/data_row_editor.png +0 -0
  6. package/frontend/assets/mockups/home.png +0 -0
  7. package/frontend/assets/mockups/sql_editor.png +0 -0
  8. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  9. package/frontend/assets/mockups/structure.png +0 -0
  10. package/frontend/assets/mockups/structure_inspector.png +0 -0
  11. package/frontend/js/api.js +15 -0
  12. package/frontend/js/app.js +279 -8
  13. package/frontend/js/components/queryEditor.js +2 -2
  14. package/frontend/js/components/sidebar.js +1 -0
  15. package/frontend/js/components/tableDesignerEditor.js +356 -0
  16. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  17. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  18. package/frontend/js/router.js +10 -0
  19. package/frontend/js/store.js +296 -1
  20. package/frontend/js/utils/tableDesigner.js +1192 -0
  21. package/frontend/js/views/data.js +253 -263
  22. package/frontend/js/views/tableDesigner.js +37 -0
  23. package/frontend/styles/base.css +87 -73
  24. package/frontend/styles/components.css +798 -251
  25. package/frontend/styles/views.css +40 -0
  26. package/package.json +1 -1
  27. package/server/routes/tableDesigner.js +60 -0
  28. package/server/server.js +4 -0
  29. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  30. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  31. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  32. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  33. package/server/services/sqlite/tableDesignerService.js +181 -0
  34. package/frontend/assets/mockups/data_edit.png +0 -0
  35. package/frontend/assets/mockups/graph_visualize.png +0 -0
  36. package/frontend/assets/mockups/overview.png +0 -0
package/README.md CHANGED
@@ -24,7 +24,7 @@ SQLite Hub keeps that workflow sharp:
24
24
 
25
25
  ### Structure view
26
26
 
27
- ![](./frontend/assets/mockups/graph_visualize.png)
27
+ ![](./frontend/assets/mockups/structure.png)
28
28
 
29
29
  Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
30
30
 
@@ -36,7 +36,7 @@ Scan rows, sort fast, move through local data quickly, and export full tables as
36
36
 
37
37
  ### Row editing
38
38
 
39
- ![](./frontend/assets/mockups/data_edit.png)
39
+ ![](./frontend/assets/mockups/data_row_editor.png)
40
40
 
41
41
  Open one record, edit it in place, commit, continue.
42
42
 
package/changelog.md CHANGED
@@ -1,3 +1,11 @@
1
+ # v0.6.0
2
+
3
+ - table designer, create tables in sqlit_hub
4
+
5
+ # v0.5.1
6
+
7
+ - bug fix in sql editor
8
+
1
9
  # v0.5.0
2
10
 
3
11
  - sql query history
Binary file
Binary file
@@ -234,6 +234,21 @@ export function getStructureDetail(tableName) {
234
234
  return request(`/api/structure/${encodeURIComponent(tableName)}`);
235
235
  }
236
236
 
237
+ export function getTableDesignerOverview() {
238
+ return request("/api/table-designer");
239
+ }
240
+
241
+ export function getTableDesignerTable(tableName) {
242
+ return request(`/api/table-designer/${encodeURIComponent(tableName)}`);
243
+ }
244
+
245
+ export function saveTableDesignerDraft(payload) {
246
+ return request("/api/table-designer/save", {
247
+ method: "POST",
248
+ body: payload,
249
+ });
250
+ }
251
+
237
252
  export function getDataTables() {
238
253
  return request("/api/data");
239
254
  }
@@ -40,6 +40,7 @@ import {
40
40
  selectConnection,
41
41
  selectQueryHistoryItem,
42
42
  selectStructureEntry,
43
+ setTableDesignerSearchQuery,
43
44
  setDataPage,
44
45
  setDataPageSize,
45
46
  setDataSearchColumn,
@@ -53,6 +54,9 @@ import {
53
54
  setRoute,
54
55
  saveQueryHistoryNotes,
55
56
  saveQueryHistoryTitle,
57
+ saveCurrentTableDesignerDraft,
58
+ queueTableDesignerCsvImport,
59
+ showToast,
56
60
  submitCreateConnection,
57
61
  submitDeleteRowConfirmation,
58
62
  submitDataRowUpdate,
@@ -62,6 +66,10 @@ import {
62
66
  submitOpenConnection,
63
67
  subscribe,
64
68
  toggleQueryHistorySavedState,
69
+ updateCurrentTableDesignerColumnField,
70
+ updateCurrentTableDesignerField,
71
+ addCurrentTableDesignerColumn,
72
+ removeCurrentTableDesignerColumn,
65
73
  } from "./store.js";
66
74
  import { renderConnectionsView } from "./views/connections.js";
67
75
  import { renderDataView } from "./views/data.js";
@@ -70,6 +78,7 @@ import { renderLandingView } from "./views/landing.js";
70
78
  import { renderOverviewView } from "./views/overview.js";
71
79
  import { renderSettingsView } from "./views/settings.js";
72
80
  import { renderStructureView } from "./views/structure.js";
81
+ import { renderTableDesignerView } from "./views/tableDesigner.js";
73
82
  import { highlightSql } from "./utils/format.js";
74
83
 
75
84
  const appRoot = document.querySelector("#app");
@@ -86,6 +95,8 @@ const shellRefs = {
86
95
  modal: document.querySelector("#modal-root"),
87
96
  toast: document.querySelector("#toast-root"),
88
97
  };
98
+ let lastRenderedRoutePath = null;
99
+ let pendingNewTableDesignerAutofocus = false;
89
100
 
90
101
  function resetStructureGraphForDatabaseChange() {
91
102
  resetPersistedStructureGraphState();
@@ -146,6 +157,21 @@ function readFileAsBase64(file) {
146
157
  });
147
158
  }
148
159
 
160
+ function readFileAsText(file) {
161
+ return new Promise((resolve, reject) => {
162
+ const reader = new FileReader();
163
+
164
+ reader.onload = () => {
165
+ resolve(String(reader.result ?? ""));
166
+ };
167
+ reader.onerror = () => {
168
+ reject(reader.error ?? new Error("The selected CSV file could not be read."));
169
+ };
170
+
171
+ reader.readAsText(file);
172
+ });
173
+ }
174
+
149
175
  async function buildConnectionLogoUpload(file) {
150
176
  if (!(file instanceof File) || !file.size) {
151
177
  return null;
@@ -200,6 +226,8 @@ function resolveView(state) {
200
226
  return renderEditorView(state, { isResultsRoute: true });
201
227
  case "structure":
202
228
  return renderStructureView(state);
229
+ case "tableDesigner":
230
+ return renderTableDesignerView(state);
203
231
  case "settings":
204
232
  return renderSettingsView(state);
205
233
  default:
@@ -212,7 +240,11 @@ function captureFocusedInputState() {
212
240
 
213
241
  if (
214
242
  !activeElement ||
215
- !(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)
243
+ !(
244
+ activeElement instanceof HTMLInputElement ||
245
+ activeElement instanceof HTMLTextAreaElement ||
246
+ activeElement instanceof HTMLSelectElement
247
+ )
216
248
  ) {
217
249
  return null;
218
250
  }
@@ -224,6 +256,8 @@ function captureFocusedInputState() {
224
256
 
225
257
  return {
226
258
  bind,
259
+ field: activeElement.dataset.field ?? null,
260
+ columnId: activeElement.dataset.columnId ?? null,
227
261
  selectionStart: activeElement.selectionStart,
228
262
  selectionEnd: activeElement.selectionEnd,
229
263
  selectionDirection: activeElement.selectionDirection,
@@ -232,24 +266,61 @@ function captureFocusedInputState() {
232
266
  };
233
267
  }
234
268
 
269
+ function buildFocusedInputSelectors(snapshot) {
270
+ if (!snapshot?.bind) {
271
+ return [];
272
+ }
273
+
274
+ const selectors = [];
275
+ const bindSelector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
276
+
277
+ if (snapshot.columnId && snapshot.field) {
278
+ selectors.push(
279
+ `${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"][data-field="${CSS.escape(snapshot.field)}"]`
280
+ );
281
+ }
282
+
283
+ if (snapshot.field) {
284
+ selectors.push(`${bindSelector}[data-field="${CSS.escape(snapshot.field)}"]`);
285
+ }
286
+
287
+ if (snapshot.columnId) {
288
+ selectors.push(`${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"]`);
289
+ }
290
+
291
+ selectors.push(bindSelector);
292
+ return selectors;
293
+ }
294
+
235
295
  function restoreFocusedInputState(snapshot) {
236
296
  if (!snapshot) {
237
- return;
297
+ return false;
238
298
  }
239
299
 
240
- const selector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
241
- const nextElement = document.querySelector(selector);
300
+ const nextElement = buildFocusedInputSelectors(snapshot)
301
+ .map((selector) => document.querySelector(selector))
302
+ .find(
303
+ (candidate) =>
304
+ candidate instanceof HTMLInputElement ||
305
+ candidate instanceof HTMLTextAreaElement ||
306
+ candidate instanceof HTMLSelectElement
307
+ );
242
308
 
243
309
  if (
244
310
  !nextElement ||
245
- !(nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement)
311
+ !(
312
+ nextElement instanceof HTMLInputElement ||
313
+ nextElement instanceof HTMLTextAreaElement ||
314
+ nextElement instanceof HTMLSelectElement
315
+ )
246
316
  ) {
247
- return;
317
+ return false;
248
318
  }
249
319
 
250
320
  nextElement.focus({ preventScroll: true });
251
321
 
252
322
  if (
323
+ (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) &&
253
324
  typeof snapshot.selectionStart === "number" &&
254
325
  typeof snapshot.selectionEnd === "number"
255
326
  ) {
@@ -262,14 +333,97 @@ function restoreFocusedInputState(snapshot) {
262
333
 
263
334
  nextElement.scrollTop = snapshot.scrollTop;
264
335
  nextElement.scrollLeft = snapshot.scrollLeft;
336
+ return true;
337
+ }
338
+
339
+ function focusNewTableDesignerNameField() {
340
+ const input = document.querySelector(
341
+ '[data-bind="table-designer-field"][data-field="tableName"]'
342
+ );
343
+
344
+ if (!(input instanceof HTMLInputElement)) {
345
+ return false;
346
+ }
347
+
348
+ input.focus({ preventScroll: true });
349
+ input.setSelectionRange(input.value.length, input.value.length);
350
+ return true;
351
+ }
352
+
353
+ function focusTableDesignerColumnNameField(columnId) {
354
+ if (!columnId) {
355
+ return false;
356
+ }
357
+
358
+ const input = document.querySelector(
359
+ `[data-bind="table-designer-column-field"][data-column-id="${CSS.escape(columnId)}"][data-field="name"]`
360
+ );
361
+
362
+ if (!(input instanceof HTMLInputElement)) {
363
+ return false;
364
+ }
365
+
366
+ input.focus({ preventScroll: true });
367
+ input.setSelectionRange(input.value.length, input.value.length);
368
+ return true;
369
+ }
370
+
371
+ async function handleTableDesignerCsvImport(fileInput) {
372
+ if (!(fileInput instanceof HTMLInputElement)) {
373
+ return;
374
+ }
375
+
376
+ const file = fileInput.files?.[0];
377
+
378
+ if (!(file instanceof File)) {
379
+ return;
380
+ }
381
+
382
+ if (!file.size) {
383
+ showToast("The selected CSV file is empty.", "alert");
384
+ fileInput.value = "";
385
+ return;
386
+ }
387
+
388
+ try {
389
+ const csvText = await readFileAsText(file);
390
+ const imported = queueTableDesignerCsvImport(file.name, csvText);
391
+
392
+ if (!imported) {
393
+ return;
394
+ }
395
+
396
+ router.navigate("/table-designer/new");
397
+ showToast(
398
+ `Imported ${imported.columnCount} columns and ${imported.importedRowCount} row${
399
+ imported.importedRowCount === 1 ? "" : "s"
400
+ } from ${file.name}.`,
401
+ "success"
402
+ );
403
+ } catch (error) {
404
+ showToast(error?.message || "CSV import failed.", "alert");
405
+ } finally {
406
+ fileInput.value = "";
407
+ }
265
408
  }
266
409
 
267
410
  function renderApp(state) {
268
411
  const focusedInput = captureFocusedInputState();
412
+ const previousRoutePath = lastRenderedRoutePath;
269
413
  const { main, panel } = resolveView(state);
270
- const isLockedRoute = ["editor", "editorResults", "data", "structure"].includes(
414
+ const isLockedRoute = ["editor", "editorResults", "data", "structure", "tableDesigner"].includes(
271
415
  state.route.name
272
416
  );
417
+ const isEnteringNewTableDesignerRoute =
418
+ state.route.name === "tableDesigner" &&
419
+ state.route.params?.isNew &&
420
+ previousRoutePath !== state.route.path;
421
+
422
+ if (isEnteringNewTableDesignerRoute) {
423
+ pendingNewTableDesignerAutofocus = true;
424
+ } else if (state.route.name !== "tableDesigner" || !state.route.params?.isNew) {
425
+ pendingNewTableDesignerAutofocus = false;
426
+ }
273
427
 
274
428
  teardownStructureGraph();
275
429
  shellRefs.topNav.innerHTML = renderTopNav(state);
@@ -281,7 +435,21 @@ function renderApp(state) {
281
435
  shellRefs.modal.innerHTML = renderModal(state);
282
436
  shellRefs.toast.innerHTML = renderToasts(state.toasts);
283
437
  shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
284
- restoreFocusedInputState(focusedInput);
438
+
439
+ if (
440
+ pendingNewTableDesignerAutofocus &&
441
+ state.route.name === "tableDesigner" &&
442
+ state.route.params?.isNew &&
443
+ state.tableDesigner.draft?.mode === "create"
444
+ ) {
445
+ if (focusNewTableDesignerNameField()) {
446
+ pendingNewTableDesignerAutofocus = false;
447
+ }
448
+ } else {
449
+ restoreFocusedInputState(focusedInput);
450
+ }
451
+
452
+ lastRenderedRoutePath = state.route.path;
285
453
 
286
454
  if (state.route.name === "structure") {
287
455
  mountStructureGraph(state).catch((error) => {
@@ -436,6 +604,53 @@ async function handleAction(actionNode) {
436
604
  await selectStructureEntry(actionNode.dataset.entryName);
437
605
  }
438
606
  return;
607
+ case "add-table-designer-column":
608
+ {
609
+ const columnId = addCurrentTableDesignerColumn();
610
+ if (columnId) {
611
+ window.requestAnimationFrame(() => {
612
+ focusTableDesignerColumnNameField(columnId);
613
+ });
614
+ }
615
+ }
616
+ return;
617
+ case "remove-table-designer-column":
618
+ if (actionNode.dataset.columnId) {
619
+ removeCurrentTableDesignerColumn(actionNode.dataset.columnId);
620
+ }
621
+ return;
622
+ case "save-table-designer": {
623
+ const savedTableName = await saveCurrentTableDesignerDraft();
624
+ if (savedTableName) {
625
+ router.navigate(`/table-designer/${encodeURIComponent(savedTableName)}`);
626
+ }
627
+ return;
628
+ }
629
+ case "copy-table-designer-sql": {
630
+ const sqlPreview = getState().tableDesigner.draft?.sqlPreview ?? "";
631
+ if (!sqlPreview.trim()) {
632
+ return;
633
+ }
634
+
635
+ try {
636
+ await navigator.clipboard.writeText(sqlPreview);
637
+ showToast("SQL preview copied.", "success");
638
+ } catch (error) {
639
+ showToast("Clipboard access failed.", "alert");
640
+ }
641
+ return;
642
+ }
643
+ case "import-table-designer-csv": {
644
+ const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
645
+
646
+ if (!(fileInput instanceof HTMLInputElement)) {
647
+ return;
648
+ }
649
+
650
+ fileInput.value = "";
651
+ fileInput.click();
652
+ return;
653
+ }
439
654
  case "select-data-row":
440
655
  if (actionNode.dataset.rowIndex) {
441
656
  selectDataRow(actionNode.dataset.rowIndex);
@@ -554,6 +769,29 @@ document.addEventListener("input", (event) => {
554
769
  return;
555
770
  }
556
771
 
772
+ if (bindNode.dataset.bind === "table-designer-search") {
773
+ setTableDesignerSearchQuery(bindNode.value);
774
+ return;
775
+ }
776
+
777
+ if (bindNode.dataset.bind === "table-designer-field") {
778
+ if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
779
+ return;
780
+ }
781
+
782
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
783
+ return;
784
+ }
785
+
786
+ if (bindNode.dataset.bind === "table-designer-column-field") {
787
+ updateCurrentTableDesignerColumnField(
788
+ bindNode.dataset.columnId,
789
+ bindNode.dataset.field,
790
+ bindNode.value
791
+ );
792
+ return;
793
+ }
794
+
557
795
  if (bindNode.dataset.bind === "query-history-search") {
558
796
  setQueryHistorySearchInput(bindNode.value);
559
797
  }
@@ -582,6 +820,39 @@ document.addEventListener("change", (event) => {
582
820
 
583
821
  if (bindNode.dataset.bind === "data-search-column") {
584
822
  setDataSearchColumn(bindNode.value);
823
+ return;
824
+ }
825
+
826
+ if (bindNode.dataset.bind === "table-designer-import-file") {
827
+ void handleTableDesignerCsvImport(bindNode);
828
+ return;
829
+ }
830
+
831
+ if (bindNode.dataset.bind === "table-designer-field") {
832
+ if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
833
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
834
+ return;
835
+ }
836
+
837
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
838
+ return;
839
+ }
840
+
841
+ if (bindNode.dataset.bind === "table-designer-column-field") {
842
+ updateCurrentTableDesignerColumnField(
843
+ bindNode.dataset.columnId,
844
+ bindNode.dataset.field,
845
+ bindNode.value
846
+ );
847
+ return;
848
+ }
849
+
850
+ if (bindNode.dataset.bind === "table-designer-column-flag") {
851
+ updateCurrentTableDesignerColumnField(
852
+ bindNode.dataset.columnId,
853
+ bindNode.dataset.field,
854
+ bindNode.checked
855
+ );
585
856
  }
586
857
  });
587
858
 
@@ -28,11 +28,11 @@ function renderEditorSurface({ query }) {
28
28
  <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
29
29
  </div>
30
30
  <div class="query-editor-layer relative z-10 h-full min-h-[140px]">
31
- <pre
31
+ <div
32
32
  aria-hidden="true"
33
33
  class="query-editor-highlight"
34
34
  data-query-editor-highlight
35
- >${renderHighlightedQuery(query)}</pre>
35
+ >${renderHighlightedQuery(query)}</div>
36
36
  <textarea
37
37
  class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
38
38
  data-bind="current-query"
@@ -7,6 +7,7 @@ const sidebarItems = [
7
7
  { label: "Data", href: "#/data", key: "data", icon: "table_rows" },
8
8
  { label: "SQL Editor", href: "#/editor", key: "editor", icon: "terminal" },
9
9
  { label: "Structure", href: "#/structure", key: "structure", icon: "account_tree" },
10
+ { label: "Table Designer", href: "#/table-designer", key: "tableDesigner", icon: "table_chart" },
10
11
  { label: "Settings", href: "#/settings", key: "settings", icon: "settings" },
11
12
  ];
12
13