sqlite-hub 1.2.0 → 1.4.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 (152) hide show
  1. package/README.md +106 -73
  2. package/bin/sqlite-hub.js +176 -2
  3. package/docs/API.md +122 -0
  4. package/docs/CLI.md +227 -0
  5. package/docs/DESIGN_GUIDELINES.md +45 -0
  6. package/docs/changelog.md +141 -0
  7. package/docs/shortkeys.md +27 -0
  8. package/docs/todo.md +16 -0
  9. package/examples/api/generate-types.js +52 -0
  10. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  11. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  12. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  22. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  34. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  40. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  42. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  43. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  44. package/frontend/js/api.js +18 -0
  45. package/frontend/js/app.js +118 -8
  46. package/frontend/js/components/connectionCard.js +1 -1
  47. package/frontend/js/components/emptyState.js +4 -4
  48. package/frontend/js/components/generateTypesDropdown.js +33 -0
  49. package/frontend/js/components/metricCard.js +1 -1
  50. package/frontend/js/components/modal.js +211 -18
  51. package/frontend/js/components/pageHeader.js +1 -1
  52. package/frontend/js/components/queryHistoryDetail.js +3 -3
  53. package/frontend/js/components/queryHistoryHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryPanel.js +1 -1
  55. package/frontend/js/components/rowEditorPanel.js +13 -3
  56. package/frontend/js/router.js +28 -6
  57. package/frontend/js/store.js +422 -8
  58. package/frontend/js/views/backups.js +529 -37
  59. package/frontend/js/views/charts.js +9 -9
  60. package/frontend/js/views/connections.js +2 -2
  61. package/frontend/js/views/data.js +1 -1
  62. package/frontend/js/views/documents.js +3 -3
  63. package/frontend/js/views/editor.js +6 -6
  64. package/frontend/js/views/mediaTagging.js +7 -5
  65. package/frontend/js/views/overview.js +3 -3
  66. package/frontend/js/views/settings.js +3 -3
  67. package/frontend/js/views/structure.js +6 -1
  68. package/frontend/styles/base.css +1 -40
  69. package/frontend/styles/components.css +109 -232
  70. package/frontend/styles/structure-graph.css +32 -68
  71. package/frontend/styles/tailwind.css +8 -4
  72. package/frontend/styles/tailwind.generated.css +1 -1
  73. package/frontend/styles/tokens.css +28 -21
  74. package/frontend/styles/views.css +94 -261
  75. package/package.json +18 -2
  76. package/server/routes/backups.js +18 -2
  77. package/server/routes/externalApi.js +36 -0
  78. package/server/routes/structure.js +22 -0
  79. package/server/services/databaseCommandService.js +14 -0
  80. package/server/services/sqlite/backupDiff.js +914 -0
  81. package/server/services/sqlite/backupService.js +77 -1
  82. package/server/services/sqlite/structureService.js +7 -0
  83. package/server/services/typeGenerationService.js +663 -0
  84. package/tailwind.config.cjs +0 -1
  85. package/.github/funding.yml +0 -2
  86. package/.github/workflows/ci.yml +0 -36
  87. package/database.sqlite +0 -0
  88. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  89. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  90. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  91. package/frontend/assets/mockups/connections_1200.webp +0 -0
  92. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  93. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  94. package/frontend/assets/mockups/documents_1200.webp +0 -0
  95. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  96. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  97. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  98. package/frontend/assets/mockups/overview_1200.webp +0 -0
  99. package/frontend/assets/mockups/settings_1200.webp +0 -0
  100. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  101. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  102. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  103. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  104. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  105. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  106. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  107. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  108. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  109. package/tests/api-token-auth.test.js +0 -279
  110. package/tests/backup-manager.test.js +0 -140
  111. package/tests/backups-view.test.js +0 -70
  112. package/tests/charts-height-preset-storage.test.js +0 -60
  113. package/tests/charts-route-state.test.js +0 -144
  114. package/tests/check-constraint-options.test.js +0 -90
  115. package/tests/cli-args.test.js +0 -113
  116. package/tests/cli-service-delegation.test.js +0 -140
  117. package/tests/connection-removal.test.js +0 -52
  118. package/tests/connections-file-dialog-route.test.js +0 -89
  119. package/tests/copy-column-modal.test.js +0 -131
  120. package/tests/database-command-service.test.js +0 -174
  121. package/tests/database-documents.test.js +0 -85
  122. package/tests/documents-view.test.js +0 -132
  123. package/tests/dropdown-button.test.js +0 -101
  124. package/tests/email-preview.test.js +0 -89
  125. package/tests/export-blob.test.js +0 -99
  126. package/tests/export-filenames.test.js +0 -38
  127. package/tests/file-path-preview.test.js +0 -165
  128. package/tests/form-controls.test.js +0 -34
  129. package/tests/json-preview.test.js +0 -49
  130. package/tests/local-request-security.test.js +0 -85
  131. package/tests/markdown-documents.test.js +0 -103
  132. package/tests/native-file-dialog.test.js +0 -105
  133. package/tests/query-editor.test.js +0 -28
  134. package/tests/query-history-detail.test.js +0 -37
  135. package/tests/query-history-header.test.js +0 -30
  136. package/tests/query-results-truncation.test.js +0 -20
  137. package/tests/risky-sql.test.js +0 -30
  138. package/tests/row-editor-json.test.js +0 -82
  139. package/tests/row-editor-null-values.test.js +0 -184
  140. package/tests/row-editor-timestamp-preview.test.js +0 -192
  141. package/tests/security-paths.test.js +0 -84
  142. package/tests/settings-api-tokens-route.test.js +0 -97
  143. package/tests/settings-metadata.test.js +0 -114
  144. package/tests/settings-view.test.js +0 -108
  145. package/tests/sql-formatter.test.js +0 -173
  146. package/tests/sql-highlight.test.js +0 -38
  147. package/tests/sql-identifier-safety.test.js +0 -171
  148. package/tests/sql-result-limit.test.js +0 -66
  149. package/tests/structure-view.test.js +0 -60
  150. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  151. package/tests/table-scroll-state.test.js +0 -70
  152. package/tests/text-cell-stats.test.js +0 -38
