sqlite-hub 1.2.0 → 1.5.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 (169) hide show
  1. package/README.md +113 -71
  2. package/bin/sqlite-hub.js +723 -116
  3. package/docs/API.md +152 -0
  4. package/docs/CLI.md +249 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +149 -0
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/guidelines/DESIGN.md +55 -0
  9. package/docs/shortkeys.md +27 -0
  10. package/docs/todo.md +4 -0
  11. package/examples/api/generate-types.js +52 -0
  12. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  22. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  24. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  27. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  28. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  35. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  36. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  40. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  43. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  44. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  45. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  46. package/frontend/js/api.js +63 -0
  47. package/frontend/js/app.js +310 -10
  48. package/frontend/js/components/connectionCard.js +4 -2
  49. package/frontend/js/components/emptyState.js +4 -4
  50. package/frontend/js/components/generateTypesDropdown.js +33 -0
  51. package/frontend/js/components/metricCard.js +1 -1
  52. package/frontend/js/components/modal.js +555 -21
  53. package/frontend/js/components/pageHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryDetail.js +3 -3
  55. package/frontend/js/components/queryHistoryHeader.js +1 -1
  56. package/frontend/js/components/queryHistoryPanel.js +1 -1
  57. package/frontend/js/components/rowEditorPanel.js +13 -3
  58. package/frontend/js/components/sidebar.js +41 -7
  59. package/frontend/js/components/topNav.js +2 -14
  60. package/frontend/js/router.js +38 -6
  61. package/frontend/js/store.js +870 -8
  62. package/frontend/js/utils/inputClear.js +36 -0
  63. package/frontend/js/utils/syntheticData.js +240 -0
  64. package/frontend/js/views/backups.js +581 -37
  65. package/frontend/js/views/charts.js +9 -9
  66. package/frontend/js/views/connections.js +2 -2
  67. package/frontend/js/views/data.js +17 -1
  68. package/frontend/js/views/documents.js +3 -3
  69. package/frontend/js/views/editor.js +6 -6
  70. package/frontend/js/views/logs.js +339 -0
  71. package/frontend/js/views/mediaTagging.js +7 -5
  72. package/frontend/js/views/overview.js +3 -3
  73. package/frontend/js/views/settings.js +80 -30
  74. package/frontend/js/views/structure.js +12 -1
  75. package/frontend/js/views/tableAdvisor.js +385 -0
  76. package/frontend/js/views/tableDesigner.js +6 -0
  77. package/frontend/styles/base.css +1 -40
  78. package/frontend/styles/components.css +258 -232
  79. package/frontend/styles/structure-graph.css +32 -68
  80. package/frontend/styles/tailwind.css +8 -4
  81. package/frontend/styles/tailwind.generated.css +1 -1
  82. package/frontend/styles/tokens.css +28 -21
  83. package/frontend/styles/views.css +102 -261
  84. package/package.json +18 -2
  85. package/server/routes/backups.js +18 -2
  86. package/server/routes/data.js +45 -0
  87. package/server/routes/externalApi.js +321 -2
  88. package/server/routes/logs.js +127 -0
  89. package/server/routes/structure.js +22 -0
  90. package/server/server.js +3 -0
  91. package/server/services/databaseCommandService.js +59 -0
  92. package/server/services/sqlite/backupDiff.js +914 -0
  93. package/server/services/sqlite/backupService.js +77 -1
  94. package/server/services/sqlite/dataBrowserService.js +36 -0
  95. package/server/services/sqlite/introspection.js +226 -22
  96. package/server/services/sqlite/structureService.js +7 -0
  97. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  98. package/server/services/sqlite/tableAdvisor.js +790 -0
  99. package/server/services/storage/appStateStore.js +773 -169
  100. package/server/services/typeGenerationService.js +663 -0
  101. package/tailwind.config.cjs +0 -1
  102. package/.github/funding.yml +0 -2
  103. package/.github/workflows/ci.yml +0 -36
  104. package/database.sqlite +0 -0
  105. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  106. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  107. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  108. package/frontend/assets/mockups/connections_1200.webp +0 -0
  109. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  110. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  111. package/frontend/assets/mockups/documents_1200.webp +0 -0
  112. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  113. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  114. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  115. package/frontend/assets/mockups/overview_1200.webp +0 -0
  116. package/frontend/assets/mockups/settings_1200.webp +0 -0
  117. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  118. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  119. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  120. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  121. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  122. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  123. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  124. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  125. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  126. package/tests/api-token-auth.test.js +0 -279
  127. package/tests/backup-manager.test.js +0 -140
  128. package/tests/backups-view.test.js +0 -70
  129. package/tests/charts-height-preset-storage.test.js +0 -60
  130. package/tests/charts-route-state.test.js +0 -144
  131. package/tests/check-constraint-options.test.js +0 -90
  132. package/tests/cli-args.test.js +0 -113
  133. package/tests/cli-service-delegation.test.js +0 -140
  134. package/tests/connection-removal.test.js +0 -52
  135. package/tests/connections-file-dialog-route.test.js +0 -89
  136. package/tests/copy-column-modal.test.js +0 -131
  137. package/tests/database-command-service.test.js +0 -174
  138. package/tests/database-documents.test.js +0 -85
  139. package/tests/documents-view.test.js +0 -132
  140. package/tests/dropdown-button.test.js +0 -101
  141. package/tests/email-preview.test.js +0 -89
  142. package/tests/export-blob.test.js +0 -99
  143. package/tests/export-filenames.test.js +0 -38
  144. package/tests/file-path-preview.test.js +0 -165
  145. package/tests/form-controls.test.js +0 -34
  146. package/tests/json-preview.test.js +0 -49
  147. package/tests/local-request-security.test.js +0 -85
  148. package/tests/markdown-documents.test.js +0 -103
  149. package/tests/native-file-dialog.test.js +0 -105
  150. package/tests/query-editor.test.js +0 -28
  151. package/tests/query-history-detail.test.js +0 -37
  152. package/tests/query-history-header.test.js +0 -30
  153. package/tests/query-results-truncation.test.js +0 -20
  154. package/tests/risky-sql.test.js +0 -30
  155. package/tests/row-editor-json.test.js +0 -82
  156. package/tests/row-editor-null-values.test.js +0 -184
  157. package/tests/row-editor-timestamp-preview.test.js +0 -192
  158. package/tests/security-paths.test.js +0 -84
  159. package/tests/settings-api-tokens-route.test.js +0 -97
  160. package/tests/settings-metadata.test.js +0 -114
  161. package/tests/settings-view.test.js +0 -108
  162. package/tests/sql-formatter.test.js +0 -173
  163. package/tests/sql-highlight.test.js +0 -38
  164. package/tests/sql-identifier-safety.test.js +0 -171
  165. package/tests/sql-result-limit.test.js +0 -66
  166. package/tests/structure-view.test.js +0 -60
  167. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  168. package/tests/table-scroll-state.test.js +0 -70
  169. package/tests/text-cell-stats.test.js +0 -38
