sqlite-hub 1.1.2 → 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 +209 -13
- 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/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +65 -16
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +489 -9
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/views/backups.js +556 -36
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +43 -17
- 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 +4 -3
- package/frontend/js/views/structure.js +27 -10
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +114 -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 +29 -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 -64
- 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 -75
- 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 -79
- 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 -155
- 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 -107
- 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 -56
- 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 = {
|
|
@@ -82,6 +86,7 @@ let chartsLoadVersion = 0;
|
|
|
82
86
|
let chartsDetailLoadVersion = 0;
|
|
83
87
|
let mediaTaggingPreviewVersion = 0;
|
|
84
88
|
let documentsLoadVersion = 0;
|
|
89
|
+
let lastOpenDocumentId = null;
|
|
85
90
|
|
|
86
91
|
function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
|
|
87
92
|
try {
|
|
@@ -270,6 +275,18 @@ const state = {
|
|
|
270
275
|
loading: false,
|
|
271
276
|
operationLoading: false,
|
|
272
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
|
+
},
|
|
273
290
|
},
|
|
274
291
|
settings: {
|
|
275
292
|
data: { ...DEFAULT_SETTINGS },
|
|
@@ -550,6 +567,21 @@ function getMediaTaggingRouteErrorKey(error = {}) {
|
|
|
550
567
|
return `route:${String(error.code ?? '').trim()}:${String(error.message ?? '').trim()}`;
|
|
551
568
|
}
|
|
552
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
|
+
|
|
553
585
|
function syncDismissedMediaTaggingIssues() {
|
|
554
586
|
const availableKeys = new Set();
|
|
555
587
|
|
|
@@ -863,6 +895,91 @@ function findDataBrowserRowIndexByIdentity(rows = [], identity) {
|
|
|
863
895
|
return rows.findIndex(row => areRowIdentitiesEqual(row?.__identity, identity));
|
|
864
896
|
}
|
|
865
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
|
+
|
|
866
983
|
function clearDataBrowserRowSelectionState() {
|
|
867
984
|
state.dataBrowser.selectedRowIndex = null;
|
|
868
985
|
state.dataBrowser.selectedRow = null;
|
|
@@ -1003,6 +1120,10 @@ function applyCurrentDocument(document) {
|
|
|
1003
1120
|
state.documents.draftContent = document?.content ?? '';
|
|
1004
1121
|
state.documents.dirty = false;
|
|
1005
1122
|
state.documents.saveError = null;
|
|
1123
|
+
|
|
1124
|
+
if (document?.id) {
|
|
1125
|
+
lastOpenDocumentId = document.id;
|
|
1126
|
+
}
|
|
1006
1127
|
}
|
|
1007
1128
|
|
|
1008
1129
|
function upsertDocumentSummary(document) {
|
|
@@ -1319,6 +1440,10 @@ function syncRouteContext() {
|
|
|
1319
1440
|
state.documents.saveError = null;
|
|
1320
1441
|
}
|
|
1321
1442
|
|
|
1443
|
+
if (route.name !== 'backups') {
|
|
1444
|
+
state.backups.diff = createEmptyBackupDiffState();
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1322
1447
|
if (route.name !== 'mediaTaggingSetup' && route.name !== 'mediaTaggingQueue') {
|
|
1323
1448
|
state.mediaTagging.selectedTagKeys = [];
|
|
1324
1449
|
}
|
|
@@ -1374,6 +1499,7 @@ async function refreshBackupsState() {
|
|
|
1374
1499
|
if (!state.connections.active) {
|
|
1375
1500
|
state.backups.items = [];
|
|
1376
1501
|
state.backups.error = null;
|
|
1502
|
+
state.backups.diff = createEmptyBackupDiffState();
|
|
1377
1503
|
emitChange();
|
|
1378
1504
|
return;
|
|
1379
1505
|
}
|
|
@@ -1951,8 +2077,9 @@ async function resolvePendingDataBrowserRow(version) {
|
|
|
1951
2077
|
}
|
|
1952
2078
|
}
|
|
1953
2079
|
|
|
1954
|
-
async function loadDataTable(version) {
|
|
2080
|
+
async function loadDataTable(version, options = {}) {
|
|
1955
2081
|
const tableName = state.dataBrowser.selectedTable;
|
|
2082
|
+
const routeRowPrimaryKey = options.rowPrimaryKey ?? null;
|
|
1956
2083
|
const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, DEFAULT_DATA_PAGE_SIZE);
|
|
1957
2084
|
const page = Math.max(1, Number(state.dataBrowser.page) || 1);
|
|
1958
2085
|
const sortColumn = state.dataBrowser.sortColumn;
|
|
@@ -2006,6 +2133,15 @@ async function loadDataTable(version) {
|
|
|
2006
2133
|
state.dataBrowser.searchColumn,
|
|
2007
2134
|
);
|
|
2008
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
|
+
}
|
|
2009
2145
|
await resolvePendingDataBrowserRow(version);
|
|
2010
2146
|
} catch (error) {
|
|
2011
2147
|
if (version !== routeLoadVersion) {
|
|
@@ -2066,7 +2202,9 @@ async function loadData(version, route) {
|
|
|
2066
2202
|
return;
|
|
2067
2203
|
}
|
|
2068
2204
|
|
|
2069
|
-
await loadDataTable(version
|
|
2205
|
+
await loadDataTable(version, {
|
|
2206
|
+
rowPrimaryKey: route.params?.rowPrimaryKey ?? null,
|
|
2207
|
+
});
|
|
2070
2208
|
} catch (error) {
|
|
2071
2209
|
if (version !== routeLoadVersion) {
|
|
2072
2210
|
return;
|
|
@@ -2425,7 +2563,12 @@ async function previewMediaTaggingDraft(options = {}) {
|
|
|
2425
2563
|
function invalidateDatabaseCaches(options = {}) {
|
|
2426
2564
|
const preserveDataBrowserState = options.preserveDataBrowserState === true;
|
|
2427
2565
|
|
|
2566
|
+
if (!preserveDataBrowserState) {
|
|
2567
|
+
lastOpenDocumentId = null;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2428
2570
|
state.overview.data = null;
|
|
2571
|
+
state.backups.diff = createEmptyBackupDiffState();
|
|
2429
2572
|
state.dataBrowser.tables = [];
|
|
2430
2573
|
if (!preserveDataBrowserState) {
|
|
2431
2574
|
state.dataBrowser.selectedTable = null;
|
|
@@ -2932,7 +3075,7 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
|
|
|
2932
3075
|
return `${before}${prefix}${text}${suffix}${after}`;
|
|
2933
3076
|
}
|
|
2934
3077
|
|
|
2935
|
-
function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
3078
|
+
export function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
2936
3079
|
if (!state.documents.selectedId) {
|
|
2937
3080
|
return false;
|
|
2938
3081
|
}
|
|
@@ -2950,6 +3093,63 @@ function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
|
2950
3093
|
return true;
|
|
2951
3094
|
}
|
|
2952
3095
|
|
|
3096
|
+
export async function insertMarkdownIntoLastOpenDocument(markdown) {
|
|
3097
|
+
if (state.route.name === 'documents' && state.documents.selectedId) {
|
|
3098
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
|
|
3099
|
+
|
|
3100
|
+
return {
|
|
3101
|
+
documentId: state.documents.selectedId,
|
|
3102
|
+
inserted,
|
|
3103
|
+
saved: false,
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
const documentId = lastOpenDocumentId ?? state.documents.selectedId;
|
|
3108
|
+
|
|
3109
|
+
if (!documentId) {
|
|
3110
|
+
return {
|
|
3111
|
+
documentId: null,
|
|
3112
|
+
inserted: false,
|
|
3113
|
+
saved: false,
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
const response = await api.getDocument(documentId);
|
|
3118
|
+
const document = response.data ?? null;
|
|
3119
|
+
|
|
3120
|
+
if (!document?.id) {
|
|
3121
|
+
return {
|
|
3122
|
+
documentId: null,
|
|
3123
|
+
inserted: false,
|
|
3124
|
+
saved: false,
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
const nextContent = buildDocumentMarkdownInsertion(document.content, markdown);
|
|
3129
|
+
|
|
3130
|
+
if (nextContent === document.content) {
|
|
3131
|
+
return {
|
|
3132
|
+
documentId: document.id,
|
|
3133
|
+
inserted: false,
|
|
3134
|
+
saved: false,
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
const updateResponse = await api.updateDocument(document.id, {
|
|
3139
|
+
content: nextContent,
|
|
3140
|
+
});
|
|
3141
|
+
const updatedDocument = updateResponse.data ?? document;
|
|
3142
|
+
|
|
3143
|
+
lastOpenDocumentId = updatedDocument.id;
|
|
3144
|
+
upsertDocumentSummary(updatedDocument);
|
|
3145
|
+
|
|
3146
|
+
return {
|
|
3147
|
+
documentId: updatedDocument.id,
|
|
3148
|
+
inserted: true,
|
|
3149
|
+
saved: true,
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
|
|
2953
3153
|
function getDocumentInsertQueryTitle(query) {
|
|
2954
3154
|
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
|
|
2955
3155
|
}
|
|
@@ -3910,7 +4110,7 @@ export async function submitCreateBackupConfirmation({ name = '', notes = '', ty
|
|
|
3910
4110
|
}
|
|
3911
4111
|
}
|
|
3912
4112
|
|
|
3913
|
-
export function
|
|
4113
|
+
export function openEditBackupModal(backupId) {
|
|
3914
4114
|
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
3915
4115
|
|
|
3916
4116
|
if (!backup) {
|
|
@@ -3919,7 +4119,7 @@ export function openEditBackupNotesModal(backupId) {
|
|
|
3919
4119
|
}
|
|
3920
4120
|
|
|
3921
4121
|
state.modal = {
|
|
3922
|
-
kind: 'edit-backup
|
|
4122
|
+
kind: 'edit-backup',
|
|
3923
4123
|
backupId: backup.id,
|
|
3924
4124
|
backupName: backup.name,
|
|
3925
4125
|
notes: backup.notes ?? '',
|
|
@@ -3929,8 +4129,8 @@ export function openEditBackupNotesModal(backupId) {
|
|
|
3929
4129
|
emitChange();
|
|
3930
4130
|
}
|
|
3931
4131
|
|
|
3932
|
-
export async function
|
|
3933
|
-
if (state.modal?.kind !== 'edit-backup
|
|
4132
|
+
export async function submitEditBackupConfirmation({ name = '', notes = '' } = {}) {
|
|
4133
|
+
if (state.modal?.kind !== 'edit-backup') {
|
|
3934
4134
|
return null;
|
|
3935
4135
|
}
|
|
3936
4136
|
|
|
@@ -3939,12 +4139,12 @@ export async function submitEditBackupNotesConfirmation(notes = '') {
|
|
|
3939
4139
|
state.backups.operationLoading = true;
|
|
3940
4140
|
|
|
3941
4141
|
try {
|
|
3942
|
-
const response = await api.updateBackup(backupId, { notes });
|
|
4142
|
+
const response = await api.updateBackup(backupId, { name, notes });
|
|
3943
4143
|
state.backups.items = state.backups.items.map(item =>
|
|
3944
4144
|
String(item.id) === String(response.data.id) ? response.data : item
|
|
3945
4145
|
);
|
|
3946
4146
|
closeModalInternal();
|
|
3947
|
-
pushToast(response.message || 'Backup
|
|
4147
|
+
pushToast(response.message || 'Backup updated.', 'success');
|
|
3948
4148
|
return response.data;
|
|
3949
4149
|
} catch (error) {
|
|
3950
4150
|
withModalError(error);
|
|
@@ -3986,6 +4186,9 @@ export async function submitDeleteBackupConfirmation() {
|
|
|
3986
4186
|
try {
|
|
3987
4187
|
const response = await api.deleteBackup(backupId);
|
|
3988
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
|
+
}
|
|
3989
4192
|
closeModalInternal();
|
|
3990
4193
|
pushToast(response.message || 'Backup deleted.', 'muted');
|
|
3991
4194
|
return true;
|
|
@@ -4016,6 +4219,75 @@ export async function downloadBackup(backupId) {
|
|
|
4016
4219
|
}
|
|
4017
4220
|
}
|
|
4018
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
|
+
|
|
4019
4291
|
export function openRestoreBackupModal(backupId) {
|
|
4020
4292
|
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
4021
4293
|
|
|
@@ -4444,6 +4716,214 @@ export async function selectStructureEntry(name) {
|
|
|
4444
4716
|
await loadStructureDetail(++routeLoadVersion);
|
|
4445
4717
|
}
|
|
4446
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
|
+
|
|
4447
4927
|
export function toggleStructureTablesPanel() {
|
|
4448
4928
|
state.structure.tablesVisible = state.structure.tablesVisible === false;
|
|
4449
4929
|
storeBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, state.structure.tablesVisible);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const EMAIL_PATTERN = /^[^\s@<>"']+@[^\s@<>"']+\.[^\s@<>"']+$/;
|
|
2
|
+
|
|
3
|
+
export function detectEmailValue(value) {
|
|
4
|
+
if (typeof value !== "string") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const text = value.trim();
|
|
9
|
+
|
|
10
|
+
if (!text || text.length > 320 || !EMAIL_PATTERN.test(text)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const atIndex = text.lastIndexOf("@");
|
|
15
|
+
const localPart = text.slice(0, atIndex);
|
|
16
|
+
const domain = text.slice(atIndex + 1);
|
|
17
|
+
|
|
18
|
+
if (!localPart || !domain || domain.startsWith(".") || domain.endsWith(".")) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
type: "email",
|
|
24
|
+
value: text,
|
|
25
|
+
localPart,
|
|
26
|
+
domain,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -83,8 +83,24 @@ function renderFallbackMarkdown(markdown = '') {
|
|
|
83
83
|
return html.join('\n');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function escapeMarkdownHtmlOutsideFences(markdown = '') {
|
|
87
|
+
const lines = String(markdown ?? '').split('\n');
|
|
88
|
+
let inFence = false;
|
|
89
|
+
|
|
90
|
+
return lines
|
|
91
|
+
.map(line => {
|
|
92
|
+
if (FENCED_CODE_PATTERN.test(line)) {
|
|
93
|
+
inFence = !inFence;
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return inFence ? line : escapeHtml(line);
|
|
98
|
+
})
|
|
99
|
+
.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
function renderMarkdownBlock(markdown = '') {
|
|
87
|
-
const source =
|
|
103
|
+
const source = escapeMarkdownHtmlOutsideFences(markdown);
|
|
88
104
|
const markedRuntime = globalThis.marked;
|
|
89
105
|
const parser = typeof markedRuntime?.parse === 'function' ? markedRuntime.parse.bind(markedRuntime) : null;
|
|
90
106
|
|