sqlite-hub 2.1.0 → 2.2.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 +7 -0
- package/docs/MCP.md +2 -2
- package/docs/changelog.md +6 -0
- package/docs/todo.md +2 -0
- package/frontend/js/api.js +7 -0
- package/frontend/js/app.js +329 -7
- package/frontend/js/components/modal.js +152 -2
- package/frontend/js/store.js +1332 -219
- package/frontend/js/utils/markdownDocuments.js +21 -4
- package/frontend/js/views/connections.js +3 -8
- package/frontend/js/views/data.js +34 -5
- package/frontend/js/views/documents.js +153 -59
- package/frontend/styles/base.css +9 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +12 -0
- package/package.json +1 -1
- package/server/routes/backups.js +37 -1
- package/server/routes/charts.js +28 -0
- package/server/routes/data.js +29 -1
- package/server/routes/documents.js +61 -5
- package/server/routes/settings.js +32 -0
- package/server/routes/tableDesigner.js +20 -1
- package/server/server.js +3 -3
- package/server/services/mcpToolService.js +4 -2
- package/server/services/sqlite/connectionManager.js +2 -0
- package/server/services/sqlite/overviewService.js +1 -0
- package/server/services/storage/appStateStore.js +287 -6
- package/server/utils/fileValidation.js +5 -0
- package/server/utils/userActionLog.js +49 -0
package/frontend/js/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as api from './api.js';
|
|
2
|
-
import { formatCellValue, inferStatusTone, truncateMiddle } from './utils/format.js';
|
|
2
|
+
import { formatBytes, formatCellValue, formatNumber, inferStatusTone, truncateMiddle } from './utils/format.js';
|
|
3
3
|
import {
|
|
4
4
|
addTableDesignerColumn,
|
|
5
5
|
addTableDesignerCheckConstraint,
|
|
@@ -462,6 +462,7 @@ const state = {
|
|
|
462
462
|
},
|
|
463
463
|
documents: {
|
|
464
464
|
items: [],
|
|
465
|
+
folders: [],
|
|
465
466
|
selectedId: null,
|
|
466
467
|
selected: null,
|
|
467
468
|
searchQuery: '',
|
|
@@ -1176,6 +1177,7 @@ function resetChartsState() {
|
|
|
1176
1177
|
function resetDocumentsState() {
|
|
1177
1178
|
documentsLoadVersion += 1;
|
|
1178
1179
|
state.documents.items = [];
|
|
1180
|
+
state.documents.folders = [];
|
|
1179
1181
|
state.documents.selectedId = null;
|
|
1180
1182
|
state.documents.selected = null;
|
|
1181
1183
|
state.documents.searchQuery = '';
|
|
@@ -1211,6 +1213,7 @@ function upsertDocumentSummary(document) {
|
|
|
1211
1213
|
const summary = {
|
|
1212
1214
|
id: document.id,
|
|
1213
1215
|
databaseKey: document.databaseKey,
|
|
1216
|
+
folderId: document.folderId ?? null,
|
|
1214
1217
|
title: document.title,
|
|
1215
1218
|
filename: document.filename,
|
|
1216
1219
|
contentLength: document.contentLength,
|
|
@@ -2170,7 +2173,12 @@ async function loadDocumentDetail(version, documentId) {
|
|
|
2170
2173
|
return;
|
|
2171
2174
|
}
|
|
2172
2175
|
|
|
2173
|
-
const document = response.data ?? null;
|
|
2176
|
+
const document = await updateDocumentMagicSnippetsOnOpen(version, response.data ?? null);
|
|
2177
|
+
|
|
2178
|
+
if (version !== routeLoadVersion) {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2174
2182
|
applyCurrentDocument(document);
|
|
2175
2183
|
upsertDocumentSummary(document);
|
|
2176
2184
|
} catch (error) {
|
|
@@ -2203,6 +2211,7 @@ async function loadDocuments(version, route) {
|
|
|
2203
2211
|
}
|
|
2204
2212
|
|
|
2205
2213
|
state.documents.items = response.data?.items ?? [];
|
|
2214
|
+
state.documents.folders = response.data?.folders ?? [];
|
|
2206
2215
|
state.documents.error = null;
|
|
2207
2216
|
|
|
2208
2217
|
await loadDocumentDetail(version, resolveDocumentSelection(route));
|
|
@@ -2212,6 +2221,7 @@ async function loadDocuments(version, route) {
|
|
|
2212
2221
|
}
|
|
2213
2222
|
|
|
2214
2223
|
state.documents.items = [];
|
|
2224
|
+
state.documents.folders = [];
|
|
2215
2225
|
applyCurrentDocument(null);
|
|
2216
2226
|
state.documents.error = normalizeError(error);
|
|
2217
2227
|
} finally {
|
|
@@ -3552,8 +3562,8 @@ export function toggleDocumentsPane(pane) {
|
|
|
3552
3562
|
}
|
|
3553
3563
|
}
|
|
3554
3564
|
|
|
3555
|
-
function normalizeDocumentInsertionRange(range = null) {
|
|
3556
|
-
const contentLength = String(
|
|
3565
|
+
function normalizeDocumentInsertionRange(range = null, currentContent = state.documents.draftContent) {
|
|
3566
|
+
const contentLength = String(currentContent ?? '').length;
|
|
3557
3567
|
const start = Number(range?.start);
|
|
3558
3568
|
const end = Number(range?.end);
|
|
3559
3569
|
const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
|
|
@@ -3575,7 +3585,7 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
|
|
|
3575
3585
|
return content;
|
|
3576
3586
|
}
|
|
3577
3587
|
|
|
3578
|
-
const normalizedRange = normalizeDocumentInsertionRange(range);
|
|
3588
|
+
const normalizedRange = normalizeDocumentInsertionRange(range, content);
|
|
3579
3589
|
const before = content.slice(0, normalizedRange.start);
|
|
3580
3590
|
const after = content.slice(normalizedRange.end);
|
|
3581
3591
|
const prefix = before && !before.endsWith('\n\n') ? (before.endsWith('\n') ? '\n' : '\n\n') : '';
|
|
@@ -3584,294 +3594,1397 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
|
|
|
3584
3594
|
return `${before}${prefix}${text}${suffix}${after}`;
|
|
3585
3595
|
}
|
|
3586
3596
|
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3597
|
+
const DOCUMENT_METADATA_MAGIC_SNIPPET_ID = 'document-metadata';
|
|
3598
|
+
const DATABASE_INFO_MAGIC_SNIPPET_ID = 'database-info';
|
|
3599
|
+
const TABLE_DEFINITION_MAGIC_SNIPPET_ID = 'table-definition';
|
|
3600
|
+
const DOCUMENT_METADATA_MAGIC_SNIPPET_PATTERN =
|
|
3601
|
+
/(^|\n)-?[^\S\r\n]*created at:[^\r\n]*(?:\r?\n)-?[^\S\r\n]*last modified:[^\r\n]*/i;
|
|
3602
|
+
const DATABASE_INFO_MAGIC_SNIPPET_START = '<!-- sqlite-hub:magic database-info -->';
|
|
3603
|
+
const DATABASE_INFO_MAGIC_SNIPPET_END = '<!-- /sqlite-hub:magic database-info -->';
|
|
3604
|
+
const DATABASE_INFO_MAGIC_SNIPPET_MARKED_PATTERN =
|
|
3605
|
+
/(^|\n)<!-- sqlite-hub:magic database-info -->[\s\S]*?<!-- \/sqlite-hub:magic database-info -->/i;
|
|
3606
|
+
const DATABASE_INFO_MAGIC_SNIPPET_LEGACY_PATTERN =
|
|
3607
|
+
/(^|\n)## Database Info[^\S\r\n]*(?:\r?\n){2}- Database Size:[^\r\n]*(?:\r?\n)- Estimated pages:[^\r\n]*(?:\r?\n)- Tables:[^\r\n]*(?:\r?\n)- Journal Mode:[^\r\n]*/i;
|
|
3608
|
+
const DATABASE_INFO_MAGIC_SNIPPET_PATTERN =
|
|
3609
|
+
/(^|\n)(?:<!-- sqlite-hub:magic database-info -->[\s\S]*?<!-- \/sqlite-hub:magic database-info -->|## Database Info[^\S\r\n]*(?:\r?\n){2}- Database Size:[^\r\n]*(?:\r?\n)- Estimated pages:[^\r\n]*(?:\r?\n)- Tables:[^\r\n]*(?:\r?\n)- Journal Mode:[^\r\n]*)/i;
|
|
3610
|
+
const TABLE_DEFINITION_MAGIC_SNIPPET_PATTERN =
|
|
3611
|
+
/(^|\n)<!-- sqlite-hub:magic table-definition ([^\r\n]+) -->[\s\S]*?<!-- \/sqlite-hub:magic table-definition -->/i;
|
|
3612
|
+
const TABLE_DEFINITION_MAGIC_SNIPPET_BLOCK_PATTERN =
|
|
3613
|
+
/(^|\n)<!-- sqlite-hub:magic table-definition ([^\r\n]+) -->[\s\S]*?<!-- \/sqlite-hub:magic table-definition -->/gi;
|
|
3614
|
+
const TABLE_DEFINITION_SAMPLE_ROW_COUNTS = new Set([3, 5, 10]);
|
|
3615
|
+
const TABLE_DEFINITION_DEFAULT_SAMPLE_ROW_COUNT = 5;
|
|
3616
|
+
const TABLE_DEFINITION_LONG_TEXT_LIMIT = 120;
|
|
3617
|
+
|
|
3618
|
+
function buildDocumentMetadataMagicSnippet(context = {}) {
|
|
3619
|
+
const document = context.document ?? {};
|
|
3620
|
+
const createdAt = String(context.databaseCreatedAt ?? context.createdAt ?? document.createdAt ?? '').trim();
|
|
3621
|
+
const lastModifiedAt = String(
|
|
3622
|
+
context.lastModifiedAt ?? context.latestActivityLogTimestamp ?? document.updatedAt ?? '',
|
|
3623
|
+
).trim();
|
|
3624
|
+
|
|
3625
|
+
if (!createdAt || !lastModifiedAt) {
|
|
3626
|
+
return '';
|
|
3590
3627
|
}
|
|
3591
3628
|
|
|
3592
|
-
|
|
3629
|
+
return [
|
|
3630
|
+
`- created at: ${createdAt}`,
|
|
3631
|
+
`- last modified: ${lastModifiedAt}`,
|
|
3632
|
+
].join('\n');
|
|
3633
|
+
}
|
|
3593
3634
|
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3635
|
+
function unwrapDatabaseInfoMagicSnippet(markdown = '') {
|
|
3636
|
+
const value = String(markdown ?? '').trim();
|
|
3637
|
+
const match = value.match(
|
|
3638
|
+
/^<!-- sqlite-hub:magic database-info -->\s*([\s\S]*?)\s*<!-- \/sqlite-hub:magic database-info -->$/i,
|
|
3639
|
+
);
|
|
3597
3640
|
|
|
3598
|
-
|
|
3599
|
-
state.documents.dirty = true;
|
|
3600
|
-
state.documents.saveError = null;
|
|
3601
|
-
emitChange();
|
|
3602
|
-
return true;
|
|
3641
|
+
return String(match?.[1] ?? value).trim();
|
|
3603
3642
|
}
|
|
3604
3643
|
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
|
|
3644
|
+
function wrapDatabaseInfoMagicSnippet(body = '') {
|
|
3645
|
+
const normalizedBody = String(body ?? '').trim();
|
|
3608
3646
|
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
inserted,
|
|
3612
|
-
saved: false,
|
|
3613
|
-
};
|
|
3647
|
+
if (!normalizedBody) {
|
|
3648
|
+
return '';
|
|
3614
3649
|
}
|
|
3615
3650
|
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
saved: false,
|
|
3623
|
-
};
|
|
3624
|
-
}
|
|
3651
|
+
return [
|
|
3652
|
+
DATABASE_INFO_MAGIC_SNIPPET_START,
|
|
3653
|
+
normalizedBody,
|
|
3654
|
+
DATABASE_INFO_MAGIC_SNIPPET_END,
|
|
3655
|
+
].join('\n');
|
|
3656
|
+
}
|
|
3625
3657
|
|
|
3626
|
-
|
|
3627
|
-
const
|
|
3658
|
+
function buildDatabaseInfoMagicSnippetBody(context = {}) {
|
|
3659
|
+
const providedMarkdown = unwrapDatabaseInfoMagicSnippet(context.databaseInfoMarkdown);
|
|
3628
3660
|
|
|
3629
|
-
if (
|
|
3630
|
-
return
|
|
3631
|
-
documentId: null,
|
|
3632
|
-
inserted: false,
|
|
3633
|
-
saved: false,
|
|
3634
|
-
};
|
|
3661
|
+
if (providedMarkdown) {
|
|
3662
|
+
return providedMarkdown;
|
|
3635
3663
|
}
|
|
3636
3664
|
|
|
3637
|
-
const
|
|
3665
|
+
const overview = context.overview;
|
|
3638
3666
|
|
|
3639
|
-
if (
|
|
3640
|
-
return
|
|
3641
|
-
documentId: document.id,
|
|
3642
|
-
inserted: false,
|
|
3643
|
-
saved: false,
|
|
3644
|
-
};
|
|
3667
|
+
if (!overview) {
|
|
3668
|
+
return '';
|
|
3645
3669
|
}
|
|
3646
3670
|
|
|
3647
|
-
const
|
|
3648
|
-
content: nextContent,
|
|
3649
|
-
});
|
|
3650
|
-
const updatedDocument = updateResponse.data ?? document;
|
|
3651
|
-
|
|
3652
|
-
lastOpenDocumentId = updatedDocument.id;
|
|
3653
|
-
upsertDocumentSummary(updatedDocument);
|
|
3671
|
+
const estimatedSizeBytes = overview.estimatedSizeBytes || overview.file?.sizeBytes || 0;
|
|
3654
3672
|
|
|
3655
|
-
return
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3673
|
+
return [
|
|
3674
|
+
'## Database Info',
|
|
3675
|
+
'',
|
|
3676
|
+
`- Database Size: ${formatBytes(overview.file?.sizeBytes ?? estimatedSizeBytes)}`,
|
|
3677
|
+
`- Estimated pages: ${formatNumber(overview.sqlite?.pageCount ?? 0)}`,
|
|
3678
|
+
`- Tables: ${formatNumber(overview.counts?.tables ?? 0)}`,
|
|
3679
|
+
`- Journal Mode: ${String(overview.sqlite?.journalMode ?? 'n/a').toUpperCase()}`,
|
|
3680
|
+
].join('\n');
|
|
3660
3681
|
}
|
|
3661
3682
|
|
|
3662
|
-
function
|
|
3663
|
-
return
|
|
3683
|
+
function buildDatabaseInfoMagicSnippet(context = {}) {
|
|
3684
|
+
return wrapDatabaseInfoMagicSnippet(buildDatabaseInfoMagicSnippetBody(context));
|
|
3664
3685
|
}
|
|
3665
3686
|
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
return;
|
|
3670
|
-
}
|
|
3687
|
+
function buildDatabaseInfoMarkdown(overview = {}) {
|
|
3688
|
+
return buildDatabaseInfoMagicSnippetBody({ overview });
|
|
3689
|
+
}
|
|
3671
3690
|
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
documentId: state.documents.selectedId,
|
|
3675
|
-
insertionRange: normalizeDocumentInsertionRange(insertionRange),
|
|
3676
|
-
queries: [],
|
|
3677
|
-
selectedHistoryId: '',
|
|
3678
|
-
loading: true,
|
|
3679
|
-
error: null,
|
|
3680
|
-
submitting: false,
|
|
3681
|
-
};
|
|
3682
|
-
emitChange();
|
|
3691
|
+
function normalizeTableDefinitionSampleRowCount(value) {
|
|
3692
|
+
const numericValue = Number(value);
|
|
3683
3693
|
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
limit: 100,
|
|
3689
|
-
});
|
|
3690
|
-
const loadedQueries = response.data?.items ?? [];
|
|
3691
|
-
const queries =
|
|
3692
|
-
kind === 'document-insert-note'
|
|
3693
|
-
? loadedQueries.filter(query => String(query.notes ?? '').trim())
|
|
3694
|
-
: loadedQueries;
|
|
3694
|
+
return TABLE_DEFINITION_SAMPLE_ROW_COUNTS.has(numericValue)
|
|
3695
|
+
? numericValue
|
|
3696
|
+
: TABLE_DEFINITION_DEFAULT_SAMPLE_ROW_COUNT;
|
|
3697
|
+
}
|
|
3695
3698
|
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
+
function normalizeTableDefinitionOptions(options = {}) {
|
|
3700
|
+
const tableName = String(options.tableName ?? options.table ?? '').trim();
|
|
3701
|
+
const markdownTable = Boolean(options.markdownTable ?? options.includeMarkdownTable ?? false);
|
|
3702
|
+
const sqlDefinition = Boolean(options.sqlDefinition ?? options.includeSqlDefinition ?? false);
|
|
3703
|
+
const sampleData = Boolean(options.sampleData ?? options.includeSampleData ?? false);
|
|
3699
3704
|
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
}
|
|
3705
|
+
return {
|
|
3706
|
+
tableName,
|
|
3707
|
+
markdownTable,
|
|
3708
|
+
sqlDefinition,
|
|
3709
|
+
sampleData,
|
|
3710
|
+
sampleRowCount: normalizeTableDefinitionSampleRowCount(options.sampleRowCount),
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3709
3713
|
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
emitChange();
|
|
3713
|
-
}
|
|
3714
|
+
function normalizeTableDefinitionTableKey(tableName) {
|
|
3715
|
+
return String(tableName ?? '').trim().toLowerCase();
|
|
3714
3716
|
}
|
|
3715
3717
|
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
return;
|
|
3719
|
-
}
|
|
3718
|
+
function getTableDefinitionSnippetKey(options = {}) {
|
|
3719
|
+
const normalized = normalizeTableDefinitionOptions(options);
|
|
3720
3720
|
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3721
|
+
return JSON.stringify({
|
|
3722
|
+
tableName: normalizeTableDefinitionTableKey(normalized.tableName),
|
|
3723
|
+
markdownTable: normalized.markdownTable,
|
|
3724
|
+
sqlDefinition: normalized.sqlDefinition,
|
|
3725
|
+
sampleData: normalized.sampleData,
|
|
3726
|
+
sampleRowCount: normalized.sampleRowCount,
|
|
3727
|
+
});
|
|
3724
3728
|
}
|
|
3725
3729
|
|
|
3726
|
-
function
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
return (modal?.queries ?? []).find(query => String(query.id) === selectedHistoryId) ?? null;
|
|
3730
|
+
function encodeDocumentMagicSnippetMetadata(metadata = {}) {
|
|
3731
|
+
return encodeURIComponent(JSON.stringify(metadata));
|
|
3730
3732
|
}
|
|
3731
3733
|
|
|
3732
|
-
function
|
|
3733
|
-
|
|
3734
|
-
return;
|
|
3734
|
+
function decodeDocumentMagicSnippetMetadata(encodedMetadata = '') {
|
|
3735
|
+
try {
|
|
3736
|
+
return JSON.parse(decodeURIComponent(String(encodedMetadata ?? '').trim()));
|
|
3737
|
+
} catch {
|
|
3738
|
+
return null;
|
|
3735
3739
|
}
|
|
3736
|
-
|
|
3737
|
-
state.modal.error = { code, message };
|
|
3738
|
-
state.modal.submitting = false;
|
|
3739
|
-
emitChange();
|
|
3740
3740
|
}
|
|
3741
3741
|
|
|
3742
|
-
function
|
|
3743
|
-
|
|
3744
|
-
return false;
|
|
3745
|
-
}
|
|
3742
|
+
function buildTableDefinitionMagicSnippetMetadata(options = {}) {
|
|
3743
|
+
const normalized = normalizeTableDefinitionOptions(options);
|
|
3746
3744
|
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3745
|
+
return {
|
|
3746
|
+
type: TABLE_DEFINITION_MAGIC_SNIPPET_ID,
|
|
3747
|
+
version: 1,
|
|
3748
|
+
tableName: normalized.tableName,
|
|
3749
|
+
markdownTable: normalized.markdownTable,
|
|
3750
|
+
sqlDefinition: normalized.sqlDefinition,
|
|
3751
|
+
sampleData: normalized.sampleData,
|
|
3752
|
+
sampleRowCount: normalized.sampleRowCount,
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3751
3755
|
|
|
3752
|
-
|
|
3756
|
+
function normalizeMarkdownInlineText(value = '') {
|
|
3757
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
3753
3758
|
}
|
|
3754
3759
|
|
|
3755
|
-
|
|
3756
|
-
|
|
3760
|
+
function escapeMarkdownTableCell(value = '') {
|
|
3761
|
+
return String(value ?? '')
|
|
3762
|
+
.replace(/\r?\n/g, ' ')
|
|
3763
|
+
.replace(/\|/g, '\\|')
|
|
3764
|
+
.trim();
|
|
3757
3765
|
}
|
|
3758
3766
|
|
|
3759
|
-
|
|
3760
|
-
|
|
3767
|
+
function escapeRegExpText(value = '') {
|
|
3768
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3761
3769
|
}
|
|
3762
3770
|
|
|
3763
|
-
|
|
3764
|
-
const
|
|
3771
|
+
function expressionReferencesColumn(expression = '', columnName = '') {
|
|
3772
|
+
const normalizedColumn = String(columnName ?? '').trim();
|
|
3765
3773
|
|
|
3766
|
-
if (!
|
|
3774
|
+
if (!normalizedColumn) {
|
|
3767
3775
|
return false;
|
|
3768
3776
|
}
|
|
3769
3777
|
|
|
3770
|
-
|
|
3778
|
+
return new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExpText(normalizedColumn)}([^A-Za-z0-9_$]|$)`, 'i').test(
|
|
3779
|
+
String(expression ?? ''),
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3771
3782
|
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3783
|
+
function tableColumnHasUniqueConstraint(table = {}, columnName = '') {
|
|
3784
|
+
return (table.indexes ?? []).some(index => {
|
|
3785
|
+
if (!index?.unique) {
|
|
3786
|
+
return false;
|
|
3787
|
+
}
|
|
3776
3788
|
|
|
3777
|
-
|
|
3789
|
+
const indexColumns = (index.columns ?? [])
|
|
3790
|
+
.map(column => String(column.name ?? '').trim())
|
|
3791
|
+
.filter(Boolean);
|
|
3778
3792
|
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3793
|
+
return indexColumns.length === 1 && indexColumns[0] === columnName;
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3782
3796
|
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3797
|
+
function tableColumnHasAutoincrement(table = {}, column = {}) {
|
|
3798
|
+
return Boolean(column.primaryKeyPosition) && /\bAUTOINCREMENT\b/i.test(String(table.ddl ?? ''));
|
|
3799
|
+
}
|
|
3786
3800
|
|
|
3787
|
-
|
|
3801
|
+
function getColumnForeignKeyRestrictions(table = {}, columnName = '') {
|
|
3802
|
+
return (table.foreignKeys ?? [])
|
|
3803
|
+
.flatMap(foreignKey =>
|
|
3804
|
+
(foreignKey.mappings ?? [])
|
|
3805
|
+
.filter(mapping => mapping.from === columnName)
|
|
3806
|
+
.map(mapping => {
|
|
3807
|
+
const referencedColumn = String(mapping.to ?? '').trim();
|
|
3808
|
+
const columnSuffix = referencedColumn ? `(${referencedColumn})` : '';
|
|
3788
3809
|
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
return inserted;
|
|
3794
|
-
} catch (error) {
|
|
3795
|
-
withModalError(error);
|
|
3796
|
-
return false;
|
|
3797
|
-
}
|
|
3810
|
+
return `REFERENCES ${foreignKey.referencedTable}${columnSuffix}`;
|
|
3811
|
+
}),
|
|
3812
|
+
)
|
|
3813
|
+
.filter(Boolean);
|
|
3798
3814
|
}
|
|
3799
3815
|
|
|
3800
|
-
|
|
3801
|
-
const
|
|
3816
|
+
function buildColumnRestrictionText(table = {}, column = {}) {
|
|
3817
|
+
const restrictions = [];
|
|
3802
3818
|
|
|
3803
|
-
if (
|
|
3804
|
-
|
|
3819
|
+
if (Number(column.primaryKeyPosition ?? 0) > 0) {
|
|
3820
|
+
restrictions.push('PRIMARY KEY');
|
|
3805
3821
|
}
|
|
3806
3822
|
|
|
3807
|
-
|
|
3808
|
-
|
|
3823
|
+
if (tableColumnHasAutoincrement(table, column)) {
|
|
3824
|
+
restrictions.push('AUTOINCREMENT');
|
|
3825
|
+
}
|
|
3809
3826
|
|
|
3810
|
-
if (
|
|
3811
|
-
|
|
3812
|
-
return false;
|
|
3827
|
+
if (column.notNull) {
|
|
3828
|
+
restrictions.push('NOT NULL');
|
|
3813
3829
|
}
|
|
3814
3830
|
|
|
3815
|
-
|
|
3816
|
-
|
|
3831
|
+
if (tableColumnHasUniqueConstraint(table, column.name)) {
|
|
3832
|
+
restrictions.push('UNIQUE');
|
|
3833
|
+
}
|
|
3817
3834
|
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
pushToast(`Inserted note from "${getDocumentInsertQueryTitle(query)}".`, 'success');
|
|
3835
|
+
if (column.defaultValue !== null && column.defaultValue !== undefined && String(column.defaultValue).trim()) {
|
|
3836
|
+
restrictions.push(`DEFAULT ${String(column.defaultValue).trim()}`);
|
|
3821
3837
|
}
|
|
3822
|
-
|
|
3838
|
+
|
|
3839
|
+
(table.checkConstraints ?? [])
|
|
3840
|
+
.filter(constraint => expressionReferencesColumn(constraint.expression, column.name))
|
|
3841
|
+
.forEach(constraint => restrictions.push(`CHECK (${String(constraint.expression ?? '').trim()})`));
|
|
3842
|
+
|
|
3843
|
+
restrictions.push(...getColumnForeignKeyRestrictions(table, column.name));
|
|
3844
|
+
|
|
3845
|
+
return restrictions.join(', ');
|
|
3823
3846
|
}
|
|
3824
3847
|
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
emitChange();
|
|
3848
|
+
function getVisibleTableDefinitionColumns(table = {}) {
|
|
3849
|
+
return (table.columns ?? []).filter(column => column.visible !== false && !column.generated);
|
|
3850
|
+
}
|
|
3829
3851
|
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
const document = response.data ?? null;
|
|
3852
|
+
function buildTableDefinitionMarkdownTable(table = {}) {
|
|
3853
|
+
const rows = getVisibleTableDefinitionColumns(table).map(column => [
|
|
3854
|
+
escapeMarkdownTableCell(column.name),
|
|
3855
|
+
escapeMarkdownTableCell(column.declaredType || column.affinity || ''),
|
|
3856
|
+
escapeMarkdownTableCell(buildColumnRestrictionText(table, column)),
|
|
3857
|
+
]);
|
|
3837
3858
|
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
}
|
|
3859
|
+
return [
|
|
3860
|
+
'| Column | Type | Restrictions |',
|
|
3861
|
+
'|---|---|---|',
|
|
3862
|
+
...rows.map(row => `| ${row.join(' | ')} |`),
|
|
3863
|
+
].join('\n');
|
|
3864
|
+
}
|
|
3842
3865
|
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
}
|
|
3866
|
+
function normalizeSqlDefinition(sql = '') {
|
|
3867
|
+
const statement = String(sql ?? '').trim();
|
|
3846
3868
|
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
state.documents.saveError = normalizeError(error);
|
|
3850
|
-
if (options.toast !== false) {
|
|
3851
|
-
pushToast(state.documents.saveError.message || 'Document could not be created.', 'alert');
|
|
3852
|
-
}
|
|
3853
|
-
return null;
|
|
3854
|
-
} finally {
|
|
3855
|
-
state.documents.saving = false;
|
|
3856
|
-
emitChange();
|
|
3869
|
+
if (!statement) {
|
|
3870
|
+
return '';
|
|
3857
3871
|
}
|
|
3872
|
+
|
|
3873
|
+
return /;\s*$/.test(statement) ? statement : `${statement};`;
|
|
3858
3874
|
}
|
|
3859
3875
|
|
|
3860
|
-
|
|
3861
|
-
const
|
|
3876
|
+
function buildTableDefinitionSqlBlock(table = {}) {
|
|
3877
|
+
const statement = normalizeSqlDefinition(table.ddl);
|
|
3862
3878
|
|
|
3863
|
-
if (!
|
|
3864
|
-
return
|
|
3879
|
+
if (!statement) {
|
|
3880
|
+
return '';
|
|
3865
3881
|
}
|
|
3866
3882
|
|
|
3867
|
-
|
|
3868
|
-
|
|
3883
|
+
return ['```sql', statement, '```'].join('\n');
|
|
3884
|
+
}
|
|
3869
3885
|
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3886
|
+
function formatTableDefinitionSampleValue(value) {
|
|
3887
|
+
if (value && typeof value === 'object' && value.__type === 'blob') {
|
|
3888
|
+
return `[BLOB - ${formatBytes(value.sizeBytes ?? 0)}]`;
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
if (value === null) {
|
|
3892
|
+
return 'NULL';
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
if (value === undefined) {
|
|
3896
|
+
return '';
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
const rawText =
|
|
3900
|
+
typeof value === 'object'
|
|
3901
|
+
? JSON.stringify(value)
|
|
3902
|
+
: String(value);
|
|
3903
|
+
const normalizedText = rawText.replace(/\s+/g, ' ').trim();
|
|
3904
|
+
|
|
3905
|
+
return normalizedText.length > TABLE_DEFINITION_LONG_TEXT_LIMIT
|
|
3906
|
+
? `${normalizedText.slice(0, TABLE_DEFINITION_LONG_TEXT_LIMIT - 3)}...`
|
|
3907
|
+
: normalizedText;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
function isNumericTableDefinitionColumn(table = {}, columnName = '') {
|
|
3911
|
+
const column = (table.columns ?? []).find(candidate => candidate.name === columnName);
|
|
3912
|
+
const affinity = String(column?.affinity ?? column?.declaredType ?? '').toUpperCase();
|
|
3913
|
+
|
|
3914
|
+
return ['INTEGER', 'REAL', 'NUMERIC'].includes(affinity);
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
function buildTableDefinitionSampleDataMarkdown(table = {}, sampleData = {}) {
|
|
3918
|
+
const columns = (sampleData.columns ?? getVisibleTableDefinitionColumns(table).map(column => column.name))
|
|
3919
|
+
.filter(columnName => columnName !== '__identity__');
|
|
3920
|
+
const rows = (sampleData.rows ?? []).slice(0, normalizeTableDefinitionSampleRowCount(sampleData.limit));
|
|
3921
|
+
|
|
3922
|
+
if (!rows.length) {
|
|
3923
|
+
return ['### Sample Data', '', '_No rows available._'].join('\n');
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
const header = `| ${columns.map(escapeMarkdownTableCell).join(' | ')} |`;
|
|
3927
|
+
const alignment = `| ${columns
|
|
3928
|
+
.map(columnName => (isNumericTableDefinitionColumn(table, columnName) ? '---:' : '---'))
|
|
3929
|
+
.join(' | ')} |`;
|
|
3930
|
+
const rowLines = rows.map(row => {
|
|
3931
|
+
const cells = columns.map(columnName => escapeMarkdownTableCell(formatTableDefinitionSampleValue(row[columnName])));
|
|
3932
|
+
|
|
3933
|
+
return `| ${cells.join(' | ')} |`;
|
|
3934
|
+
});
|
|
3935
|
+
|
|
3936
|
+
return ['### Sample Data', '', header, alignment, ...rowLines].join('\n');
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
function buildMissingTableDefinitionSnippetBody(tableName, message = 'Table no longer exists.') {
|
|
3940
|
+
return [`## Definition: ${normalizeMarkdownInlineText(tableName || 'Table')}`, '', message].join('\n');
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
function buildTableDefinitionSnippetBody({ table = null, sampleData = null, options = {}, errorMessage = '' } = {}) {
|
|
3944
|
+
const normalizedOptions = normalizeTableDefinitionOptions(options);
|
|
3945
|
+
|
|
3946
|
+
if (!table || errorMessage) {
|
|
3947
|
+
return buildMissingTableDefinitionSnippetBody(normalizedOptions.tableName, errorMessage || 'Table no longer exists.');
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
if (table.isShadow) {
|
|
3951
|
+
return buildMissingTableDefinitionSnippetBody(
|
|
3952
|
+
normalizedOptions.tableName,
|
|
3953
|
+
'Shadow tables are not available for table definitions.',
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
const sections = [`## Definition: ${normalizeMarkdownInlineText(table.name ?? normalizedOptions.tableName)}`];
|
|
3958
|
+
|
|
3959
|
+
if (normalizedOptions.markdownTable) {
|
|
3960
|
+
sections.push('', buildTableDefinitionMarkdownTable(table));
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
if (normalizedOptions.sqlDefinition) {
|
|
3964
|
+
const sqlBlock = buildTableDefinitionSqlBlock(table);
|
|
3965
|
+
|
|
3966
|
+
if (sqlBlock) {
|
|
3967
|
+
sections.push('', sqlBlock);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
if (normalizedOptions.sampleData) {
|
|
3972
|
+
sections.push('', buildTableDefinitionSampleDataMarkdown(table, sampleData ?? {}));
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
if (sections.length === 1) {
|
|
3976
|
+
sections.push('', '_No table definition options are enabled._');
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
return sections.join('\n');
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
export function buildTableDefinitionMagicSnippet(context = {}) {
|
|
3983
|
+
const options = normalizeTableDefinitionOptions(context.options ?? context);
|
|
3984
|
+
const metadata = buildTableDefinitionMagicSnippetMetadata(options);
|
|
3985
|
+
const body = buildTableDefinitionSnippetBody({
|
|
3986
|
+
table: context.table ?? null,
|
|
3987
|
+
sampleData: context.sampleData ?? null,
|
|
3988
|
+
options,
|
|
3989
|
+
errorMessage: context.errorMessage ?? '',
|
|
3990
|
+
});
|
|
3991
|
+
|
|
3992
|
+
if (!options.tableName) {
|
|
3993
|
+
return '';
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
return [
|
|
3997
|
+
`<!-- sqlite-hub:magic table-definition ${encodeDocumentMagicSnippetMetadata(metadata)} -->`,
|
|
3998
|
+
body,
|
|
3999
|
+
'<!-- /sqlite-hub:magic table-definition -->',
|
|
4000
|
+
].join('\n');
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
export function getDocumentTableDefinitionMagicSnippets(content = '') {
|
|
4004
|
+
TABLE_DEFINITION_MAGIC_SNIPPET_BLOCK_PATTERN.lastIndex = 0;
|
|
4005
|
+
|
|
4006
|
+
return Array.from(String(content ?? '').matchAll(TABLE_DEFINITION_MAGIC_SNIPPET_BLOCK_PATTERN))
|
|
4007
|
+
.map(match => {
|
|
4008
|
+
const metadata = decodeDocumentMagicSnippetMetadata(match[2]);
|
|
4009
|
+
const options = normalizeTableDefinitionOptions(metadata ?? {});
|
|
4010
|
+
|
|
4011
|
+
return metadata && options.tableName
|
|
4012
|
+
? {
|
|
4013
|
+
metadata,
|
|
4014
|
+
options,
|
|
4015
|
+
key: getTableDefinitionSnippetKey(options),
|
|
4016
|
+
tableKey: normalizeTableDefinitionTableKey(options.tableName),
|
|
4017
|
+
}
|
|
4018
|
+
: null;
|
|
4019
|
+
})
|
|
4020
|
+
.filter(Boolean);
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
export function hasDocumentTableDefinitionMagicSnippet(content = '', tableName = '') {
|
|
4024
|
+
const tableKey = normalizeTableDefinitionTableKey(tableName);
|
|
4025
|
+
|
|
4026
|
+
if (!tableKey) {
|
|
4027
|
+
return false;
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
return getDocumentTableDefinitionMagicSnippets(content).some(snippet => snippet.tableKey === tableKey);
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
async function buildTableDefinitionMagicSnippetFromOptions(options = {}) {
|
|
4034
|
+
const normalizedOptions = normalizeTableDefinitionOptions(options);
|
|
4035
|
+
|
|
4036
|
+
if (!normalizedOptions.tableName) {
|
|
4037
|
+
return '';
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
try {
|
|
4041
|
+
const structureResponse = await api.getStructureDetail(normalizedOptions.tableName);
|
|
4042
|
+
const table = structureResponse.data ?? null;
|
|
4043
|
+
|
|
4044
|
+
if (!table || table.isShadow) {
|
|
4045
|
+
return buildTableDefinitionMagicSnippet({
|
|
4046
|
+
...normalizedOptions,
|
|
4047
|
+
errorMessage: table?.isShadow
|
|
4048
|
+
? 'Shadow tables are not available for table definitions.'
|
|
4049
|
+
: 'Table no longer exists.',
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
let sampleData = null;
|
|
4054
|
+
|
|
4055
|
+
if (normalizedOptions.sampleData) {
|
|
4056
|
+
const sampleResponse = await api.getDataTable(normalizedOptions.tableName, {
|
|
4057
|
+
limit: normalizedOptions.sampleRowCount,
|
|
4058
|
+
offset: 0,
|
|
4059
|
+
});
|
|
4060
|
+
|
|
4061
|
+
sampleData = sampleResponse.data ?? {};
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
return buildTableDefinitionMagicSnippet({
|
|
4065
|
+
...normalizedOptions,
|
|
4066
|
+
table,
|
|
4067
|
+
sampleData,
|
|
4068
|
+
});
|
|
4069
|
+
} catch {
|
|
4070
|
+
return buildTableDefinitionMagicSnippet({
|
|
4071
|
+
...normalizedOptions,
|
|
4072
|
+
errorMessage: 'Table no longer exists.',
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
async function hydrateDocumentMetadataMagicSnippetContext(context = {}) {
|
|
4078
|
+
return {
|
|
4079
|
+
...context,
|
|
4080
|
+
latestActivityLogTimestamp: await getLatestActivityLogTimestamp(context.document?.updatedAt ?? null),
|
|
4081
|
+
databaseCreatedAt: await getDatabaseCreatedAt(context.databaseCreatedAt ?? null),
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
async function hydrateDatabaseInfoMagicSnippetContext(context = {}) {
|
|
4086
|
+
try {
|
|
4087
|
+
const response = await api.getOverview();
|
|
4088
|
+
|
|
4089
|
+
return {
|
|
4090
|
+
...context,
|
|
4091
|
+
overview: response.data ?? {},
|
|
4092
|
+
};
|
|
4093
|
+
} catch {
|
|
4094
|
+
return context;
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
async function hydrateTableDefinitionMagicSnippetContext(context = {}) {
|
|
4099
|
+
const content = String(context.content ?? context.document?.content ?? '');
|
|
4100
|
+
const snippets = getDocumentTableDefinitionMagicSnippets(content);
|
|
4101
|
+
|
|
4102
|
+
if (!snippets.length) {
|
|
4103
|
+
return context;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
const tableDefinitionSnippets = new Map(context.tableDefinitionSnippets ?? []);
|
|
4107
|
+
|
|
4108
|
+
for (const snippet of snippets) {
|
|
4109
|
+
if (tableDefinitionSnippets.has(snippet.key)) {
|
|
4110
|
+
continue;
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
tableDefinitionSnippets.set(snippet.key, {
|
|
4114
|
+
markdown: await buildTableDefinitionMagicSnippetFromOptions(snippet.options),
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
return {
|
|
4119
|
+
...context,
|
|
4120
|
+
tableDefinitionSnippets,
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
const DOCUMENT_MAGIC_SNIPPETS = [
|
|
4125
|
+
{
|
|
4126
|
+
id: DOCUMENT_METADATA_MAGIC_SNIPPET_ID,
|
|
4127
|
+
pattern: DOCUMENT_METADATA_MAGIC_SNIPPET_PATTERN,
|
|
4128
|
+
build: buildDocumentMetadataMagicSnippet,
|
|
4129
|
+
hydrateContext: hydrateDocumentMetadataMagicSnippetContext,
|
|
4130
|
+
},
|
|
4131
|
+
{
|
|
4132
|
+
id: DATABASE_INFO_MAGIC_SNIPPET_ID,
|
|
4133
|
+
pattern: DATABASE_INFO_MAGIC_SNIPPET_PATTERN,
|
|
4134
|
+
build: buildDatabaseInfoMagicSnippet,
|
|
4135
|
+
hydrateContext: hydrateDatabaseInfoMagicSnippetContext,
|
|
4136
|
+
},
|
|
4137
|
+
{
|
|
4138
|
+
id: TABLE_DEFINITION_MAGIC_SNIPPET_ID,
|
|
4139
|
+
pattern: TABLE_DEFINITION_MAGIC_SNIPPET_PATTERN,
|
|
4140
|
+
hydrateContext: hydrateTableDefinitionMagicSnippetContext,
|
|
4141
|
+
replace(content, context = {}) {
|
|
4142
|
+
if (!context.tableDefinitionSnippets) {
|
|
4143
|
+
return String(content ?? '');
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
TABLE_DEFINITION_MAGIC_SNIPPET_BLOCK_PATTERN.lastIndex = 0;
|
|
4147
|
+
|
|
4148
|
+
return String(content ?? '').replace(
|
|
4149
|
+
TABLE_DEFINITION_MAGIC_SNIPPET_BLOCK_PATTERN,
|
|
4150
|
+
(match, prefix = '', encodedMetadata = '') => {
|
|
4151
|
+
const metadata = decodeDocumentMagicSnippetMetadata(encodedMetadata);
|
|
4152
|
+
const options = normalizeTableDefinitionOptions(metadata ?? {});
|
|
4153
|
+
const key = getTableDefinitionSnippetKey(options);
|
|
4154
|
+
const replacement = context.tableDefinitionSnippets.get(key)?.markdown;
|
|
4155
|
+
|
|
4156
|
+
return replacement ? `${prefix}${replacement}` : match;
|
|
4157
|
+
},
|
|
4158
|
+
);
|
|
4159
|
+
},
|
|
4160
|
+
},
|
|
4161
|
+
];
|
|
4162
|
+
|
|
4163
|
+
function getDocumentMagicSnippet(snippetId) {
|
|
4164
|
+
return DOCUMENT_MAGIC_SNIPPETS.find(snippet => snippet.id === snippetId) ?? null;
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
function hasDocumentMagicSnippet(content, snippet) {
|
|
4168
|
+
return snippet.pattern.test(String(content ?? ''));
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
function replaceDocumentMagicSnippet(content, snippet, context = {}) {
|
|
4172
|
+
const block = String(snippet.build(context) ?? '').trim();
|
|
4173
|
+
|
|
4174
|
+
if (!block || !hasDocumentMagicSnippet(content, snippet)) {
|
|
4175
|
+
return String(content ?? '');
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
return String(content ?? '').replace(snippet.pattern, (match, prefix = '') => `${prefix}${block}`);
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
async function buildDocumentMagicSnippetContext(content, document) {
|
|
4182
|
+
let context = { document, content, databaseCreatedAt: state.connections.active?.createdAt ?? null };
|
|
4183
|
+
|
|
4184
|
+
for (const snippet of DOCUMENT_MAGIC_SNIPPETS) {
|
|
4185
|
+
if (!hasDocumentMagicSnippet(content, snippet) || !snippet.hydrateContext) {
|
|
4186
|
+
continue;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
context = await snippet.hydrateContext(context);
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
return context;
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
export function updateDocumentMagicSnippets(content, context = {}) {
|
|
4196
|
+
return DOCUMENT_MAGIC_SNIPPETS.reduce(
|
|
4197
|
+
(nextContent, snippet) =>
|
|
4198
|
+
typeof snippet.replace === 'function'
|
|
4199
|
+
? snippet.replace(nextContent, context)
|
|
4200
|
+
: replaceDocumentMagicSnippet(nextContent, snippet, context),
|
|
4201
|
+
String(content ?? ''),
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
export function insertDocumentMagicSnippet(currentContent, snippetId, context = {}, range = null) {
|
|
4206
|
+
const content = String(currentContent ?? '');
|
|
4207
|
+
const snippet = getDocumentMagicSnippet(snippetId);
|
|
4208
|
+
|
|
4209
|
+
if (!snippet) {
|
|
4210
|
+
return {
|
|
4211
|
+
alreadyInserted: false,
|
|
4212
|
+
content,
|
|
4213
|
+
inserted: false,
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
if (hasDocumentMagicSnippet(content, snippet)) {
|
|
4218
|
+
return {
|
|
4219
|
+
alreadyInserted: true,
|
|
4220
|
+
content,
|
|
4221
|
+
inserted: false,
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
const block = String(snippet.build(context) ?? '').trim();
|
|
4226
|
+
|
|
4227
|
+
if (!block) {
|
|
4228
|
+
return {
|
|
4229
|
+
alreadyInserted: false,
|
|
4230
|
+
content,
|
|
4231
|
+
inserted: false,
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
return {
|
|
4236
|
+
alreadyInserted: false,
|
|
4237
|
+
content: buildDocumentMarkdownInsertion(content, block, range),
|
|
4238
|
+
inserted: true,
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
export function upsertDocumentMagicSnippet(currentContent, snippetId, context = {}, range = null) {
|
|
4243
|
+
const content = String(currentContent ?? '');
|
|
4244
|
+
const snippet = getDocumentMagicSnippet(snippetId);
|
|
4245
|
+
const block = String(snippet?.build(context) ?? '').trim();
|
|
4246
|
+
|
|
4247
|
+
if (!snippet || !block) {
|
|
4248
|
+
return content;
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
if (hasDocumentMagicSnippet(content, snippet)) {
|
|
4252
|
+
return replaceDocumentMagicSnippet(content, snippet, context);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
return buildDocumentMarkdownInsertion(content, block, range);
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
export function upsertDatabaseInfoMarkdown(currentContent, databaseInfoMarkdown, range = null) {
|
|
4259
|
+
return upsertDocumentMagicSnippet(
|
|
4260
|
+
currentContent,
|
|
4261
|
+
DATABASE_INFO_MAGIC_SNIPPET_ID,
|
|
4262
|
+
{ overview: null, databaseInfoMarkdown },
|
|
4263
|
+
range,
|
|
4264
|
+
);
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
export function getLatestActivityLogTimestampFromResponse(response = {}) {
|
|
4268
|
+
return String(response?.data?.items?.[0]?.occurredAt ?? '').trim() || null;
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
async function getLatestActivityLogTimestamp(fallback = null) {
|
|
4272
|
+
try {
|
|
4273
|
+
const response = await api.getLogs({
|
|
4274
|
+
range: 'all',
|
|
4275
|
+
limit: 1,
|
|
4276
|
+
});
|
|
4277
|
+
|
|
4278
|
+
return getLatestActivityLogTimestampFromResponse(response) ?? fallback;
|
|
4279
|
+
} catch {
|
|
4280
|
+
return fallback;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
async function getDatabaseCreatedAt(fallback = null) {
|
|
4285
|
+
try {
|
|
4286
|
+
const response = await api.getOverview();
|
|
4287
|
+
|
|
4288
|
+
return String(response.data?.file?.createdAt ?? response.data?.connection?.createdAt ?? '').trim() || fallback;
|
|
4289
|
+
} catch {
|
|
4290
|
+
return fallback;
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
async function updateDocumentMagicSnippetsOnOpen(version, document) {
|
|
4295
|
+
if (!document?.id) {
|
|
4296
|
+
return document;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
const context = await buildDocumentMagicSnippetContext(document.content, document);
|
|
4300
|
+
const nextContent = updateDocumentMagicSnippets(document.content, context);
|
|
4301
|
+
|
|
4302
|
+
if (nextContent === String(document.content ?? '')) {
|
|
4303
|
+
return document;
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
try {
|
|
4307
|
+
const response = await api.updateDocument(document.id, {
|
|
4308
|
+
content: nextContent,
|
|
4309
|
+
});
|
|
4310
|
+
|
|
4311
|
+
if (version !== routeLoadVersion) {
|
|
4312
|
+
return null;
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
return response.data ?? { ...document, content: nextContent };
|
|
4316
|
+
} catch {
|
|
4317
|
+
return document;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
export function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
4322
|
+
if (!state.documents.selectedId) {
|
|
4323
|
+
return false;
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
const nextContent = buildDocumentMarkdownInsertion(state.documents.draftContent, markdown, range);
|
|
4327
|
+
|
|
4328
|
+
if (nextContent === state.documents.draftContent) {
|
|
4329
|
+
return false;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
state.documents.draftContent = nextContent;
|
|
4333
|
+
state.documents.dirty = true;
|
|
4334
|
+
state.documents.saveError = null;
|
|
4335
|
+
emitChange();
|
|
4336
|
+
return true;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
export async function insertMarkdownIntoLastOpenDocument(markdown) {
|
|
4340
|
+
if (state.route.name === 'documents' && state.documents.selectedId) {
|
|
4341
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
|
|
4342
|
+
|
|
4343
|
+
return {
|
|
4344
|
+
documentId: state.documents.selectedId,
|
|
4345
|
+
inserted,
|
|
4346
|
+
saved: false,
|
|
4347
|
+
};
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
const documentId = lastOpenDocumentId ?? state.documents.selectedId;
|
|
4351
|
+
|
|
4352
|
+
if (!documentId) {
|
|
4353
|
+
return {
|
|
4354
|
+
documentId: null,
|
|
4355
|
+
inserted: false,
|
|
4356
|
+
saved: false,
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
const response = await api.getDocument(documentId);
|
|
4361
|
+
const document = response.data ?? null;
|
|
4362
|
+
|
|
4363
|
+
if (!document?.id) {
|
|
4364
|
+
return {
|
|
4365
|
+
documentId: null,
|
|
4366
|
+
inserted: false,
|
|
4367
|
+
saved: false,
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
const nextContent = buildDocumentMarkdownInsertion(document.content, markdown);
|
|
4372
|
+
|
|
4373
|
+
if (nextContent === document.content) {
|
|
4374
|
+
return {
|
|
4375
|
+
documentId: document.id,
|
|
4376
|
+
inserted: false,
|
|
4377
|
+
saved: false,
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
const updateResponse = await api.updateDocument(document.id, {
|
|
4382
|
+
content: nextContent,
|
|
4383
|
+
});
|
|
4384
|
+
const updatedDocument = updateResponse.data ?? document;
|
|
4385
|
+
|
|
4386
|
+
lastOpenDocumentId = updatedDocument.id;
|
|
4387
|
+
upsertDocumentSummary(updatedDocument);
|
|
4388
|
+
|
|
4389
|
+
return {
|
|
4390
|
+
documentId: updatedDocument.id,
|
|
4391
|
+
inserted: true,
|
|
4392
|
+
saved: true,
|
|
4393
|
+
};
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
function getDocumentInsertQueryTitle(query) {
|
|
4397
|
+
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
export function buildSavedQueriesMarkdown(queries = []) {
|
|
4401
|
+
const queryTitles = (Array.isArray(queries) ? queries : [])
|
|
4402
|
+
.map(query => String(getDocumentInsertQueryTitle(query)).replace(/\s+/g, ' ').trim())
|
|
4403
|
+
.filter(Boolean);
|
|
4404
|
+
|
|
4405
|
+
if (!queryTitles.length) {
|
|
4406
|
+
return '';
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
return [
|
|
4410
|
+
'## Saved Queries',
|
|
4411
|
+
'',
|
|
4412
|
+
...queryTitles.map(title => `- ${title}`),
|
|
4413
|
+
].join('\n');
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
async function openDocumentInsertModal(kind, insertionRange = null) {
|
|
4417
|
+
if (!state.documents.selectedId) {
|
|
4418
|
+
pushToast('Select a document before inserting Markdown.', 'alert');
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
state.modal = {
|
|
4423
|
+
kind,
|
|
4424
|
+
documentId: state.documents.selectedId,
|
|
4425
|
+
insertionRange: normalizeDocumentInsertionRange(insertionRange),
|
|
4426
|
+
queries: [],
|
|
4427
|
+
selectedHistoryId: '',
|
|
4428
|
+
loading: true,
|
|
4429
|
+
error: null,
|
|
4430
|
+
submitting: false,
|
|
4431
|
+
};
|
|
4432
|
+
emitChange();
|
|
4433
|
+
|
|
4434
|
+
try {
|
|
4435
|
+
const response = await api.getQueryHistory({
|
|
4436
|
+
tab: 'saved',
|
|
4437
|
+
onlySaved: true,
|
|
4438
|
+
limit: 100,
|
|
4439
|
+
});
|
|
4440
|
+
const loadedQueries = response.data?.items ?? [];
|
|
4441
|
+
const queries =
|
|
4442
|
+
kind === 'document-insert-note'
|
|
4443
|
+
? loadedQueries.filter(query => String(query.notes ?? '').trim())
|
|
4444
|
+
: loadedQueries;
|
|
4445
|
+
|
|
4446
|
+
if (state.modal?.kind !== kind) {
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
state.modal.queries = queries;
|
|
4451
|
+
state.modal.selectedHistoryId = String(queries[0]?.id ?? '');
|
|
4452
|
+
state.modal.loading = false;
|
|
4453
|
+
state.modal.error = null;
|
|
4454
|
+
emitChange();
|
|
4455
|
+
} catch (error) {
|
|
4456
|
+
if (state.modal?.kind !== kind) {
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
state.modal.loading = false;
|
|
4461
|
+
state.modal.error = normalizeError(error);
|
|
4462
|
+
emitChange();
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
export function updateDocumentInsertQuerySelection(historyId) {
|
|
4467
|
+
if (state.modal?.kind !== 'document-insert-table' && state.modal?.kind !== 'document-insert-note') {
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
state.modal.selectedHistoryId = String(historyId ?? '');
|
|
4472
|
+
state.modal.error = null;
|
|
4473
|
+
emitChange();
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
function getSelectedDocumentInsertQuery(modal) {
|
|
4477
|
+
const selectedHistoryId = String(modal?.selectedHistoryId ?? '');
|
|
4478
|
+
|
|
4479
|
+
return (modal?.queries ?? []).find(query => String(query.id) === selectedHistoryId) ?? null;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
function setDocumentInsertModalError(message, code = 'DOCUMENT_INSERT_UNAVAILABLE') {
|
|
4483
|
+
if (!state.modal) {
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
state.modal.error = { code, message };
|
|
4488
|
+
state.modal.submitting = false;
|
|
4489
|
+
emitChange();
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
function canSubmitDocumentInsertModal(modal, kind) {
|
|
4493
|
+
if (modal?.kind !== kind) {
|
|
4494
|
+
return false;
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
if (String(modal.documentId ?? '') !== String(state.documents.selectedId ?? '')) {
|
|
4498
|
+
setDocumentInsertModalError('The selected document changed while the dialog was open.');
|
|
4499
|
+
return false;
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
return true;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
export async function openDocumentInsertTableModal(insertionRange = null) {
|
|
4506
|
+
await openDocumentInsertModal('document-insert-table', insertionRange);
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
export async function openDocumentInsertNoteModal(insertionRange = null) {
|
|
4510
|
+
await openDocumentInsertModal('document-insert-note', insertionRange);
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function getDocumentTableDefinitionModalOptions(modal = state.modal) {
|
|
4514
|
+
return normalizeTableDefinitionOptions({
|
|
4515
|
+
tableName: modal?.selectedTableName,
|
|
4516
|
+
markdownTable: modal?.markdownTable,
|
|
4517
|
+
sqlDefinition: modal?.sqlDefinition,
|
|
4518
|
+
sampleData: modal?.sampleData,
|
|
4519
|
+
sampleRowCount: modal?.sampleRowCount,
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4523
|
+
function setDocumentTableDefinitionModalError(message, code = 'TABLE_DEFINITION_INSERT_UNAVAILABLE') {
|
|
4524
|
+
setDocumentInsertModalError(message, code);
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
export async function openDocumentInsertTableDefinitionModal(insertionRange = null) {
|
|
4528
|
+
if (!state.documents.selectedId) {
|
|
4529
|
+
pushToast('Select a document before inserting a table definition.', 'alert');
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
state.modal = {
|
|
4534
|
+
kind: 'document-insert-table-definition',
|
|
4535
|
+
documentId: state.documents.selectedId,
|
|
4536
|
+
insertionRange: normalizeDocumentInsertionRange(insertionRange),
|
|
4537
|
+
tables: [],
|
|
4538
|
+
selectedTableName: '',
|
|
4539
|
+
markdownTable: true,
|
|
4540
|
+
sqlDefinition: false,
|
|
4541
|
+
sampleData: false,
|
|
4542
|
+
sampleRowCount: TABLE_DEFINITION_DEFAULT_SAMPLE_ROW_COUNT,
|
|
4543
|
+
loading: true,
|
|
4544
|
+
error: null,
|
|
4545
|
+
submitting: false,
|
|
4546
|
+
};
|
|
4547
|
+
emitChange();
|
|
4548
|
+
|
|
4549
|
+
try {
|
|
4550
|
+
const response = await api.getDataTables();
|
|
4551
|
+
const tables = (response.data?.tables ?? []).filter(table => !table.isShadow && table.tableKind !== 'shadow');
|
|
4552
|
+
|
|
4553
|
+
if (state.modal?.kind !== 'document-insert-table-definition') {
|
|
4554
|
+
return;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
state.modal.tables = tables;
|
|
4558
|
+
state.modal.selectedTableName = String(tables[0]?.name ?? '');
|
|
4559
|
+
state.modal.loading = false;
|
|
4560
|
+
state.modal.error = null;
|
|
4561
|
+
emitChange();
|
|
4562
|
+
} catch (error) {
|
|
4563
|
+
if (state.modal?.kind !== 'document-insert-table-definition') {
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
state.modal.loading = false;
|
|
4568
|
+
state.modal.error = normalizeError(error);
|
|
4569
|
+
emitChange();
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
export function updateDocumentTableDefinitionSelection(tableName) {
|
|
4574
|
+
if (state.modal?.kind !== 'document-insert-table-definition') {
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
const selectedTable = (state.modal.tables ?? []).find(table => table.name === tableName);
|
|
4579
|
+
|
|
4580
|
+
state.modal.selectedTableName = String(selectedTable?.name ?? '');
|
|
4581
|
+
state.modal.error = null;
|
|
4582
|
+
emitChange();
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
export function updateDocumentTableDefinitionOption(option, value) {
|
|
4586
|
+
if (state.modal?.kind !== 'document-insert-table-definition') {
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
const normalizedOption = String(option ?? '');
|
|
4591
|
+
|
|
4592
|
+
if (!['markdownTable', 'sqlDefinition', 'sampleData'].includes(normalizedOption)) {
|
|
4593
|
+
return;
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
state.modal[normalizedOption] = Boolean(value);
|
|
4597
|
+
state.modal.error = null;
|
|
4598
|
+
emitChange();
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
export function updateDocumentTableDefinitionSampleRowCount(value) {
|
|
4602
|
+
if (state.modal?.kind !== 'document-insert-table-definition') {
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
state.modal.sampleRowCount = normalizeTableDefinitionSampleRowCount(value);
|
|
4607
|
+
state.modal.error = null;
|
|
4608
|
+
emitChange();
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
export async function submitDocumentInsertTableDefinition() {
|
|
4612
|
+
const modal = state.modal;
|
|
4613
|
+
|
|
4614
|
+
if (!canSubmitDocumentInsertModal(modal, 'document-insert-table-definition')) {
|
|
4615
|
+
return false;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
const options = getDocumentTableDefinitionModalOptions(modal);
|
|
4619
|
+
|
|
4620
|
+
if (!options.tableName) {
|
|
4621
|
+
setDocumentTableDefinitionModalError('Select a table before inserting a definition.');
|
|
4622
|
+
return false;
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
if (!options.markdownTable && !options.sqlDefinition && !options.sampleData) {
|
|
4626
|
+
setDocumentTableDefinitionModalError('Select at least one table definition option.');
|
|
4627
|
+
return false;
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
if (hasDocumentTableDefinitionMagicSnippet(state.documents.draftContent, options.tableName)) {
|
|
4631
|
+
pushToast('Magic Snippet already inserted.');
|
|
4632
|
+
return false;
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
startModalSubmission();
|
|
4636
|
+
|
|
4637
|
+
try {
|
|
4638
|
+
const markdown = await buildTableDefinitionMagicSnippetFromOptions(options);
|
|
4639
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown, modal.insertionRange);
|
|
4640
|
+
|
|
4641
|
+
closeModalInternal();
|
|
4642
|
+
if (inserted) {
|
|
4643
|
+
pushToast(`Inserted definition for "${options.tableName}".`, 'success');
|
|
4644
|
+
}
|
|
4645
|
+
return inserted;
|
|
4646
|
+
} catch (error) {
|
|
4647
|
+
withModalError(error);
|
|
4648
|
+
return false;
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
export async function submitDocumentInsertTable() {
|
|
4653
|
+
const modal = state.modal;
|
|
4654
|
+
|
|
4655
|
+
if (!canSubmitDocumentInsertModal(modal, 'document-insert-table')) {
|
|
4656
|
+
return false;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
const query = getSelectedDocumentInsertQuery(modal);
|
|
4660
|
+
|
|
4661
|
+
if (!query) {
|
|
4662
|
+
setDocumentInsertModalError('Select a saved query before inserting a table.');
|
|
4663
|
+
return false;
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4666
|
+
startModalSubmission();
|
|
4667
|
+
|
|
4668
|
+
try {
|
|
4669
|
+
const response = await api.getQueryExport(query.rawSql, 'md');
|
|
4670
|
+
const markdownTable = String(response.data?.content ?? '').trim();
|
|
4671
|
+
|
|
4672
|
+
if (!markdownTable) {
|
|
4673
|
+
throw new Error('The selected query returned no Markdown table content.');
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdownTable, modal.insertionRange);
|
|
4677
|
+
|
|
4678
|
+
closeModalInternal();
|
|
4679
|
+
if (inserted) {
|
|
4680
|
+
pushToast(`Inserted table from "${getDocumentInsertQueryTitle(query)}".`, 'success');
|
|
4681
|
+
}
|
|
4682
|
+
return inserted;
|
|
4683
|
+
} catch (error) {
|
|
4684
|
+
withModalError(error);
|
|
4685
|
+
return false;
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
export async function insertCurrentDocumentDatabaseInfo(insertionRange = null) {
|
|
4690
|
+
if (!state.documents.selectedId) {
|
|
4691
|
+
pushToast('Select a document before inserting database info.', 'alert');
|
|
4692
|
+
return false;
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
const snippet = getDocumentMagicSnippet(DATABASE_INFO_MAGIC_SNIPPET_ID);
|
|
4696
|
+
|
|
4697
|
+
if (snippet && hasDocumentMagicSnippet(state.documents.draftContent, snippet)) {
|
|
4698
|
+
pushToast('Magic Snippet already inserted.');
|
|
4699
|
+
return false;
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
try {
|
|
4703
|
+
const response = await api.getOverview();
|
|
4704
|
+
const result = insertDocumentMagicSnippet(
|
|
4705
|
+
state.documents.draftContent,
|
|
4706
|
+
DATABASE_INFO_MAGIC_SNIPPET_ID,
|
|
4707
|
+
{ databaseInfoMarkdown: buildDatabaseInfoMarkdown(response.data ?? {}) },
|
|
4708
|
+
insertionRange,
|
|
4709
|
+
);
|
|
4710
|
+
|
|
4711
|
+
if (result.alreadyInserted) {
|
|
4712
|
+
pushToast('Magic Snippet already inserted.');
|
|
4713
|
+
return false;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
if (!result.inserted || result.content === state.documents.draftContent) {
|
|
4717
|
+
return false;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
state.documents.draftContent = result.content;
|
|
4721
|
+
state.documents.dirty = true;
|
|
4722
|
+
state.documents.saveError = null;
|
|
4723
|
+
emitChange();
|
|
4724
|
+
pushToast('Database info inserted.', 'success');
|
|
4725
|
+
return true;
|
|
4726
|
+
} catch (error) {
|
|
4727
|
+
pushToast(normalizeError(error).message || 'Database info could not be inserted.', 'alert');
|
|
4728
|
+
return false;
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4732
|
+
export async function insertCurrentDocumentTimeMetadata(insertionRange = null) {
|
|
4733
|
+
if (!state.documents.selectedId) {
|
|
4734
|
+
pushToast('Select a document before inserting time metadata.', 'alert');
|
|
4735
|
+
return false;
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
const snippet = getDocumentMagicSnippet(DOCUMENT_METADATA_MAGIC_SNIPPET_ID);
|
|
4739
|
+
|
|
4740
|
+
if (snippet && hasDocumentMagicSnippet(state.documents.draftContent, snippet)) {
|
|
4741
|
+
pushToast('Magic Snippet already inserted.');
|
|
4742
|
+
return false;
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
const latestActivityLogTimestamp = await getLatestActivityLogTimestamp(state.documents.selected?.updatedAt ?? null);
|
|
4746
|
+
const databaseCreatedAt = await getDatabaseCreatedAt(state.connections.active?.createdAt ?? state.documents.selected?.createdAt ?? null);
|
|
4747
|
+
const result = insertDocumentMagicSnippet(
|
|
4748
|
+
state.documents.draftContent,
|
|
4749
|
+
DOCUMENT_METADATA_MAGIC_SNIPPET_ID,
|
|
4750
|
+
{
|
|
4751
|
+
document: state.documents.selected,
|
|
4752
|
+
databaseCreatedAt,
|
|
4753
|
+
latestActivityLogTimestamp,
|
|
4754
|
+
},
|
|
4755
|
+
insertionRange,
|
|
4756
|
+
);
|
|
4757
|
+
|
|
4758
|
+
if (result.alreadyInserted) {
|
|
4759
|
+
pushToast('Magic Snippet already inserted.');
|
|
4760
|
+
return false;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
if (!result.inserted || result.content === state.documents.draftContent) {
|
|
4764
|
+
return false;
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
state.documents.draftContent = result.content;
|
|
4768
|
+
state.documents.dirty = true;
|
|
4769
|
+
state.documents.saveError = null;
|
|
4770
|
+
emitChange();
|
|
4771
|
+
pushToast('Time metadata inserted.', 'success');
|
|
4772
|
+
return true;
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4775
|
+
export async function insertCurrentDocumentSavedQueries(insertionRange = null) {
|
|
4776
|
+
if (!state.documents.selectedId) {
|
|
4777
|
+
pushToast('Select a document before inserting saved queries.', 'alert');
|
|
4778
|
+
return false;
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
try {
|
|
4782
|
+
const response = await api.getQueryHistory({
|
|
4783
|
+
tab: 'saved',
|
|
4784
|
+
onlySaved: true,
|
|
4785
|
+
limit: 100,
|
|
4786
|
+
});
|
|
4787
|
+
const savedQueriesMarkdown = buildSavedQueriesMarkdown(response.data?.items ?? []);
|
|
4788
|
+
|
|
4789
|
+
if (!savedQueriesMarkdown) {
|
|
4790
|
+
pushToast('No saved queries are available for this database.', 'alert');
|
|
4791
|
+
return false;
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(savedQueriesMarkdown, insertionRange);
|
|
4795
|
+
|
|
4796
|
+
if (inserted) {
|
|
4797
|
+
pushToast('Saved queries inserted.', 'success');
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
return inserted;
|
|
4801
|
+
} catch (error) {
|
|
4802
|
+
pushToast(normalizeError(error).message || 'Saved queries could not be inserted.', 'alert');
|
|
4803
|
+
return false;
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
export function submitDocumentInsertNote() {
|
|
4808
|
+
const modal = state.modal;
|
|
4809
|
+
|
|
4810
|
+
if (!canSubmitDocumentInsertModal(modal, 'document-insert-note')) {
|
|
4811
|
+
return false;
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
const query = getSelectedDocumentInsertQuery(modal);
|
|
4815
|
+
const note = String(query?.notes ?? '').trim();
|
|
4816
|
+
|
|
4817
|
+
if (!query || !note) {
|
|
4818
|
+
setDocumentInsertModalError('Select a saved query with notes before inserting.');
|
|
4819
|
+
return false;
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
startModalSubmission();
|
|
4823
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(note, modal.insertionRange);
|
|
4824
|
+
|
|
4825
|
+
closeModalInternal();
|
|
4826
|
+
if (inserted) {
|
|
4827
|
+
pushToast(`Inserted note from "${getDocumentInsertQueryTitle(query)}".`, 'success');
|
|
4828
|
+
}
|
|
4829
|
+
return inserted;
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
export function openCreateDocumentFolderModal() {
|
|
4833
|
+
state.modal = {
|
|
4834
|
+
kind: 'create-document-folder',
|
|
4835
|
+
name: '',
|
|
4836
|
+
error: null,
|
|
4837
|
+
submitting: false,
|
|
4838
|
+
};
|
|
4839
|
+
emitChange();
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
export async function submitCreateDocumentFolder(name) {
|
|
4843
|
+
const folderName = String(name ?? '').trim();
|
|
4844
|
+
|
|
4845
|
+
if (!folderName) {
|
|
4846
|
+
withModalError(new Error('Folder name is required.'));
|
|
4847
|
+
return null;
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4850
|
+
startModalSubmission();
|
|
4851
|
+
|
|
4852
|
+
try {
|
|
4853
|
+
const response = await api.createDocumentFolder({ name: folderName });
|
|
4854
|
+
const folder = response.data ?? null;
|
|
4855
|
+
|
|
4856
|
+
state.documents.folders = response.metadata?.folders ?? (
|
|
4857
|
+
folder ? [...state.documents.folders.filter(candidate => candidate.id !== folder.id), folder] : state.documents.folders
|
|
4858
|
+
);
|
|
4859
|
+
state.documents.folders.sort((left, right) =>
|
|
4860
|
+
String(left.name ?? '').localeCompare(String(right.name ?? ''), undefined, { sensitivity: 'base' }) ||
|
|
4861
|
+
String(left.id).localeCompare(String(right.id)),
|
|
4862
|
+
);
|
|
4863
|
+
closeModalInternal();
|
|
4864
|
+
|
|
4865
|
+
if (folder) {
|
|
4866
|
+
pushToast(`Folder "${folder.name}" created.`, 'success');
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
return folder;
|
|
4870
|
+
} catch (error) {
|
|
4871
|
+
withModalError(error);
|
|
4872
|
+
return null;
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
export async function moveCurrentDocumentToFolder(folderId = null) {
|
|
4877
|
+
const documentId = state.documents.selectedId;
|
|
4878
|
+
|
|
4879
|
+
if (!documentId) {
|
|
4880
|
+
pushToast('Select a document before moving it.', 'alert');
|
|
4881
|
+
return null;
|
|
4882
|
+
}
|
|
4883
|
+
|
|
4884
|
+
const normalizedFolderId = String(folderId ?? '').trim() || null;
|
|
4885
|
+
const currentFolderId = state.documents.selected?.folderId ?? null;
|
|
4886
|
+
|
|
4887
|
+
if (String(currentFolderId ?? '') === String(normalizedFolderId ?? '')) {
|
|
4888
|
+
return state.documents.selected;
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
state.documents.saving = true;
|
|
4892
|
+
state.documents.saveError = null;
|
|
4893
|
+
emitChange();
|
|
4894
|
+
|
|
4895
|
+
try {
|
|
4896
|
+
const response = await api.updateDocument(documentId, {
|
|
4897
|
+
folderId: normalizedFolderId,
|
|
4898
|
+
});
|
|
4899
|
+
const document = response.data ?? null;
|
|
4900
|
+
|
|
4901
|
+
if (document) {
|
|
4902
|
+
upsertDocumentSummary(document);
|
|
4903
|
+
if (String(state.documents.selectedId ?? '') === String(documentId)) {
|
|
4904
|
+
if (state.documents.dirty) {
|
|
4905
|
+
state.documents.selected = {
|
|
4906
|
+
...state.documents.selected,
|
|
4907
|
+
folderId: document.folderId ?? null,
|
|
4908
|
+
updatedAt: document.updatedAt,
|
|
4909
|
+
};
|
|
4910
|
+
} else {
|
|
4911
|
+
applyCurrentDocument(document);
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
const folder = normalizedFolderId
|
|
4917
|
+
? state.documents.folders.find(candidate => String(candidate.id) === String(normalizedFolderId))
|
|
4918
|
+
: null;
|
|
4919
|
+
pushToast(`Document moved to ${folder ? `"${folder.name}"` : 'No Folder'}.`, 'success');
|
|
4920
|
+
return document;
|
|
4921
|
+
} catch (error) {
|
|
4922
|
+
state.documents.saveError = normalizeError(error);
|
|
4923
|
+
pushToast(state.documents.saveError.message || 'Document could not be moved.', 'alert');
|
|
4924
|
+
return null;
|
|
4925
|
+
} finally {
|
|
4926
|
+
state.documents.saving = false;
|
|
4927
|
+
emitChange();
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
export async function createDocument(options = {}) {
|
|
4932
|
+
state.documents.saving = true;
|
|
4933
|
+
state.documents.saveError = null;
|
|
4934
|
+
emitChange();
|
|
4935
|
+
|
|
4936
|
+
try {
|
|
4937
|
+
const response = await api.createDocument({
|
|
4938
|
+
title: options.title,
|
|
4939
|
+
filename: options.filename,
|
|
4940
|
+
content: options.content,
|
|
4941
|
+
folderId: options.folderId,
|
|
4942
|
+
});
|
|
4943
|
+
const document = response.data ?? null;
|
|
4944
|
+
|
|
4945
|
+
if (document) {
|
|
4946
|
+
upsertDocumentSummary(document);
|
|
4947
|
+
applyCurrentDocument(document);
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
if (options.toast !== false) {
|
|
4951
|
+
pushToast(`Document "${document?.filename ?? 'Untitled.md'}" created.`, 'success');
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
return document;
|
|
4955
|
+
} catch (error) {
|
|
4956
|
+
state.documents.saveError = normalizeError(error);
|
|
4957
|
+
if (options.toast !== false) {
|
|
4958
|
+
pushToast(state.documents.saveError.message || 'Document could not be created.', 'alert');
|
|
4959
|
+
}
|
|
4960
|
+
return null;
|
|
4961
|
+
} finally {
|
|
4962
|
+
state.documents.saving = false;
|
|
4963
|
+
emitChange();
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
export async function saveCurrentDocument(options = {}) {
|
|
4968
|
+
const documentId = state.documents.selectedId;
|
|
4969
|
+
|
|
4970
|
+
if (!documentId) {
|
|
4971
|
+
return null;
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4974
|
+
const submittedFilename = state.documents.draftFilename;
|
|
4975
|
+
const submittedDraftContent = state.documents.draftContent;
|
|
4976
|
+
|
|
4977
|
+
state.documents.saving = true;
|
|
4978
|
+
state.documents.saveError = null;
|
|
3873
4979
|
|
|
3874
4980
|
try {
|
|
4981
|
+
const latestActivityLogTimestamp = await getLatestActivityLogTimestamp(state.documents.selected?.updatedAt ?? null);
|
|
4982
|
+
const databaseCreatedAt = await getDatabaseCreatedAt(state.connections.active?.createdAt ?? state.documents.selected?.createdAt ?? null);
|
|
4983
|
+
const submittedContent = updateDocumentMagicSnippets(submittedDraftContent, {
|
|
4984
|
+
document: state.documents.selected,
|
|
4985
|
+
databaseCreatedAt,
|
|
4986
|
+
latestActivityLogTimestamp,
|
|
4987
|
+
});
|
|
3875
4988
|
const response = await api.updateDocument(documentId, {
|
|
3876
4989
|
filename: submittedFilename,
|
|
3877
4990
|
content: submittedContent,
|
|
@@ -3882,7 +4995,7 @@ export async function saveCurrentDocument(options = {}) {
|
|
|
3882
4995
|
const draftUnchanged =
|
|
3883
4996
|
String(state.documents.selectedId ?? '') === String(documentId) &&
|
|
3884
4997
|
state.documents.draftFilename === submittedFilename &&
|
|
3885
|
-
state.documents.draftContent ===
|
|
4998
|
+
state.documents.draftContent === submittedDraftContent;
|
|
3886
4999
|
|
|
3887
5000
|
upsertDocumentSummary(document);
|
|
3888
5001
|
if (draftUnchanged) {
|
|
@@ -3946,15 +5059,15 @@ export async function deleteCurrentDocument(options = {}) {
|
|
|
3946
5059
|
}
|
|
3947
5060
|
}
|
|
3948
5061
|
|
|
3949
|
-
export
|
|
5062
|
+
export function toggleCurrentDocumentTodo(lineIndex) {
|
|
3950
5063
|
if (!state.documents.selectedId) {
|
|
3951
|
-
return
|
|
5064
|
+
return false;
|
|
3952
5065
|
}
|
|
3953
5066
|
|
|
3954
5067
|
const nextContent = toggleMarkdownTodoLine(state.documents.draftContent, lineIndex);
|
|
3955
5068
|
|
|
3956
5069
|
if (nextContent === state.documents.draftContent) {
|
|
3957
|
-
return
|
|
5070
|
+
return false;
|
|
3958
5071
|
}
|
|
3959
5072
|
|
|
3960
5073
|
state.documents.draftContent = nextContent;
|
|
@@ -3962,7 +5075,7 @@ export async function toggleCurrentDocumentTodo(lineIndex) {
|
|
|
3962
5075
|
state.documents.saveError = null;
|
|
3963
5076
|
emitChange();
|
|
3964
5077
|
|
|
3965
|
-
return
|
|
5078
|
+
return true;
|
|
3966
5079
|
}
|
|
3967
5080
|
|
|
3968
5081
|
export async function createDocumentFromMarkdownExport({ filename, content, title } = {}) {
|