@@ -23,6 +23,11 @@ import { MEDIA_TAGGING_DEFAULT_MAPPING_TABLE, MEDIA_TAGGING_DEFAULT_TAG_TABLE }
23
23
  import { buildTextExportFilename } from './utils/exportFilenames.js';
24
24
  import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
25
25
  import { buildRiskySqlBackupContext, detectRiskySqlOperations } from './utils/riskySql.js';
26
+ import {
27
+ buildSyntheticDataMappings,
28
+ getDefaultSyntheticOptions,
29
+ normalizeSyntheticGeneratorType,
30
+ } from './utils/syntheticData.js';
26
31
 
27
32
  const listeners = new Set();
28
33
  const DEFAULT_SETTINGS = {
@@ -59,7 +64,18 @@ const UI_PREFERENCE_STORAGE_KEYS = {
59
64
  };
60
65
  const QUERY_HISTORY_PAGE_SIZE = 30;
61
66
  const QUERY_HISTORY_RUN_LIMIT = 8;
67
+ const LOG_PAGE_SIZE = 80;
68
+ const LOG_KINDS = new Set(['all', 'query', 'access']);
69
+ const LOG_RANGES = new Set(['1h', '24h', '7d', '30d', 'all']);
70
+ const LOG_ACTORS = new Set(['all', 'user', 'cli', 'api', 'mcp']);
71
+ const LOG_STATUSES = new Set(['all', 'success', 'error']);
72
+ const LOG_QUERY_TYPES = new Set(['all', 'select', 'insert', 'update', 'delete', 'pragma', 'create', 'alter', 'drop', 'other']);
73
+ const LOG_DESTRUCTIVE_FILTERS = new Set(['all', 'yes', 'no']);
62
74
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
75
+ const TYPE_GENERATION_TARGETS = new Set(['typescript', 'rust', 'kotlin', 'swift']);
76
+ const TYPE_GENERATION_NAMING = new Set(['preserve', 'camel', 'pascal', 'snake']);
77
+ const TYPE_GENERATION_NULLABLE = new Set(['native', 'optional']);
78
+ const TYPE_GENERATION_JSON_TYPES = new Set(['unknown', 'record', 'json-value']);
63
79
  const EDITOR_RESULT_TABS = new Set(['results', 'performance', 'messages']);
64
80
  const COPY_COLUMN_MODES = new Set(['column', 'column-with-header', 'first-10', 'markdown-todo']);
65
81
  const TEXT_EXPORT_FORMAT_LABELS = {
@@ -254,6 +270,24 @@ function storeQueryHistoryTab(tab) {
254
270
  }
255
271
  }
256
272
 
273
+ function normalizeSetValue(value, allowedValues, fallback) {
274
+ const normalized = String(value ?? fallback).trim().toLowerCase();
275
+ return allowedValues.has(normalized) ? normalized : fallback;
276
+ }
277
+
278
+ function normalizeLogFilters(filters = {}) {
279
+ return {
280
+ kind: normalizeSetValue(filters.kind, LOG_KINDS, 'all'),
281
+ range: normalizeSetValue(filters.range, LOG_RANGES, 'all'),
282
+ actor: normalizeSetValue(filters.actor, LOG_ACTORS, 'all'),
283
+ status: normalizeSetValue(filters.status, LOG_STATUSES, 'all'),
284
+ queryType: normalizeSetValue(filters.queryType, LOG_QUERY_TYPES, 'all'),
285
+ destructive: normalizeSetValue(filters.destructive, LOG_DESTRUCTIVE_FILTERS, 'all'),
286
+ searchInput: String(filters.searchInput ?? filters.search ?? ''),
287
+ search: String(filters.search ?? filters.searchInput ?? '').trim(),
288
+ };
289
+ }
290
+
257
291
  const state = {
258
292
  ready: false,
259
293
  route: { name: 'landing', path: '/', params: {} },
@@ -271,6 +305,18 @@ const state = {
271
305
  loading: false,
272
306
  operationLoading: false,
273
307
  error: null,
308
+ diff: {
309
+ visible: false,
310
+ backupId: null,
311
+ backupName: '',
312
+ backupCreatedAt: null,
313
+ currentLabel: '',
314
+ activeTab: 'schema',
315
+ requestId: null,
316
+ loading: false,
317
+ error: null,
318
+ data: null,
319
+ },
274
320
  },
275
321
  settings: {
276
322
  data: { ...DEFAULT_SETTINGS },
@@ -292,6 +338,18 @@ const state = {
292
338
  loading: false,
293
339
  error: null,
294
340
  },
341
+ logs: {
342
+ items: [],
343
+ total: 0,
344
+ limit: LOG_PAGE_SIZE,
345
+ offset: 0,
346
+ hasMore: false,
347
+ loading: false,
348
+ loadingMore: false,
349
+ error: null,
350
+ filters: normalizeLogFilters(),
351
+ metadata: null,
352
+ },
295
353
  dataBrowser: {
296
354
  tables: [],
297
355
  selectedTable: null,
@@ -316,6 +374,15 @@ const state = {
316
374
  error: null,
317
375
  saveError: null,
318
376
  },
377
+ tableAdvisor: {
378
+ tables: [],
379
+ selectedTableName: null,
380
+ result: null,
381
+ loading: false,
382
+ analysisLoading: false,
383
+ error: null,
384
+ analysisError: null,
385
+ },
319
386
  editor: {
320
387
  sqlText: readStoredString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft),
321
388
  editorPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.sqlEditorEditorVisible, true),
@@ -551,6 +618,21 @@ function getMediaTaggingRouteErrorKey(error = {}) {
551
618
  return `route:${String(error.code ?? '').trim()}:${String(error.message ?? '').trim()}`;
552
619
  }
553
620
 
621
+ function createEmptyBackupDiffState() {
622
+ return {
623
+ visible: false,
624
+ backupId: null,
625
+ backupName: '',
626
+ backupCreatedAt: null,
627
+ currentLabel: '',
628
+ activeTab: 'schema',
629
+ requestId: null,
630
+ loading: false,
631
+ error: null,
632
+ data: null,
633
+ };
634
+ }
635
+
554
636
  function syncDismissedMediaTaggingIssues() {
555
637
  const availableKeys = new Set();
556
638
 
@@ -591,6 +673,7 @@ function requiresActiveDatabase(routeName) {
591
673
  return [
592
674
  'overview',
593
675
  'backups',
676
+ 'logs',
594
677
  'data',
595
678
  'editor',
596
679
  'editorResults',
@@ -598,6 +681,7 @@ function requiresActiveDatabase(routeName) {
598
681
  'documents',
599
682
  'structure',
600
683
  'tableDesigner',
684
+ 'tableAdvisor',
601
685
  'mediaTaggingSetup',
602
686
  'mediaTaggingQueue',
603
687
  ].includes(routeName);
@@ -864,6 +948,91 @@ function findDataBrowserRowIndexByIdentity(rows = [], identity) {
864
948
  return rows.findIndex(row => areRowIdentitiesEqual(row?.__identity, identity));
865
949
  }
866
950
 
951
+ function coerceDataRouteIdentityValue(column, value) {
952
+ const text = String(value ?? '');
953
+ const affinity = String(column?.affinity ?? '').toUpperCase();
954
+
955
+ if (['INTEGER', 'REAL', 'NUMERIC'].includes(affinity) && text.trim() !== '') {
956
+ const numberValue = Number(text);
957
+
958
+ if (Number.isFinite(numberValue)) {
959
+ return numberValue;
960
+ }
961
+ }
962
+
963
+ return value;
964
+ }
965
+
966
+ function parseCompositeDataRoutePrimaryKey(rawValue) {
967
+ try {
968
+ const parsed = JSON.parse(rawValue);
969
+
970
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
971
+ return parsed;
972
+ }
973
+ } catch {
974
+ return null;
975
+ }
976
+
977
+ return null;
978
+ }
979
+
980
+ function buildDataRouteRowIdentity(table, rawPrimaryKey) {
981
+ const primaryKeyValue = String(rawPrimaryKey ?? '').trim();
982
+ const identityStrategy = table?.identityStrategy ?? null;
983
+
984
+ if (!primaryKeyValue || !identityStrategy) {
985
+ return null;
986
+ }
987
+
988
+ if (identityStrategy.type === 'rowid') {
989
+ const numberValue = Number(primaryKeyValue);
990
+
991
+ return {
992
+ kind: 'rowid',
993
+ values: {
994
+ rowid: Number.isInteger(numberValue) ? numberValue : primaryKeyValue,
995
+ },
996
+ };
997
+ }
998
+
999
+ if (identityStrategy.type !== 'primaryKey') {
1000
+ return null;
1001
+ }
1002
+
1003
+ const columns = identityStrategy.columns ?? [];
1004
+
1005
+ if (columns.length === 1) {
1006
+ const columnName = columns[0];
1007
+ const column = table?.columnMeta?.find(candidate => candidate.name === columnName);
1008
+
1009
+ return {
1010
+ kind: 'primaryKey',
1011
+ columns,
1012
+ values: {
1013
+ [columnName]: coerceDataRouteIdentityValue(column, primaryKeyValue),
1014
+ },
1015
+ };
1016
+ }
1017
+
1018
+ const parsedValue = parseCompositeDataRoutePrimaryKey(primaryKeyValue);
1019
+
1020
+ if (!parsedValue || columns.some(columnName => !Object.prototype.hasOwnProperty.call(parsedValue, columnName))) {
1021
+ return null;
1022
+ }
1023
+
1024
+ return {
1025
+ kind: 'primaryKey',
1026
+ columns,
1027
+ values: Object.fromEntries(
1028
+ columns.map(columnName => {
1029
+ const column = table?.columnMeta?.find(candidate => candidate.name === columnName);
1030
+ return [columnName, coerceDataRouteIdentityValue(column, parsedValue[columnName])];
1031
+ }),
1032
+ ),
1033
+ };
1034
+ }
1035
+
867
1036
  function clearDataBrowserRowSelectionState() {
868
1037
  state.dataBrowser.selectedRowIndex = null;
869
1038
  state.dataBrowser.selectedRow = null;
@@ -1174,6 +1343,7 @@ function resolveQueryHistorySql(historyId) {
1174
1343
 
1175
1344
  function clearRouteSlices() {
1176
1345
  state.overview.error = null;
1346
+ state.logs.error = null;
1177
1347
  state.dataBrowser.error = null;
1178
1348
  state.dataBrowser.saveError = null;
1179
1349
  state.charts.error = null;
@@ -1181,6 +1351,8 @@ function clearRouteSlices() {
1181
1351
  state.charts.resultError = null;
1182
1352
  state.tableDesigner.error = null;
1183
1353
  state.tableDesigner.saveError = null;
1354
+ state.tableAdvisor.error = null;
1355
+ state.tableAdvisor.analysisError = null;
1184
1356
  state.structure.error = null;
1185
1357
  state.mediaTagging.error = null;
1186
1358
  state.documents.error = null;
@@ -1194,6 +1366,15 @@ function setMissingDatabaseState() {
1194
1366
  state.overview.data = null;
1195
1367
  state.overview.error = error;
1196
1368
 
1369
+ state.logs.loading = false;
1370
+ state.logs.loadingMore = false;
1371
+ state.logs.items = [];
1372
+ state.logs.total = 0;
1373
+ state.logs.offset = 0;
1374
+ state.logs.hasMore = false;
1375
+ state.logs.error = error;
1376
+ state.logs.metadata = null;
1377
+
1197
1378
  state.dataBrowser.loading = false;
1198
1379
  state.dataBrowser.tableLoading = false;
1199
1380
  state.dataBrowser.tables = [];
@@ -1234,6 +1415,14 @@ function setMissingDatabaseState() {
1234
1415
  state.tableDesigner.error = error;
1235
1416
  state.tableDesigner.saveError = null;
1236
1417
 
1418
+ state.tableAdvisor.loading = false;
1419
+ state.tableAdvisor.analysisLoading = false;
1420
+ state.tableAdvisor.tables = [];
1421
+ state.tableAdvisor.selectedTableName = null;
1422
+ state.tableAdvisor.result = null;
1423
+ state.tableAdvisor.error = error;
1424
+ state.tableAdvisor.analysisError = null;
1425
+
1237
1426
  state.mediaTagging.loading = false;
1238
1427
  state.mediaTagging.previewLoading = false;
1239
1428
  state.mediaTagging.saving = false;
@@ -1324,6 +1513,10 @@ function syncRouteContext() {
1324
1513
  state.documents.saveError = null;
1325
1514
  }
1326
1515
 
1516
+ if (route.name !== 'backups') {
1517
+ state.backups.diff = createEmptyBackupDiffState();
1518
+ }
1519
+
1327
1520
  if (route.name !== 'mediaTaggingSetup' && route.name !== 'mediaTaggingQueue') {
1328
1521
  state.mediaTagging.selectedTagKeys = [];
1329
1522
  }
@@ -1375,10 +1568,70 @@ async function refreshSettingsState() {
1375
1568
  }
1376
1569
  }
1377
1570
 
1571
+ function buildLogRequestOptions({ append = false } = {}) {
1572
+ const filters = normalizeLogFilters(state.logs.filters);
1573
+
1574
+ return {
1575
+ ...filters,
1576
+ search: filters.search,
1577
+ limit: state.logs.limit,
1578
+ offset: append ? state.logs.items.length : 0,
1579
+ };
1580
+ }
1581
+
1582
+ async function loadLogs(options = {}) {
1583
+ const append = Boolean(options.append);
1584
+
1585
+ if (append && (state.logs.loading || state.logs.loadingMore || !state.logs.hasMore)) {
1586
+ return;
1587
+ }
1588
+
1589
+ if (append) {
1590
+ state.logs.loadingMore = true;
1591
+ } else {
1592
+ state.logs.loading = true;
1593
+ state.logs.offset = 0;
1594
+ state.logs.error = null;
1595
+ }
1596
+
1597
+ emitChange();
1598
+
1599
+ try {
1600
+ const response = await api.getLogs(buildLogRequestOptions({ append }));
1601
+ const data = response.data ?? {};
1602
+ const items = Array.isArray(data.items) ? data.items : [];
1603
+
1604
+ state.logs.items = append ? [...state.logs.items, ...items] : items;
1605
+ state.logs.total = Number(data.total ?? state.logs.items.length);
1606
+ state.logs.limit = Number(data.limit ?? state.logs.limit);
1607
+ state.logs.offset = Number(data.offset ?? 0);
1608
+ state.logs.hasMore = Boolean(data.hasMore);
1609
+ state.logs.metadata = response.metadata ?? data.filters ?? null;
1610
+ state.logs.filters = normalizeLogFilters({
1611
+ ...state.logs.filters,
1612
+ ...(data.filters ?? {}),
1613
+ ...(response.metadata ?? {}),
1614
+ });
1615
+ state.logs.error = null;
1616
+ } catch (error) {
1617
+ state.logs.error = normalizeError(error);
1618
+ if (!append) {
1619
+ state.logs.items = [];
1620
+ state.logs.total = 0;
1621
+ state.logs.hasMore = false;
1622
+ }
1623
+ } finally {
1624
+ state.logs.loading = false;
1625
+ state.logs.loadingMore = false;
1626
+ emitChange();
1627
+ }
1628
+ }
1629
+
1378
1630
  async function refreshBackupsState() {
1379
1631
  if (!state.connections.active) {
1380
1632
  state.backups.items = [];
1381
1633
  state.backups.error = null;
1634
+ state.backups.diff = createEmptyBackupDiffState();
1382
1635
  emitChange();
1383
1636
  return;
1384
1637
  }
@@ -1431,6 +1684,48 @@ export function setSettingsSection(section) {
1431
1684
  emitChange();
1432
1685
  }
1433
1686
 
1687
+ export async function setLogFilter(field, value) {
1688
+ if (!Object.prototype.hasOwnProperty.call(state.logs.filters, field)) {
1689
+ return;
1690
+ }
1691
+
1692
+ state.logs.filters = normalizeLogFilters({
1693
+ ...state.logs.filters,
1694
+ [field]: value,
1695
+ });
1696
+ state.logs.items = [];
1697
+ state.logs.total = 0;
1698
+ state.logs.hasMore = false;
1699
+ await loadLogs();
1700
+ }
1701
+
1702
+ export function setLogSearchInput(value) {
1703
+ state.logs.filters = normalizeLogFilters({
1704
+ ...state.logs.filters,
1705
+ searchInput: value,
1706
+ });
1707
+ }
1708
+
1709
+ export async function applyLogSearch(value = state.logs.filters.searchInput) {
1710
+ state.logs.filters = normalizeLogFilters({
1711
+ ...state.logs.filters,
1712
+ searchInput: value,
1713
+ search: value,
1714
+ });
1715
+ state.logs.items = [];
1716
+ state.logs.total = 0;
1717
+ state.logs.hasMore = false;
1718
+ await loadLogs();
1719
+ }
1720
+
1721
+ export async function refreshLogs() {
1722
+ await loadLogs();
1723
+ }
1724
+
1725
+ export async function loadMoreLogs() {
1726
+ await loadLogs({ append: true });
1727
+ }
1728
+
1434
1729
  export async function checkSettingsAppVersion() {
1435
1730
  state.settings.versionCheckLoading = true;
1436
1731
  state.settings.versionCheckError = null;
@@ -1956,8 +2251,9 @@ async function resolvePendingDataBrowserRow(version) {
1956
2251
  }
1957
2252
  }
1958
2253
 
1959
- async function loadDataTable(version) {
2254
+ async function loadDataTable(version, options = {}) {
1960
2255
  const tableName = state.dataBrowser.selectedTable;
2256
+ const routeRowPrimaryKey = options.rowPrimaryKey ?? null;
1961
2257
  const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, DEFAULT_DATA_PAGE_SIZE);
1962
2258
  const page = Math.max(1, Number(state.dataBrowser.page) || 1);
1963
2259
  const sortColumn = state.dataBrowser.sortColumn;
@@ -2011,6 +2307,15 @@ async function loadDataTable(version) {
2011
2307
  state.dataBrowser.searchColumn,
2012
2308
  );
2013
2309
  clearDataBrowserRowSelectionState();
2310
+ const routeRowIdentity = buildDataRouteRowIdentity(state.dataBrowser.table, routeRowPrimaryKey);
2311
+ if (routeRowPrimaryKey !== null && routeRowPrimaryKey !== undefined) {
2312
+ state.dataBrowser.pendingOpenRow = routeRowIdentity
2313
+ ? {
2314
+ tableName,
2315
+ identity: routeRowIdentity,
2316
+ }
2317
+ : null;
2318
+ }
2014
2319
  await resolvePendingDataBrowserRow(version);
2015
2320
  } catch (error) {
2016
2321
  if (version !== routeLoadVersion) {
@@ -2071,7 +2376,9 @@ async function loadData(version, route) {
2071
2376
  return;
2072
2377
  }
2073
2378
 
2074
- await loadDataTable(version);
2379
+ await loadDataTable(version, {
2380
+ rowPrimaryKey: route.params?.rowPrimaryKey ?? null,
2381
+ });
2075
2382
  } catch (error) {
2076
2383
  if (version !== routeLoadVersion) {
2077
2384
  return;
@@ -2091,6 +2398,94 @@ async function loadData(version, route) {
2091
2398
  }
2092
2399
  }
2093
2400
 
2401
+ async function loadTableAdvisorResult(version, tableName) {
2402
+ if (!tableName) {
2403
+ state.tableAdvisor.result = null;
2404
+ state.tableAdvisor.analysisError = null;
2405
+ state.tableAdvisor.analysisLoading = false;
2406
+ return;
2407
+ }
2408
+
2409
+ state.tableAdvisor.analysisLoading = true;
2410
+ state.tableAdvisor.analysisError = null;
2411
+ state.tableAdvisor.result = null;
2412
+ emitChange();
2413
+
2414
+ try {
2415
+ const response = await api.analyzeTableAdvisor(tableName);
2416
+
2417
+ if (version !== routeLoadVersion) {
2418
+ return;
2419
+ }
2420
+
2421
+ state.tableAdvisor.result = response.data ?? null;
2422
+ state.tableAdvisor.analysisError = null;
2423
+ } catch (error) {
2424
+ if (version !== routeLoadVersion) {
2425
+ return;
2426
+ }
2427
+
2428
+ state.tableAdvisor.result = null;
2429
+ state.tableAdvisor.analysisError = normalizeError(error);
2430
+ } finally {
2431
+ if (version === routeLoadVersion) {
2432
+ state.tableAdvisor.analysisLoading = false;
2433
+ emitChange();
2434
+ }
2435
+ }
2436
+ }
2437
+
2438
+ async function loadTableAdvisor(version, route) {
2439
+ state.tableAdvisor.loading = true;
2440
+ state.tableAdvisor.error = null;
2441
+ state.tableAdvisor.analysisError = null;
2442
+ emitChange();
2443
+
2444
+ try {
2445
+ const response = await api.getDataTables();
2446
+
2447
+ if (version !== routeLoadVersion) {
2448
+ return;
2449
+ }
2450
+
2451
+ const tables = response.data?.tables ?? [];
2452
+ const requestedTableName = route.params?.tableName ?? null;
2453
+ const selectedTableName =
2454
+ requestedTableName && tables.some(table => table.name === requestedTableName)
2455
+ ? requestedTableName
2456
+ : (state.tableAdvisor.selectedTableName && tables.some(table => table.name === state.tableAdvisor.selectedTableName)
2457
+ ? state.tableAdvisor.selectedTableName
2458
+ : (tables[0]?.name ?? null));
2459
+
2460
+ state.tableAdvisor.tables = tables;
2461
+ state.tableAdvisor.selectedTableName = selectedTableName;
2462
+ state.tableAdvisor.error = null;
2463
+
2464
+ if (!selectedTableName) {
2465
+ state.tableAdvisor.result = null;
2466
+ return;
2467
+ }
2468
+
2469
+ state.tableAdvisor.loading = false;
2470
+ emitChange();
2471
+ await loadTableAdvisorResult(version, selectedTableName);
2472
+ } catch (error) {
2473
+ if (version !== routeLoadVersion) {
2474
+ return;
2475
+ }
2476
+
2477
+ state.tableAdvisor.tables = [];
2478
+ state.tableAdvisor.selectedTableName = null;
2479
+ state.tableAdvisor.result = null;
2480
+ state.tableAdvisor.error = normalizeError(error);
2481
+ } finally {
2482
+ if (version === routeLoadVersion) {
2483
+ state.tableAdvisor.loading = false;
2484
+ emitChange();
2485
+ }
2486
+ }
2487
+ }
2488
+
2094
2489
  async function loadStructureDetail(version) {
2095
2490
  const entry = getCurrentStructureEntry(state);
2096
2491
 
@@ -2435,6 +2830,7 @@ function invalidateDatabaseCaches(options = {}) {
2435
2830
  }
2436
2831
 
2437
2832
  state.overview.data = null;
2833
+ state.backups.diff = createEmptyBackupDiffState();
2438
2834
  state.dataBrowser.tables = [];
2439
2835
  if (!preserveDataBrowserState) {
2440
2836
  state.dataBrowser.selectedTable = null;
@@ -2537,6 +2933,9 @@ async function loadRouteData(route, options = {}) {
2537
2933
  case 'backups':
2538
2934
  await refreshBackupsState();
2539
2935
  return;
2936
+ case 'logs':
2937
+ await loadLogs();
2938
+ return;
2540
2939
  case 'data':
2541
2940
  await loadData(version, route);
2542
2941
  return;
@@ -2556,6 +2955,9 @@ async function loadRouteData(route, options = {}) {
2556
2955
  case 'tableDesigner':
2557
2956
  await loadTableDesigner(version, route);
2558
2957
  return;
2958
+ case 'tableAdvisor':
2959
+ await loadTableAdvisor(version, route);
2960
+ return;
2559
2961
  case 'mediaTaggingSetup':
2560
2962
  case 'mediaTaggingQueue':
2561
2963
  await loadMediaTagging(version);
@@ -2758,6 +3160,186 @@ export function openDataExportModal() {
2758
3160
  emitChange();
2759
3161
  }
2760
3162
 
3163
+ function getGenerateDataModalColumn(modal, columnName) {
3164
+ return (modal?.columns ?? []).find(column => column.name === columnName) ?? null;
3165
+ }
3166
+
3167
+ function resetGenerateDataPreview(modal) {
3168
+ if (!modal) {
3169
+ return;
3170
+ }
3171
+
3172
+ modal.previewRows = [];
3173
+ modal.previewColumns = (modal.columns ?? []).map(column => column.name);
3174
+ }
3175
+
3176
+ function buildGenerateDataPayload(modal) {
3177
+ return {
3178
+ rowCount: Number(modal.rowCount ?? 100),
3179
+ mappings: (modal.mappings ?? []).map(mapping => ({
3180
+ columnName: mapping.columnName,
3181
+ generator: mapping.generator,
3182
+ options: { ...(mapping.options ?? {}) },
3183
+ })),
3184
+ };
3185
+ }
3186
+
3187
+ export function openGenerateDataModal() {
3188
+ const table = state.dataBrowser.table;
3189
+ const tableName = table?.name ?? state.dataBrowser.selectedTable;
3190
+
3191
+ if (!tableName || !table) {
3192
+ pushToast('Select a table before generating rows.', 'alert');
3193
+ return;
3194
+ }
3195
+
3196
+ const columns = (table.columnMeta ?? []).filter(column => column.visible && !column.generated);
3197
+
3198
+ state.modal = {
3199
+ kind: 'generate-data',
3200
+ tableName,
3201
+ columns,
3202
+ rowCount: 100,
3203
+ mappings: buildSyntheticDataMappings(columns, table.foreignKeys ?? []),
3204
+ previewColumns: columns.map(column => column.name),
3205
+ previewRows: [],
3206
+ previewLoading: false,
3207
+ previewRequestId: 0,
3208
+ error: null,
3209
+ submitting: false,
3210
+ };
3211
+ emitChange();
3212
+ }
3213
+
3214
+ export function updateGenerateDataModal(field, value, options = {}) {
3215
+ if (state.modal?.kind !== 'generate-data') {
3216
+ return;
3217
+ }
3218
+
3219
+ if (field === 'rowCount') {
3220
+ const nextValue = String(value ?? '');
3221
+
3222
+ if (state.modal.rowCount === nextValue) {
3223
+ return;
3224
+ }
3225
+
3226
+ state.modal.rowCount = nextValue;
3227
+ state.modal.error = null;
3228
+ resetGenerateDataPreview(state.modal);
3229
+ if (options.notify !== false) {
3230
+ emitChange();
3231
+ }
3232
+ }
3233
+ }
3234
+
3235
+ export function updateGenerateDataMapping(columnName, field, value, options = {}) {
3236
+ const modal = state.modal;
3237
+
3238
+ if (modal?.kind !== 'generate-data') {
3239
+ return;
3240
+ }
3241
+
3242
+ const mapping = (modal.mappings ?? []).find(item => item.columnName === columnName);
3243
+
3244
+ if (!mapping) {
3245
+ return;
3246
+ }
3247
+
3248
+ const column = getGenerateDataModalColumn(modal, columnName);
3249
+
3250
+ if (field === 'generator') {
3251
+ const generator = normalizeSyntheticGeneratorType(value, mapping.generator);
3252
+
3253
+ if (mapping.generator === generator) {
3254
+ return;
3255
+ }
3256
+
3257
+ mapping.generator = generator;
3258
+ mapping.options = getDefaultSyntheticOptions(generator, column ?? {});
3259
+ } else {
3260
+ const nextValue = value;
3261
+
3262
+ if ((mapping.options ?? {})[field] === nextValue) {
3263
+ return;
3264
+ }
3265
+
3266
+ mapping.options = {
3267
+ ...(mapping.options ?? {}),
3268
+ [field]: nextValue,
3269
+ };
3270
+ }
3271
+
3272
+ modal.error = null;
3273
+ resetGenerateDataPreview(modal);
3274
+ if (options.notify !== false) {
3275
+ emitChange();
3276
+ }
3277
+ }
3278
+
3279
+ export async function previewGenerateDataRows() {
3280
+ const modal = state.modal;
3281
+
3282
+ if (modal?.kind !== 'generate-data') {
3283
+ return null;
3284
+ }
3285
+
3286
+ const requestId = (modal.previewRequestId ?? 0) + 1;
3287
+ modal.previewRequestId = requestId;
3288
+ modal.previewLoading = true;
3289
+ modal.error = null;
3290
+ emitChange();
3291
+
3292
+ try {
3293
+ const response = await api.previewSyntheticDataRows(modal.tableName, buildGenerateDataPayload(modal));
3294
+
3295
+ if (state.modal?.kind !== 'generate-data' || state.modal.previewRequestId !== requestId) {
3296
+ return null;
3297
+ }
3298
+
3299
+ state.modal.previewColumns = response.data?.columns ?? [];
3300
+ state.modal.previewRows = response.data?.rows ?? [];
3301
+ state.modal.error = null;
3302
+ return response.data;
3303
+ } catch (error) {
3304
+ if (state.modal?.kind === 'generate-data' && state.modal.previewRequestId === requestId) {
3305
+ state.modal.error = normalizeError(error);
3306
+ }
3307
+
3308
+ return null;
3309
+ } finally {
3310
+ if (state.modal?.kind === 'generate-data' && state.modal.previewRequestId === requestId) {
3311
+ state.modal.previewLoading = false;
3312
+ emitChange();
3313
+ }
3314
+ }
3315
+ }
3316
+
3317
+ export async function submitGenerateDataRows() {
3318
+ const modal = state.modal;
3319
+
3320
+ if (modal?.kind !== 'generate-data') {
3321
+ return null;
3322
+ }
3323
+
3324
+ startModalSubmission();
3325
+
3326
+ try {
3327
+ const response = await api.insertSyntheticDataRows(modal.tableName, buildGenerateDataPayload(modal));
3328
+
3329
+ if (state.modal?.kind === 'generate-data') {
3330
+ closeModalInternal();
3331
+ }
3332
+
3333
+ pushToast(response.message || `Generated ${response.data?.insertedRowCount ?? modal.rowCount} rows for ${modal.tableName}.`, 'success');
3334
+ await loadDataTable(++routeLoadVersion);
3335
+ clearDataBrowserRowSelectionState();
3336
+ return response.data;
3337
+ } catch (error) {
3338
+ withModalError(error);
3339
+ return null;
3340
+ }
3341
+ }
3342
+
2761
3343
  export function openCopyColumnModal({ scope = 'editor', columnName = '', mode = 'column' } = {}) {
2762
3344
  const resultScope = normalizeCopyColumnScope(scope);
2763
3345
  const normalizedColumnName = String(columnName ?? '');
@@ -3976,7 +4558,7 @@ export async function submitCreateBackupConfirmation({ name = '', notes = '', ty
3976
4558
  }
3977
4559
  }
3978
4560
 
3979
- export function openEditBackupNotesModal(backupId) {
4561
+ export function openEditBackupModal(backupId) {
3980
4562
  const backup = state.backups.items.find(item => String(item.id) === String(backupId));
3981
4563
 
3982
4564
  if (!backup) {
@@ -3985,7 +4567,7 @@ export function openEditBackupNotesModal(backupId) {
3985
4567
  }
3986
4568
 
3987
4569
  state.modal = {
3988
- kind: 'edit-backup-notes',
4570
+ kind: 'edit-backup',
3989
4571
  backupId: backup.id,
3990
4572
  backupName: backup.name,
3991
4573
  notes: backup.notes ?? '',
@@ -3995,8 +4577,8 @@ export function openEditBackupNotesModal(backupId) {
3995
4577
  emitChange();
3996
4578
  }
3997
4579
 
3998
- export async function submitEditBackupNotesConfirmation(notes = '') {
3999
- if (state.modal?.kind !== 'edit-backup-notes') {
4580
+ export async function submitEditBackupConfirmation({ name = '', notes = '' } = {}) {
4581
+ if (state.modal?.kind !== 'edit-backup') {
4000
4582
  return null;
4001
4583
  }
4002
4584
 
@@ -4005,12 +4587,12 @@ export async function submitEditBackupNotesConfirmation(notes = '') {
4005
4587
  state.backups.operationLoading = true;
4006
4588
 
4007
4589
  try {
4008
- const response = await api.updateBackup(backupId, { notes });
4590
+ const response = await api.updateBackup(backupId, { name, notes });
4009
4591
  state.backups.items = state.backups.items.map(item =>
4010
4592
  String(item.id) === String(response.data.id) ? response.data : item
4011
4593
  );
4012
4594
  closeModalInternal();
4013
- pushToast(response.message || 'Backup notes updated.', 'success');
4595
+ pushToast(response.message || 'Backup updated.', 'success');
4014
4596
  return response.data;
4015
4597
  } catch (error) {
4016
4598
  withModalError(error);
@@ -4052,6 +4634,9 @@ export async function submitDeleteBackupConfirmation() {
4052
4634
  try {
4053
4635
  const response = await api.deleteBackup(backupId);
4054
4636
  state.backups.items = state.backups.items.filter(item => String(item.id) !== String(backupId));
4637
+ if (String(state.backups.diff.backupId ?? '') === String(backupId)) {
4638
+ state.backups.diff = createEmptyBackupDiffState();
4639
+ }
4055
4640
  closeModalInternal();
4056
4641
  pushToast(response.message || 'Backup deleted.', 'muted');
4057
4642
  return true;
@@ -4082,6 +4667,75 @@ export async function downloadBackup(backupId) {
4082
4667
  }
4083
4668
  }
4084
4669
 
4670
+ export async function openBackupDiffDrawer(backupId) {
4671
+ const backup = state.backups.items.find(item => String(item.id) === String(backupId));
4672
+
4673
+ if (!backup) {
4674
+ pushToast('The selected backup could not be loaded.', 'alert');
4675
+ return null;
4676
+ }
4677
+
4678
+ const requestId = crypto.randomUUID();
4679
+ state.backups.diff = {
4680
+ visible: true,
4681
+ backupId: backup.id,
4682
+ backupName: backup.name,
4683
+ backupCreatedAt: backup.createdAt,
4684
+ currentLabel: state.connections.active?.label ?? 'Current database',
4685
+ activeTab: 'schema',
4686
+ requestId,
4687
+ loading: true,
4688
+ error: null,
4689
+ data: null,
4690
+ };
4691
+ state.backups.operationLoading = true;
4692
+ emitChange();
4693
+
4694
+ try {
4695
+ const response = await api.getBackupDiff(backup.id);
4696
+
4697
+ if (state.backups.diff.requestId === requestId) {
4698
+ state.backups.diff.data = response.data ?? null;
4699
+ state.backups.diff.backupName = response.data?.backup?.name ?? state.backups.diff.backupName;
4700
+ state.backups.diff.backupCreatedAt = response.data?.backup?.createdAt ?? state.backups.diff.backupCreatedAt;
4701
+ state.backups.diff.currentLabel = response.data?.current?.label ?? state.backups.diff.currentLabel;
4702
+ state.backups.diff.error = null;
4703
+ }
4704
+
4705
+ return response.data ?? null;
4706
+ } catch (error) {
4707
+ if (state.backups.diff.requestId === requestId) {
4708
+ state.backups.diff.error = normalizeError(error);
4709
+ } else {
4710
+ pushToast(normalizeError(error).message || 'Backup comparison failed.', 'alert');
4711
+ }
4712
+
4713
+ return null;
4714
+ } finally {
4715
+ state.backups.operationLoading = false;
4716
+
4717
+ if (state.backups.diff.requestId === requestId) {
4718
+ state.backups.diff.loading = false;
4719
+ }
4720
+
4721
+ emitChange();
4722
+ }
4723
+ }
4724
+
4725
+ export function setBackupDiffTab(tab) {
4726
+ if (!state.backups.diff.visible) {
4727
+ return;
4728
+ }
4729
+
4730
+ state.backups.diff.activeTab = tab === 'data' ? 'data' : 'schema';
4731
+ emitChange();
4732
+ }
4733
+
4734
+ export function closeBackupDiffDrawer() {
4735
+ state.backups.diff = createEmptyBackupDiffState();
4736
+ emitChange();
4737
+ }
4738
+
4085
4739
  export function openRestoreBackupModal(backupId) {
4086
4740
  const backup = state.backups.items.find(item => String(item.id) === String(backupId));
4087
4741
 
@@ -4510,6 +5164,214 @@ export async function selectStructureEntry(name) {
4510
5164
  await loadStructureDetail(++routeLoadVersion);
4511
5165
  }
4512
5166
 
5167
+ function getDefaultTypeGenerationOptions(target = 'typescript') {
5168
+ const propertyNaming = target === 'rust' ? 'snake' : 'camel';
5169
+
5170
+ return {
5171
+ typeName: '',
5172
+ propertyNaming,
5173
+ nullableMode: 'native',
5174
+ includeComments: false,
5175
+ includeDefaultsAsComments: false,
5176
+ includeGeneratedColumns: true,
5177
+ includeHiddenColumns: false,
5178
+ exportDeclaration: true,
5179
+ jsonType: 'unknown',
5180
+ };
5181
+ }
5182
+
5183
+ function normalizeTypeGenerationOptions(options = {}, target = 'typescript') {
5184
+ const defaults = getDefaultTypeGenerationOptions(target);
5185
+ const propertyNaming = TYPE_GENERATION_NAMING.has(options.propertyNaming)
5186
+ ? options.propertyNaming
5187
+ : defaults.propertyNaming;
5188
+ const nullableMode =
5189
+ TYPE_GENERATION_NULLABLE.has(options.nullableMode) && (target === 'typescript' || options.nullableMode !== 'optional')
5190
+ ? options.nullableMode
5191
+ : 'native';
5192
+ const jsonType = TYPE_GENERATION_JSON_TYPES.has(options.jsonType) ? options.jsonType : 'unknown';
5193
+
5194
+ return {
5195
+ ...defaults,
5196
+ ...options,
5197
+ propertyNaming,
5198
+ nullableMode,
5199
+ jsonType,
5200
+ typeName: String(options.typeName ?? ''),
5201
+ };
5202
+ }
5203
+
5204
+ function getStructureTableNames() {
5205
+ return (state.structure.data?.grouped?.tables ?? [])
5206
+ .filter(table => table?.type === 'table')
5207
+ .map(table => String(table.name ?? '').trim())
5208
+ .filter(Boolean);
5209
+ }
5210
+
5211
+ function getTypeGenerationRequestOptions(modal) {
5212
+ const options = { ...modal.options };
5213
+
5214
+ if (!String(options.typeName ?? '').trim()) {
5215
+ delete options.typeName;
5216
+ }
5217
+
5218
+ if (modal.target !== 'typescript') {
5219
+ delete options.jsonType;
5220
+ }
5221
+
5222
+ return options;
5223
+ }
5224
+
5225
+ function combineGeneratedTypeFiles(files = []) {
5226
+ return files
5227
+ .map(file => [`// ${file.fileName}`, file.code].filter(Boolean).join('\n'))
5228
+ .join('\n\n');
5229
+ }
5230
+
5231
+ async function refreshTypeGenerationPreview() {
5232
+ const modal = state.modal;
5233
+
5234
+ if (modal?.kind !== 'generate-types') {
5235
+ return;
5236
+ }
5237
+
5238
+ const requestId = (modal.previewRequestId ?? 0) + 1;
5239
+ modal.previewRequestId = requestId;
5240
+ modal.loading = true;
5241
+ modal.error = null;
5242
+ emitChange();
5243
+
5244
+ try {
5245
+ const options = getTypeGenerationRequestOptions(modal);
5246
+ const isAllTables = modal.scope === 'all';
5247
+ const tableNames = isAllTables ? modal.tableNames : [modal.tableName];
5248
+
5249
+ if (!tableNames.length) {
5250
+ throw new Error('No tables available for type generation.');
5251
+ }
5252
+
5253
+ const responses = await Promise.all(
5254
+ tableNames.map(async tableName => {
5255
+ const response = await api.generateStructureTypes(tableName, {
5256
+ target: modal.target,
5257
+ options,
5258
+ });
5259
+
5260
+ return {
5261
+ ...response.data,
5262
+ tableName,
5263
+ warnings: response.warnings ?? [],
5264
+ metadata: response.metadata ?? {},
5265
+ };
5266
+ }),
5267
+ );
5268
+
5269
+ if (state.modal?.kind !== 'generate-types' || state.modal.previewRequestId !== requestId) {
5270
+ return;
5271
+ }
5272
+
5273
+ if (isAllTables) {
5274
+ state.modal.result = {
5275
+ scope: 'all',
5276
+ target: modal.target,
5277
+ fileName: `${responses.length} files`,
5278
+ files: responses,
5279
+ code: combineGeneratedTypeFiles(responses),
5280
+ };
5281
+ state.modal.warnings = responses.flatMap(file =>
5282
+ (file.warnings ?? []).map(warning => `${file.tableName}: ${warning}`),
5283
+ );
5284
+ state.modal.metadata = {
5285
+ fileCount: responses.length,
5286
+ columnCount: responses.reduce((count, file) => count + Number(file.metadata?.columnCount ?? 0), 0),
5287
+ };
5288
+ } else {
5289
+ const [response] = responses;
5290
+ state.modal.result = response;
5291
+ state.modal.warnings = response.warnings ?? [];
5292
+ state.modal.metadata = response.metadata ?? {};
5293
+ }
5294
+ } catch (error) {
5295
+ if (state.modal?.kind !== 'generate-types' || state.modal.previewRequestId !== requestId) {
5296
+ return;
5297
+ }
5298
+
5299
+ state.modal.error = normalizeError(error);
5300
+ state.modal.result = null;
5301
+ state.modal.warnings = error.warnings ?? [];
5302
+ } finally {
5303
+ if (state.modal?.kind === 'generate-types' && state.modal.previewRequestId === requestId) {
5304
+ state.modal.loading = false;
5305
+ emitChange();
5306
+ }
5307
+ }
5308
+ }
5309
+
5310
+ export async function openGenerateTypesModal(tableName, target = 'typescript', scope = 'selected') {
5311
+ const normalizedTarget = TYPE_GENERATION_TARGETS.has(target) ? target : 'typescript';
5312
+ const normalizedScope = scope === 'all' ? 'all' : 'selected';
5313
+ const tableNames = getStructureTableNames();
5314
+ const normalizedTableName = String(tableName ?? state.structure.selectedName ?? '').trim();
5315
+
5316
+ if (normalizedScope === 'all' && !tableNames.length) {
5317
+ pushToast('No tables available for type generation.', 'alert');
5318
+ return;
5319
+ }
5320
+
5321
+ if (normalizedScope === 'selected' && !normalizedTableName) {
5322
+ pushToast('Select a table before generating types.', 'alert');
5323
+ return;
5324
+ }
5325
+
5326
+ state.modal = {
5327
+ kind: 'generate-types',
5328
+ scope: normalizedScope,
5329
+ tableName: normalizedScope === 'all' ? '' : normalizedTableName,
5330
+ tableNames: normalizedScope === 'all' ? tableNames : [],
5331
+ target: normalizedTarget,
5332
+ options: getDefaultTypeGenerationOptions(normalizedTarget),
5333
+ result: null,
5334
+ warnings: [],
5335
+ metadata: {},
5336
+ loading: false,
5337
+ error: null,
5338
+ submitting: false,
5339
+ };
5340
+ emitChange();
5341
+ await refreshTypeGenerationPreview();
5342
+ }
5343
+
5344
+ export async function updateGenerateTypesModal(field, value) {
5345
+ if (state.modal?.kind !== 'generate-types') {
5346
+ return;
5347
+ }
5348
+
5349
+ if (field === 'target') {
5350
+ const target = TYPE_GENERATION_TARGETS.has(value) ? value : 'typescript';
5351
+ state.modal.target = target;
5352
+ state.modal.options = normalizeTypeGenerationOptions(
5353
+ {
5354
+ ...state.modal.options,
5355
+ propertyNaming: getDefaultTypeGenerationOptions(target).propertyNaming,
5356
+ },
5357
+ target,
5358
+ );
5359
+ } else if (field in state.modal.options) {
5360
+ const booleanFields = new Set([
5361
+ 'includeComments',
5362
+ 'includeDefaultsAsComments',
5363
+ 'includeGeneratedColumns',
5364
+ 'includeHiddenColumns',
5365
+ 'exportDeclaration',
5366
+ ]);
5367
+ state.modal.options[field] = booleanFields.has(field) ? Boolean(value) : String(value ?? '');
5368
+ state.modal.options = normalizeTypeGenerationOptions(state.modal.options, state.modal.target);
5369
+ }
5370
+
5371
+ emitChange();
5372
+ await refreshTypeGenerationPreview();
5373
+ }
5374
+
4513
5375
  export function toggleStructureTablesPanel() {
4514
5376
  state.structure.tablesVisible = state.structure.tablesVisible === false;
4515
5377
  storeBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, state.structure.tablesVisible);