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.
@@ -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(state.documents.draftContent ?? '').length;
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
- export function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
3588
- if (!state.documents.selectedId) {
3589
- return false;
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
- const nextContent = buildDocumentMarkdownInsertion(state.documents.draftContent, markdown, range);
3629
+ return [
3630
+ `- created at: ${createdAt}`,
3631
+ `- last modified: ${lastModifiedAt}`,
3632
+ ].join('\n');
3633
+ }
3593
3634
 
3594
- if (nextContent === state.documents.draftContent) {
3595
- return false;
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
- state.documents.draftContent = nextContent;
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
- export async function insertMarkdownIntoLastOpenDocument(markdown) {
3606
- if (state.route.name === 'documents' && state.documents.selectedId) {
3607
- const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
3644
+ function wrapDatabaseInfoMagicSnippet(body = '') {
3645
+ const normalizedBody = String(body ?? '').trim();
3608
3646
 
3609
- return {
3610
- documentId: state.documents.selectedId,
3611
- inserted,
3612
- saved: false,
3613
- };
3647
+ if (!normalizedBody) {
3648
+ return '';
3614
3649
  }
3615
3650
 
3616
- const documentId = lastOpenDocumentId ?? state.documents.selectedId;
3617
-
3618
- if (!documentId) {
3619
- return {
3620
- documentId: null,
3621
- inserted: false,
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
- const response = await api.getDocument(documentId);
3627
- const document = response.data ?? null;
3658
+ function buildDatabaseInfoMagicSnippetBody(context = {}) {
3659
+ const providedMarkdown = unwrapDatabaseInfoMagicSnippet(context.databaseInfoMarkdown);
3628
3660
 
3629
- if (!document?.id) {
3630
- return {
3631
- documentId: null,
3632
- inserted: false,
3633
- saved: false,
3634
- };
3661
+ if (providedMarkdown) {
3662
+ return providedMarkdown;
3635
3663
  }
3636
3664
 
3637
- const nextContent = buildDocumentMarkdownInsertion(document.content, markdown);
3665
+ const overview = context.overview;
3638
3666
 
3639
- if (nextContent === document.content) {
3640
- return {
3641
- documentId: document.id,
3642
- inserted: false,
3643
- saved: false,
3644
- };
3667
+ if (!overview) {
3668
+ return '';
3645
3669
  }
3646
3670
 
3647
- const updateResponse = await api.updateDocument(document.id, {
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
- documentId: updatedDocument.id,
3657
- inserted: true,
3658
- saved: true,
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 getDocumentInsertQueryTitle(query) {
3663
- return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
3683
+ function buildDatabaseInfoMagicSnippet(context = {}) {
3684
+ return wrapDatabaseInfoMagicSnippet(buildDatabaseInfoMagicSnippetBody(context));
3664
3685
  }
3665
3686
 
3666
- async function openDocumentInsertModal(kind, insertionRange = null) {
3667
- if (!state.documents.selectedId) {
3668
- pushToast('Select a document before inserting Markdown.', 'alert');
3669
- return;
3670
- }
3687
+ function buildDatabaseInfoMarkdown(overview = {}) {
3688
+ return buildDatabaseInfoMagicSnippetBody({ overview });
3689
+ }
3671
3690
 
3672
- state.modal = {
3673
- kind,
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
- try {
3685
- const response = await api.getQueryHistory({
3686
- tab: 'saved',
3687
- onlySaved: true,
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
- if (state.modal?.kind !== kind) {
3697
- return;
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
- state.modal.queries = queries;
3701
- state.modal.selectedHistoryId = String(queries[0]?.id ?? '');
3702
- state.modal.loading = false;
3703
- state.modal.error = null;
3704
- emitChange();
3705
- } catch (error) {
3706
- if (state.modal?.kind !== kind) {
3707
- return;
3708
- }
3705
+ return {
3706
+ tableName,
3707
+ markdownTable,
3708
+ sqlDefinition,
3709
+ sampleData,
3710
+ sampleRowCount: normalizeTableDefinitionSampleRowCount(options.sampleRowCount),
3711
+ };
3712
+ }
3709
3713
 
3710
- state.modal.loading = false;
3711
- state.modal.error = normalizeError(error);
3712
- emitChange();
3713
- }
3714
+ function normalizeTableDefinitionTableKey(tableName) {
3715
+ return String(tableName ?? '').trim().toLowerCase();
3714
3716
  }
3715
3717
 
3716
- export function updateDocumentInsertQuerySelection(historyId) {
3717
- if (state.modal?.kind !== 'document-insert-table' && state.modal?.kind !== 'document-insert-note') {
3718
- return;
3719
- }
3718
+ function getTableDefinitionSnippetKey(options = {}) {
3719
+ const normalized = normalizeTableDefinitionOptions(options);
3720
3720
 
3721
- state.modal.selectedHistoryId = String(historyId ?? '');
3722
- state.modal.error = null;
3723
- emitChange();
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 getSelectedDocumentInsertQuery(modal) {
3727
- const selectedHistoryId = String(modal?.selectedHistoryId ?? '');
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 setDocumentInsertModalError(message, code = 'DOCUMENT_INSERT_UNAVAILABLE') {
3733
- if (!state.modal) {
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 canSubmitDocumentInsertModal(modal, kind) {
3743
- if (modal?.kind !== kind) {
3744
- return false;
3745
- }
3742
+ function buildTableDefinitionMagicSnippetMetadata(options = {}) {
3743
+ const normalized = normalizeTableDefinitionOptions(options);
3746
3744
 
3747
- if (String(modal.documentId ?? '') !== String(state.documents.selectedId ?? '')) {
3748
- setDocumentInsertModalError('The selected document changed while the dialog was open.');
3749
- return false;
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
- return true;
3756
+ function normalizeMarkdownInlineText(value = '') {
3757
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
3753
3758
  }
3754
3759
 
3755
- export async function openDocumentInsertTableModal(insertionRange = null) {
3756
- await openDocumentInsertModal('document-insert-table', insertionRange);
3760
+ function escapeMarkdownTableCell(value = '') {
3761
+ return String(value ?? '')
3762
+ .replace(/\r?\n/g, ' ')
3763
+ .replace(/\|/g, '\\|')
3764
+ .trim();
3757
3765
  }
3758
3766
 
3759
- export async function openDocumentInsertNoteModal(insertionRange = null) {
3760
- await openDocumentInsertModal('document-insert-note', insertionRange);
3767
+ function escapeRegExpText(value = '') {
3768
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3761
3769
  }
3762
3770
 
3763
- export async function submitDocumentInsertTable() {
3764
- const modal = state.modal;
3771
+ function expressionReferencesColumn(expression = '', columnName = '') {
3772
+ const normalizedColumn = String(columnName ?? '').trim();
3765
3773
 
3766
- if (!canSubmitDocumentInsertModal(modal, 'document-insert-table')) {
3774
+ if (!normalizedColumn) {
3767
3775
  return false;
3768
3776
  }
3769
3777
 
3770
- const query = getSelectedDocumentInsertQuery(modal);
3778
+ return new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExpText(normalizedColumn)}([^A-Za-z0-9_$]|$)`, 'i').test(
3779
+ String(expression ?? ''),
3780
+ );
3781
+ }
3771
3782
 
3772
- if (!query) {
3773
- setDocumentInsertModalError('Select a saved query before inserting a table.');
3774
- return false;
3775
- }
3783
+ function tableColumnHasUniqueConstraint(table = {}, columnName = '') {
3784
+ return (table.indexes ?? []).some(index => {
3785
+ if (!index?.unique) {
3786
+ return false;
3787
+ }
3776
3788
 
3777
- startModalSubmission();
3789
+ const indexColumns = (index.columns ?? [])
3790
+ .map(column => String(column.name ?? '').trim())
3791
+ .filter(Boolean);
3778
3792
 
3779
- try {
3780
- const response = await api.getQueryExport(query.rawSql, 'md');
3781
- const markdownTable = String(response.data?.content ?? '').trim();
3793
+ return indexColumns.length === 1 && indexColumns[0] === columnName;
3794
+ });
3795
+ }
3782
3796
 
3783
- if (!markdownTable) {
3784
- throw new Error('The selected query returned no Markdown table content.');
3785
- }
3797
+ function tableColumnHasAutoincrement(table = {}, column = {}) {
3798
+ return Boolean(column.primaryKeyPosition) && /\bAUTOINCREMENT\b/i.test(String(table.ddl ?? ''));
3799
+ }
3786
3800
 
3787
- const inserted = insertMarkdownIntoCurrentDocumentDraft(markdownTable, modal.insertionRange);
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
- closeModalInternal();
3790
- if (inserted) {
3791
- pushToast(`Inserted table from "${getDocumentInsertQueryTitle(query)}".`, 'success');
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
- export function submitDocumentInsertNote() {
3801
- const modal = state.modal;
3816
+ function buildColumnRestrictionText(table = {}, column = {}) {
3817
+ const restrictions = [];
3802
3818
 
3803
- if (!canSubmitDocumentInsertModal(modal, 'document-insert-note')) {
3804
- return false;
3819
+ if (Number(column.primaryKeyPosition ?? 0) > 0) {
3820
+ restrictions.push('PRIMARY KEY');
3805
3821
  }
3806
3822
 
3807
- const query = getSelectedDocumentInsertQuery(modal);
3808
- const note = String(query?.notes ?? '').trim();
3823
+ if (tableColumnHasAutoincrement(table, column)) {
3824
+ restrictions.push('AUTOINCREMENT');
3825
+ }
3809
3826
 
3810
- if (!query || !note) {
3811
- setDocumentInsertModalError('Select a saved query with notes before inserting.');
3812
- return false;
3827
+ if (column.notNull) {
3828
+ restrictions.push('NOT NULL');
3813
3829
  }
3814
3830
 
3815
- startModalSubmission();
3816
- const inserted = insertMarkdownIntoCurrentDocumentDraft(note, modal.insertionRange);
3831
+ if (tableColumnHasUniqueConstraint(table, column.name)) {
3832
+ restrictions.push('UNIQUE');
3833
+ }
3817
3834
 
3818
- closeModalInternal();
3819
- if (inserted) {
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
- return inserted;
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
- export async function createDocument(options = {}) {
3826
- state.documents.saving = true;
3827
- state.documents.saveError = null;
3828
- emitChange();
3848
+ function getVisibleTableDefinitionColumns(table = {}) {
3849
+ return (table.columns ?? []).filter(column => column.visible !== false && !column.generated);
3850
+ }
3829
3851
 
3830
- try {
3831
- const response = await api.createDocument({
3832
- title: options.title,
3833
- filename: options.filename,
3834
- content: options.content,
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
- if (document) {
3839
- upsertDocumentSummary(document);
3840
- applyCurrentDocument(document);
3841
- }
3859
+ return [
3860
+ '| Column | Type | Restrictions |',
3861
+ '|---|---|---|',
3862
+ ...rows.map(row => `| ${row.join(' | ')} |`),
3863
+ ].join('\n');
3864
+ }
3842
3865
 
3843
- if (options.toast !== false) {
3844
- pushToast(`Document "${document?.filename ?? 'Untitled.md'}" created.`, 'success');
3845
- }
3866
+ function normalizeSqlDefinition(sql = '') {
3867
+ const statement = String(sql ?? '').trim();
3846
3868
 
3847
- return document;
3848
- } catch (error) {
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
- export async function saveCurrentDocument(options = {}) {
3861
- const documentId = state.documents.selectedId;
3876
+ function buildTableDefinitionSqlBlock(table = {}) {
3877
+ const statement = normalizeSqlDefinition(table.ddl);
3862
3878
 
3863
- if (!documentId) {
3864
- return null;
3879
+ if (!statement) {
3880
+ return '';
3865
3881
  }
3866
3882
 
3867
- const submittedFilename = state.documents.draftFilename;
3868
- const submittedContent = state.documents.draftContent;
3883
+ return ['```sql', statement, '```'].join('\n');
3884
+ }
3869
3885
 
3870
- state.documents.saving = true;
3871
- state.documents.saveError = null;
3872
- emitChange();
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 === submittedContent;
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 async function toggleCurrentDocumentTodo(lineIndex) {
5062
+ export function toggleCurrentDocumentTodo(lineIndex) {
3950
5063
  if (!state.documents.selectedId) {
3951
- return null;
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 state.documents.selected;
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 saveCurrentDocument({ toast: false });
5078
+ return true;
3966
5079
  }
3967
5080
 
3968
5081
  export async function createDocumentFromMarkdownExport({ filename, content, title } = {}) {