@@ -60,6 +60,10 @@ const UI_PREFERENCE_STORAGE_KEYS = {
60
60
  const QUERY_HISTORY_PAGE_SIZE = 30;
61
61
  const QUERY_HISTORY_RUN_LIMIT = 8;
62
62
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
63
+ const TYPE_GENERATION_TARGETS = new Set(['typescript', 'rust', 'kotlin', 'swift']);
64
+ const TYPE_GENERATION_NAMING = new Set(['preserve', 'camel', 'pascal', 'snake']);
65
+ const TYPE_GENERATION_NULLABLE = new Set(['native', 'optional']);
66
+ const TYPE_GENERATION_JSON_TYPES = new Set(['unknown', 'record', 'json-value']);
63
67
  const EDITOR_RESULT_TABS = new Set(['results', 'performance', 'messages']);
64
68
  const COPY_COLUMN_MODES = new Set(['column', 'column-with-header', 'first-10', 'markdown-todo']);
65
69
  const TEXT_EXPORT_FORMAT_LABELS = {
@@ -271,6 +275,18 @@ const state = {
271
275
  loading: false,
272
276
  operationLoading: false,
273
277
  error: null,
278
+ diff: {
279
+ visible: false,
280
+ backupId: null,
281
+ backupName: '',
282
+ backupCreatedAt: null,
283
+ currentLabel: '',
284
+ activeTab: 'schema',
285
+ requestId: null,
286
+ loading: false,
287
+ error: null,
288
+ data: null,
289
+ },
274
290
  },
275
291
  settings: {
276
292
  data: { ...DEFAULT_SETTINGS },
@@ -551,6 +567,21 @@ function getMediaTaggingRouteErrorKey(error = {}) {
551
567
  return `route:${String(error.code ?? '').trim()}:${String(error.message ?? '').trim()}`;
552
568
  }
553
569
 
570
+ function createEmptyBackupDiffState() {
571
+ return {
572
+ visible: false,
573
+ backupId: null,
574
+ backupName: '',
575
+ backupCreatedAt: null,
576
+ currentLabel: '',
577
+ activeTab: 'schema',
578
+ requestId: null,
579
+ loading: false,
580
+ error: null,
581
+ data: null,
582
+ };
583
+ }
584
+
554
585
  function syncDismissedMediaTaggingIssues() {
555
586
  const availableKeys = new Set();
556
587
 
@@ -864,6 +895,91 @@ function findDataBrowserRowIndexByIdentity(rows = [], identity) {
864
895
  return rows.findIndex(row => areRowIdentitiesEqual(row?.__identity, identity));
865
896
  }
866
897
 
898
+ function coerceDataRouteIdentityValue(column, value) {
899
+ const text = String(value ?? '');
900
+ const affinity = String(column?.affinity ?? '').toUpperCase();
901
+
902
+ if (['INTEGER', 'REAL', 'NUMERIC'].includes(affinity) && text.trim() !== '') {
903
+ const numberValue = Number(text);
904
+
905
+ if (Number.isFinite(numberValue)) {
906
+ return numberValue;
907
+ }
908
+ }
909
+
910
+ return value;
911
+ }
912
+
913
+ function parseCompositeDataRoutePrimaryKey(rawValue) {
914
+ try {
915
+ const parsed = JSON.parse(rawValue);
916
+
917
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
918
+ return parsed;
919
+ }
920
+ } catch {
921
+ return null;
922
+ }
923
+
924
+ return null;
925
+ }
926
+
927
+ function buildDataRouteRowIdentity(table, rawPrimaryKey) {
928
+ const primaryKeyValue = String(rawPrimaryKey ?? '').trim();
929
+ const identityStrategy = table?.identityStrategy ?? null;
930
+
931
+ if (!primaryKeyValue || !identityStrategy) {
932
+ return null;
933
+ }
934
+
935
+ if (identityStrategy.type === 'rowid') {
936
+ const numberValue = Number(primaryKeyValue);
937
+
938
+ return {
939
+ kind: 'rowid',
940
+ values: {
941
+ rowid: Number.isInteger(numberValue) ? numberValue : primaryKeyValue,
942
+ },
943
+ };
944
+ }
945
+
946
+ if (identityStrategy.type !== 'primaryKey') {
947
+ return null;
948
+ }
949
+
950
+ const columns = identityStrategy.columns ?? [];
951
+
952
+ if (columns.length === 1) {
953
+ const columnName = columns[0];
954
+ const column = table?.columnMeta?.find(candidate => candidate.name === columnName);
955
+
956
+ return {
957
+ kind: 'primaryKey',
958
+ columns,
959
+ values: {
960
+ [columnName]: coerceDataRouteIdentityValue(column, primaryKeyValue),
961
+ },
962
+ };
963
+ }
964
+
965
+ const parsedValue = parseCompositeDataRoutePrimaryKey(primaryKeyValue);
966
+
967
+ if (!parsedValue || columns.some(columnName => !Object.prototype.hasOwnProperty.call(parsedValue, columnName))) {
968
+ return null;
969
+ }
970
+
971
+ return {
972
+ kind: 'primaryKey',
973
+ columns,
974
+ values: Object.fromEntries(
975
+ columns.map(columnName => {
976
+ const column = table?.columnMeta?.find(candidate => candidate.name === columnName);
977
+ return [columnName, coerceDataRouteIdentityValue(column, parsedValue[columnName])];
978
+ }),
979
+ ),
980
+ };
981
+ }
982
+
867
983
  function clearDataBrowserRowSelectionState() {
868
984
  state.dataBrowser.selectedRowIndex = null;
869
985
  state.dataBrowser.selectedRow = null;
@@ -1324,6 +1440,10 @@ function syncRouteContext() {
1324
1440
  state.documents.saveError = null;
1325
1441
  }
1326
1442
 
1443
+ if (route.name !== 'backups') {
1444
+ state.backups.diff = createEmptyBackupDiffState();
1445
+ }
1446
+
1327
1447
  if (route.name !== 'mediaTaggingSetup' && route.name !== 'mediaTaggingQueue') {
1328
1448
  state.mediaTagging.selectedTagKeys = [];
1329
1449
  }
@@ -1379,6 +1499,7 @@ async function refreshBackupsState() {
1379
1499
  if (!state.connections.active) {
1380
1500
  state.backups.items = [];
1381
1501
  state.backups.error = null;
1502
+ state.backups.diff = createEmptyBackupDiffState();
1382
1503
  emitChange();
1383
1504
  return;
1384
1505
  }
@@ -1956,8 +2077,9 @@ async function resolvePendingDataBrowserRow(version) {
1956
2077
  }
1957
2078
  }
1958
2079
 
1959
- async function loadDataTable(version) {
2080
+ async function loadDataTable(version, options = {}) {
1960
2081
  const tableName = state.dataBrowser.selectedTable;
2082
+ const routeRowPrimaryKey = options.rowPrimaryKey ?? null;
1961
2083
  const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, DEFAULT_DATA_PAGE_SIZE);
1962
2084
  const page = Math.max(1, Number(state.dataBrowser.page) || 1);
1963
2085
  const sortColumn = state.dataBrowser.sortColumn;
@@ -2011,6 +2133,15 @@ async function loadDataTable(version) {
2011
2133
  state.dataBrowser.searchColumn,
2012
2134
  );
2013
2135
  clearDataBrowserRowSelectionState();
2136
+ const routeRowIdentity = buildDataRouteRowIdentity(state.dataBrowser.table, routeRowPrimaryKey);
2137
+ if (routeRowPrimaryKey !== null && routeRowPrimaryKey !== undefined) {
2138
+ state.dataBrowser.pendingOpenRow = routeRowIdentity
2139
+ ? {
2140
+ tableName,
2141
+ identity: routeRowIdentity,
2142
+ }
2143
+ : null;
2144
+ }
2014
2145
  await resolvePendingDataBrowserRow(version);
2015
2146
  } catch (error) {
2016
2147
  if (version !== routeLoadVersion) {
@@ -2071,7 +2202,9 @@ async function loadData(version, route) {
2071
2202
  return;
2072
2203
  }
2073
2204
 
2074
- await loadDataTable(version);
2205
+ await loadDataTable(version, {
2206
+ rowPrimaryKey: route.params?.rowPrimaryKey ?? null,
2207
+ });
2075
2208
  } catch (error) {
2076
2209
  if (version !== routeLoadVersion) {
2077
2210
  return;
@@ -2435,6 +2568,7 @@ function invalidateDatabaseCaches(options = {}) {
2435
2568
  }
2436
2569
 
2437
2570
  state.overview.data = null;
2571
+ state.backups.diff = createEmptyBackupDiffState();
2438
2572
  state.dataBrowser.tables = [];
2439
2573
  if (!preserveDataBrowserState) {
2440
2574
  state.dataBrowser.selectedTable = null;
@@ -3976,7 +4110,7 @@ export async function submitCreateBackupConfirmation({ name = '', notes = '', ty
3976
4110
  }
3977
4111
  }
3978
4112
 
3979
- export function openEditBackupNotesModal(backupId) {
4113
+ export function openEditBackupModal(backupId) {
3980
4114
  const backup = state.backups.items.find(item => String(item.id) === String(backupId));
3981
4115
 
3982
4116
  if (!backup) {
@@ -3985,7 +4119,7 @@ export function openEditBackupNotesModal(backupId) {
3985
4119
  }
3986
4120
 
3987
4121
  state.modal = {
3988
- kind: 'edit-backup-notes',
4122
+ kind: 'edit-backup',
3989
4123
  backupId: backup.id,
3990
4124
  backupName: backup.name,
3991
4125
  notes: backup.notes ?? '',
@@ -3995,8 +4129,8 @@ export function openEditBackupNotesModal(backupId) {
3995
4129
  emitChange();
3996
4130
  }
3997
4131
 
3998
- export async function submitEditBackupNotesConfirmation(notes = '') {
3999
- if (state.modal?.kind !== 'edit-backup-notes') {
4132
+ export async function submitEditBackupConfirmation({ name = '', notes = '' } = {}) {
4133
+ if (state.modal?.kind !== 'edit-backup') {
4000
4134
  return null;
4001
4135
  }
4002
4136
 
@@ -4005,12 +4139,12 @@ export async function submitEditBackupNotesConfirmation(notes = '') {
4005
4139
  state.backups.operationLoading = true;
4006
4140
 
4007
4141
  try {
4008
- const response = await api.updateBackup(backupId, { notes });
4142
+ const response = await api.updateBackup(backupId, { name, notes });
4009
4143
  state.backups.items = state.backups.items.map(item =>
4010
4144
  String(item.id) === String(response.data.id) ? response.data : item
4011
4145
  );
4012
4146
  closeModalInternal();
4013
- pushToast(response.message || 'Backup notes updated.', 'success');
4147
+ pushToast(response.message || 'Backup updated.', 'success');
4014
4148
  return response.data;
4015
4149
  } catch (error) {
4016
4150
  withModalError(error);
@@ -4052,6 +4186,9 @@ export async function submitDeleteBackupConfirmation() {
4052
4186
  try {
4053
4187
  const response = await api.deleteBackup(backupId);
4054
4188
  state.backups.items = state.backups.items.filter(item => String(item.id) !== String(backupId));
4189
+ if (String(state.backups.diff.backupId ?? '') === String(backupId)) {
4190
+ state.backups.diff = createEmptyBackupDiffState();
4191
+ }
4055
4192
  closeModalInternal();
4056
4193
  pushToast(response.message || 'Backup deleted.', 'muted');
4057
4194
  return true;
@@ -4082,6 +4219,75 @@ export async function downloadBackup(backupId) {
4082
4219
  }
4083
4220
  }
4084
4221
 
4222
+ export async function openBackupDiffDrawer(backupId) {
4223
+ const backup = state.backups.items.find(item => String(item.id) === String(backupId));
4224
+
4225
+ if (!backup) {
4226
+ pushToast('The selected backup could not be loaded.', 'alert');
4227
+ return null;
4228
+ }
4229
+
4230
+ const requestId = crypto.randomUUID();
4231
+ state.backups.diff = {
4232
+ visible: true,
4233
+ backupId: backup.id,
4234
+ backupName: backup.name,
4235
+ backupCreatedAt: backup.createdAt,
4236
+ currentLabel: state.connections.active?.label ?? 'Current database',
4237
+ activeTab: 'schema',
4238
+ requestId,
4239
+ loading: true,
4240
+ error: null,
4241
+ data: null,
4242
+ };
4243
+ state.backups.operationLoading = true;
4244
+ emitChange();
4245
+
4246
+ try {
4247
+ const response = await api.getBackupDiff(backup.id);
4248
+
4249
+ if (state.backups.diff.requestId === requestId) {
4250
+ state.backups.diff.data = response.data ?? null;
4251
+ state.backups.diff.backupName = response.data?.backup?.name ?? state.backups.diff.backupName;
4252
+ state.backups.diff.backupCreatedAt = response.data?.backup?.createdAt ?? state.backups.diff.backupCreatedAt;
4253
+ state.backups.diff.currentLabel = response.data?.current?.label ?? state.backups.diff.currentLabel;
4254
+ state.backups.diff.error = null;
4255
+ }
4256
+
4257
+ return response.data ?? null;
4258
+ } catch (error) {
4259
+ if (state.backups.diff.requestId === requestId) {
4260
+ state.backups.diff.error = normalizeError(error);
4261
+ } else {
4262
+ pushToast(normalizeError(error).message || 'Backup comparison failed.', 'alert');
4263
+ }
4264
+
4265
+ return null;
4266
+ } finally {
4267
+ state.backups.operationLoading = false;
4268
+
4269
+ if (state.backups.diff.requestId === requestId) {
4270
+ state.backups.diff.loading = false;
4271
+ }
4272
+
4273
+ emitChange();
4274
+ }
4275
+ }
4276
+
4277
+ export function setBackupDiffTab(tab) {
4278
+ if (!state.backups.diff.visible) {
4279
+ return;
4280
+ }
4281
+
4282
+ state.backups.diff.activeTab = tab === 'data' ? 'data' : 'schema';
4283
+ emitChange();
4284
+ }
4285
+
4286
+ export function closeBackupDiffDrawer() {
4287
+ state.backups.diff = createEmptyBackupDiffState();
4288
+ emitChange();
4289
+ }
4290
+
4085
4291
  export function openRestoreBackupModal(backupId) {
4086
4292
  const backup = state.backups.items.find(item => String(item.id) === String(backupId));
4087
4293
 
@@ -4510,6 +4716,214 @@ export async function selectStructureEntry(name) {
4510
4716
  await loadStructureDetail(++routeLoadVersion);
4511
4717
  }
4512
4718
 
4719
+ function getDefaultTypeGenerationOptions(target = 'typescript') {
4720
+ const propertyNaming = target === 'rust' ? 'snake' : 'camel';
4721
+
4722
+ return {
4723
+ typeName: '',
4724
+ propertyNaming,
4725
+ nullableMode: 'native',
4726
+ includeComments: false,
4727
+ includeDefaultsAsComments: false,
4728
+ includeGeneratedColumns: true,
4729
+ includeHiddenColumns: false,
4730
+ exportDeclaration: true,
4731
+ jsonType: 'unknown',
4732
+ };
4733
+ }
4734
+
4735
+ function normalizeTypeGenerationOptions(options = {}, target = 'typescript') {
4736
+ const defaults = getDefaultTypeGenerationOptions(target);
4737
+ const propertyNaming = TYPE_GENERATION_NAMING.has(options.propertyNaming)
4738
+ ? options.propertyNaming
4739
+ : defaults.propertyNaming;
4740
+ const nullableMode =
4741
+ TYPE_GENERATION_NULLABLE.has(options.nullableMode) && (target === 'typescript' || options.nullableMode !== 'optional')
4742
+ ? options.nullableMode
4743
+ : 'native';
4744
+ const jsonType = TYPE_GENERATION_JSON_TYPES.has(options.jsonType) ? options.jsonType : 'unknown';
4745
+
4746
+ return {
4747
+ ...defaults,
4748
+ ...options,
4749
+ propertyNaming,
4750
+ nullableMode,
4751
+ jsonType,
4752
+ typeName: String(options.typeName ?? ''),
4753
+ };
4754
+ }
4755
+
4756
+ function getStructureTableNames() {
4757
+ return (state.structure.data?.grouped?.tables ?? [])
4758
+ .filter(table => table?.type === 'table')
4759
+ .map(table => String(table.name ?? '').trim())
4760
+ .filter(Boolean);
4761
+ }
4762
+
4763
+ function getTypeGenerationRequestOptions(modal) {
4764
+ const options = { ...modal.options };
4765
+
4766
+ if (!String(options.typeName ?? '').trim()) {
4767
+ delete options.typeName;
4768
+ }
4769
+
4770
+ if (modal.target !== 'typescript') {
4771
+ delete options.jsonType;
4772
+ }
4773
+
4774
+ return options;
4775
+ }
4776
+
4777
+ function combineGeneratedTypeFiles(files = []) {
4778
+ return files
4779
+ .map(file => [`// ${file.fileName}`, file.code].filter(Boolean).join('\n'))
4780
+ .join('\n\n');
4781
+ }
4782
+
4783
+ async function refreshTypeGenerationPreview() {
4784
+ const modal = state.modal;
4785
+
4786
+ if (modal?.kind !== 'generate-types') {
4787
+ return;
4788
+ }
4789
+
4790
+ const requestId = (modal.previewRequestId ?? 0) + 1;
4791
+ modal.previewRequestId = requestId;
4792
+ modal.loading = true;
4793
+ modal.error = null;
4794
+ emitChange();
4795
+
4796
+ try {
4797
+ const options = getTypeGenerationRequestOptions(modal);
4798
+ const isAllTables = modal.scope === 'all';
4799
+ const tableNames = isAllTables ? modal.tableNames : [modal.tableName];
4800
+
4801
+ if (!tableNames.length) {
4802
+ throw new Error('No tables available for type generation.');
4803
+ }
4804
+
4805
+ const responses = await Promise.all(
4806
+ tableNames.map(async tableName => {
4807
+ const response = await api.generateStructureTypes(tableName, {
4808
+ target: modal.target,
4809
+ options,
4810
+ });
4811
+
4812
+ return {
4813
+ ...response.data,
4814
+ tableName,
4815
+ warnings: response.warnings ?? [],
4816
+ metadata: response.metadata ?? {},
4817
+ };
4818
+ }),
4819
+ );
4820
+
4821
+ if (state.modal?.kind !== 'generate-types' || state.modal.previewRequestId !== requestId) {
4822
+ return;
4823
+ }
4824
+
4825
+ if (isAllTables) {
4826
+ state.modal.result = {
4827
+ scope: 'all',
4828
+ target: modal.target,
4829
+ fileName: `${responses.length} files`,
4830
+ files: responses,
4831
+ code: combineGeneratedTypeFiles(responses),
4832
+ };
4833
+ state.modal.warnings = responses.flatMap(file =>
4834
+ (file.warnings ?? []).map(warning => `${file.tableName}: ${warning}`),
4835
+ );
4836
+ state.modal.metadata = {
4837
+ fileCount: responses.length,
4838
+ columnCount: responses.reduce((count, file) => count + Number(file.metadata?.columnCount ?? 0), 0),
4839
+ };
4840
+ } else {
4841
+ const [response] = responses;
4842
+ state.modal.result = response;
4843
+ state.modal.warnings = response.warnings ?? [];
4844
+ state.modal.metadata = response.metadata ?? {};
4845
+ }
4846
+ } catch (error) {
4847
+ if (state.modal?.kind !== 'generate-types' || state.modal.previewRequestId !== requestId) {
4848
+ return;
4849
+ }
4850
+
4851
+ state.modal.error = normalizeError(error);
4852
+ state.modal.result = null;
4853
+ state.modal.warnings = error.warnings ?? [];
4854
+ } finally {
4855
+ if (state.modal?.kind === 'generate-types' && state.modal.previewRequestId === requestId) {
4856
+ state.modal.loading = false;
4857
+ emitChange();
4858
+ }
4859
+ }
4860
+ }
4861
+
4862
+ export async function openGenerateTypesModal(tableName, target = 'typescript', scope = 'selected') {
4863
+ const normalizedTarget = TYPE_GENERATION_TARGETS.has(target) ? target : 'typescript';
4864
+ const normalizedScope = scope === 'all' ? 'all' : 'selected';
4865
+ const tableNames = getStructureTableNames();
4866
+ const normalizedTableName = String(tableName ?? state.structure.selectedName ?? '').trim();
4867
+
4868
+ if (normalizedScope === 'all' && !tableNames.length) {
4869
+ pushToast('No tables available for type generation.', 'alert');
4870
+ return;
4871
+ }
4872
+
4873
+ if (normalizedScope === 'selected' && !normalizedTableName) {
4874
+ pushToast('Select a table before generating types.', 'alert');
4875
+ return;
4876
+ }
4877
+
4878
+ state.modal = {
4879
+ kind: 'generate-types',
4880
+ scope: normalizedScope,
4881
+ tableName: normalizedScope === 'all' ? '' : normalizedTableName,
4882
+ tableNames: normalizedScope === 'all' ? tableNames : [],
4883
+ target: normalizedTarget,
4884
+ options: getDefaultTypeGenerationOptions(normalizedTarget),
4885
+ result: null,
4886
+ warnings: [],
4887
+ metadata: {},
4888
+ loading: false,
4889
+ error: null,
4890
+ submitting: false,
4891
+ };
4892
+ emitChange();
4893
+ await refreshTypeGenerationPreview();
4894
+ }
4895
+
4896
+ export async function updateGenerateTypesModal(field, value) {
4897
+ if (state.modal?.kind !== 'generate-types') {
4898
+ return;
4899
+ }
4900
+
4901
+ if (field === 'target') {
4902
+ const target = TYPE_GENERATION_TARGETS.has(value) ? value : 'typescript';
4903
+ state.modal.target = target;
4904
+ state.modal.options = normalizeTypeGenerationOptions(
4905
+ {
4906
+ ...state.modal.options,
4907
+ propertyNaming: getDefaultTypeGenerationOptions(target).propertyNaming,
4908
+ },
4909
+ target,
4910
+ );
4911
+ } else if (field in state.modal.options) {
4912
+ const booleanFields = new Set([
4913
+ 'includeComments',
4914
+ 'includeDefaultsAsComments',
4915
+ 'includeGeneratedColumns',
4916
+ 'includeHiddenColumns',
4917
+ 'exportDeclaration',
4918
+ ]);
4919
+ state.modal.options[field] = booleanFields.has(field) ? Boolean(value) : String(value ?? '');
4920
+ state.modal.options = normalizeTypeGenerationOptions(state.modal.options, state.modal.target);
4921
+ }
4922
+
4923
+ emitChange();
4924
+ await refreshTypeGenerationPreview();
4925
+ }
4926
+
4513
4927
  export function toggleStructureTablesPanel() {
4514
4928
  state.structure.tablesVisible = state.structure.tablesVisible === false;
4515
4929
  storeBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, state.structure.tablesVisible);