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.
- package/README.md +106 -73
- package/bin/sqlite-hub.js +176 -2
- package/docs/API.md +122 -0
- package/docs/CLI.md +227 -0
- package/docs/DESIGN_GUIDELINES.md +45 -0
- package/docs/changelog.md +141 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +16 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +18 -0
- package/frontend/js/app.js +118 -8
- package/frontend/js/components/connectionCard.js +1 -1
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +211 -18
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +422 -8
- package/frontend/js/views/backups.js +529 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +1 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +3 -3
- package/frontend/js/views/structure.js +6 -1
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +109 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +94 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/externalApi.js +36 -0
- package/server/routes/structure.js +22 -0
- package/server/services/databaseCommandService.js +14 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
package/frontend/js/store.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
3999
|
-
if (state.modal?.kind !== 'edit-backup
|
|
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
|
|
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);
|