sqlite-hub 2.0.1 → 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.
Files changed (98) hide show
  1. package/README.md +21 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +16 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/docs/todo.md +2 -0
  6. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  28. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  33. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  37. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  43. package/frontend/js/api.js +18 -0
  44. package/frontend/js/app.js +567 -15
  45. package/frontend/js/components/badges.js +16 -0
  46. package/frontend/js/components/connectionCard.js +35 -0
  47. package/frontend/js/components/modal.js +268 -247
  48. package/frontend/js/components/queryHistoryDetail.js +25 -45
  49. package/frontend/js/components/rowEditorPanel.js +7 -60
  50. package/frontend/js/components/structureGraph.js +292 -13
  51. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  52. package/frontend/js/components/tableDesignerEditor.js +51 -16
  53. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  54. package/frontend/js/store.js +1847 -190
  55. package/frontend/js/utils/connectionRegistry.js +76 -0
  56. package/frontend/js/utils/markdownDocuments.js +21 -4
  57. package/frontend/js/utils/tableDesigner.js +431 -62
  58. package/frontend/js/views/backups.js +2 -5
  59. package/frontend/js/views/charts.js +20 -10
  60. package/frontend/js/views/connections.js +101 -6
  61. package/frontend/js/views/data.js +52 -9
  62. package/frontend/js/views/documents.js +153 -59
  63. package/frontend/js/views/structure.js +28 -5
  64. package/frontend/js/views/tableAdvisor.js +14 -6
  65. package/frontend/js/views/tableDesigner.js +44 -9
  66. package/frontend/styles/base.css +9 -0
  67. package/frontend/styles/components.css +676 -79
  68. package/frontend/styles/structure-graph.css +49 -0
  69. package/frontend/styles/tailwind.generated.css +1 -1
  70. package/frontend/styles/views.css +12 -0
  71. package/package.json +1 -1
  72. package/server/routes/backups.js +37 -1
  73. package/server/routes/charts.js +28 -0
  74. package/server/routes/connections.js +12 -0
  75. package/server/routes/data.js +30 -2
  76. package/server/routes/documents.js +61 -5
  77. package/server/routes/settings.js +32 -0
  78. package/server/routes/tableDesigner.js +35 -2
  79. package/server/server.js +3 -3
  80. package/server/services/mcpToolService.js +30 -0
  81. package/server/services/sqlite/connectionManager.js +25 -2
  82. package/server/services/sqlite/dataBrowserService.js +21 -0
  83. package/server/services/sqlite/introspection.js +39 -1
  84. package/server/services/sqlite/overviewService.js +1 -0
  85. package/server/services/sqlite/structureService.js +66 -11
  86. package/server/services/sqlite/tableAdvisor.js +6 -0
  87. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  88. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  89. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  90. package/server/services/sqlite/tableDesignerService.js +110 -5
  91. package/server/services/storage/appStateStore.js +590 -10
  92. package/server/utils/fileValidation.js +5 -0
  93. package/server/utils/userActionLog.js +49 -0
  94. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  95. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  96. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  97. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  98. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -1,10 +1,13 @@
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
- createTableDesignerDraftFromCsvImport,
5
+ addTableDesignerCheckConstraint,
6
+ createTableDesignerDraftFromImport,
6
7
  createNewTableDesignerDraft,
7
8
  hydrateTableDesignerDraft,
9
+ recalculateTableDesignerDraft,
10
+ removeTableDesignerCheckConstraint,
8
11
  removeTableDesignerColumn,
9
12
  updateTableDesignerColumnField,
10
13
  updateTableDesignerConstraintField,
@@ -28,6 +31,10 @@ import {
28
31
  getDefaultSyntheticOptions,
29
32
  normalizeSyntheticGeneratorType,
30
33
  } from './utils/syntheticData.js';
34
+ import {
35
+ normalizeConnectionTagKey,
36
+ normalizeConnectionTagName,
37
+ } from './utils/connectionRegistry.js';
31
38
 
32
39
  const listeners = new Set();
33
40
  const DEFAULT_SETTINGS = {
@@ -90,6 +97,16 @@ const MISSING_DATABASE_ERROR = {
90
97
  message: 'No active SQLite database selected.',
91
98
  };
92
99
 
100
+ function createClosedTableDesignerConstraintsDrawer() {
101
+ return {
102
+ visible: false,
103
+ columnId: '',
104
+ columnName: '',
105
+ editingConstraintId: '',
106
+ editor: null,
107
+ };
108
+ }
109
+
93
110
  let routeLoadVersion = 0;
94
111
  let queryHistoryLoadVersion = 0;
95
112
  let queryHistoryDetailLoadVersion = 0;
@@ -296,6 +313,9 @@ const state = {
296
313
  connections: {
297
314
  recent: [],
298
315
  active: null,
316
+ tags: [],
317
+ searchQuery: '',
318
+ selectedTagIds: [],
299
319
  loading: false,
300
320
  backupLoading: false,
301
321
  error: null,
@@ -442,6 +462,7 @@ const state = {
442
462
  },
443
463
  documents: {
444
464
  items: [],
465
+ folders: [],
445
466
  selectedId: null,
446
467
  selected: null,
447
468
  searchQuery: '',
@@ -465,6 +486,7 @@ const state = {
465
486
  tablesVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true),
466
487
  sqlPreviewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true),
467
488
  pendingImportedDraft: null,
489
+ constraintsDrawer: createClosedTableDesignerConstraintsDrawer(),
468
490
  loading: false,
469
491
  detailLoading: false,
470
492
  saving: false,
@@ -1155,6 +1177,7 @@ function resetChartsState() {
1155
1177
  function resetDocumentsState() {
1156
1178
  documentsLoadVersion += 1;
1157
1179
  state.documents.items = [];
1180
+ state.documents.folders = [];
1158
1181
  state.documents.selectedId = null;
1159
1182
  state.documents.selected = null;
1160
1183
  state.documents.searchQuery = '';
@@ -1190,6 +1213,7 @@ function upsertDocumentSummary(document) {
1190
1213
  const summary = {
1191
1214
  id: document.id,
1192
1215
  databaseKey: document.databaseKey,
1216
+ folderId: document.folderId ?? null,
1193
1217
  title: document.title,
1194
1218
  filename: document.filename,
1195
1219
  contentLength: document.contentLength,
@@ -1531,13 +1555,17 @@ async function refreshConnectionsState() {
1531
1555
  emitChange();
1532
1556
 
1533
1557
  try {
1534
- const [recentResponse, activeResponse] = await Promise.all([
1558
+ const [recentResponse, activeResponse, tagsResponse] = await Promise.all([
1535
1559
  api.getRecentConnections(),
1536
1560
  api.getActiveConnection(),
1561
+ api.getConnectionTags(),
1537
1562
  ]);
1538
1563
 
1539
1564
  state.connections.recent = recentResponse.data ?? [];
1540
1565
  state.connections.active = activeResponse.data ?? null;
1566
+ state.connections.tags = tagsResponse.data ?? [];
1567
+ const availableTagIds = new Set(state.connections.tags.map(tag => String(tag.id)));
1568
+ state.connections.selectedTagIds = state.connections.selectedTagIds.filter(tagId => availableTagIds.has(String(tagId)));
1541
1569
  state.connections.error = null;
1542
1570
  } catch (error) {
1543
1571
  state.connections.error = normalizeError(error);
@@ -2145,7 +2173,12 @@ async function loadDocumentDetail(version, documentId) {
2145
2173
  return;
2146
2174
  }
2147
2175
 
2148
- const document = response.data ?? null;
2176
+ const document = await updateDocumentMagicSnippetsOnOpen(version, response.data ?? null);
2177
+
2178
+ if (version !== routeLoadVersion) {
2179
+ return;
2180
+ }
2181
+
2149
2182
  applyCurrentDocument(document);
2150
2183
  upsertDocumentSummary(document);
2151
2184
  } catch (error) {
@@ -2178,6 +2211,7 @@ async function loadDocuments(version, route) {
2178
2211
  }
2179
2212
 
2180
2213
  state.documents.items = response.data?.items ?? [];
2214
+ state.documents.folders = response.data?.folders ?? [];
2181
2215
  state.documents.error = null;
2182
2216
 
2183
2217
  await loadDocumentDetail(version, resolveDocumentSelection(route));
@@ -2187,6 +2221,7 @@ async function loadDocuments(version, route) {
2187
2221
  }
2188
2222
 
2189
2223
  state.documents.items = [];
2224
+ state.documents.folders = [];
2190
2225
  applyCurrentDocument(null);
2191
2226
  state.documents.error = normalizeError(error);
2192
2227
  } finally {
@@ -2474,7 +2509,7 @@ async function loadTableAdvisor(version, route) {
2474
2509
  return;
2475
2510
  }
2476
2511
 
2477
- const tables = response.data?.tables ?? [];
2512
+ const tables = (response.data?.tables ?? []).filter(table => !table?.isShadow);
2478
2513
  const requestedTableName = route.params?.tableName ?? null;
2479
2514
  const selectedTableName =
2480
2515
  requestedTableName && tables.some(table => table.name === requestedTableName)
@@ -2613,6 +2648,7 @@ async function loadTableDesignerDetail(version, tableName) {
2613
2648
  }
2614
2649
 
2615
2650
  state.tableDesigner.selectedTableName = tableName;
2651
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
2616
2652
  state.tableDesigner.draft = decorateTableDesignerDraft(response.data?.draft ?? null);
2617
2653
  } catch (error) {
2618
2654
  if (version !== routeLoadVersion) {
@@ -2649,6 +2685,7 @@ async function loadTableDesigner(version, route) {
2649
2685
  state.tableDesigner.selectedTableName = null;
2650
2686
  state.tableDesigner.detailLoading = false;
2651
2687
  state.tableDesigner.pendingImportedDraft = null;
2688
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
2652
2689
  state.tableDesigner.draft = decorateTableDesignerDraft(importedDraft ?? createNewTableDesignerDraft());
2653
2690
  return;
2654
2691
  }
@@ -2665,6 +2702,7 @@ async function loadTableDesigner(version, route) {
2665
2702
  if (!tableName) {
2666
2703
  state.tableDesigner.selectedTableName = null;
2667
2704
  state.tableDesigner.detailLoading = false;
2705
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
2668
2706
  state.tableDesigner.draft = null;
2669
2707
  return;
2670
2708
  }
@@ -3221,6 +3259,11 @@ export function openGenerateDataModal() {
3221
3259
  return;
3222
3260
  }
3223
3261
 
3262
+ if (table.isShadow) {
3263
+ pushToast('Shadow tables are read-only in Data.', 'alert');
3264
+ return;
3265
+ }
3266
+
3224
3267
  const columns = (table.columnMeta ?? []).filter(column => column.visible && !column.generated);
3225
3268
 
3226
3269
  state.modal = {
@@ -3519,8 +3562,8 @@ export function toggleDocumentsPane(pane) {
3519
3562
  }
3520
3563
  }
3521
3564
 
3522
- function normalizeDocumentInsertionRange(range = null) {
3523
- const contentLength = String(state.documents.draftContent ?? '').length;
3565
+ function normalizeDocumentInsertionRange(range = null, currentContent = state.documents.draftContent) {
3566
+ const contentLength = String(currentContent ?? '').length;
3524
3567
  const start = Number(range?.start);
3525
3568
  const end = Number(range?.end);
3526
3569
  const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
@@ -3542,7 +3585,7 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
3542
3585
  return content;
3543
3586
  }
3544
3587
 
3545
- const normalizedRange = normalizeDocumentInsertionRange(range);
3588
+ const normalizedRange = normalizeDocumentInsertionRange(range, content);
3546
3589
  const before = content.slice(0, normalizedRange.start);
3547
3590
  const after = content.slice(normalizedRange.end);
3548
3591
  const prefix = before && !before.endsWith('\n\n') ? (before.endsWith('\n') ? '\n' : '\n\n') : '';
@@ -3551,254 +3594,1351 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
3551
3594
  return `${before}${prefix}${text}${suffix}${after}`;
3552
3595
  }
3553
3596
 
3554
- export function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
3555
- if (!state.documents.selectedId) {
3556
- 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 '';
3557
3627
  }
3558
3628
 
3559
- const nextContent = buildDocumentMarkdownInsertion(state.documents.draftContent, markdown, range);
3629
+ return [
3630
+ `- created at: ${createdAt}`,
3631
+ `- last modified: ${lastModifiedAt}`,
3632
+ ].join('\n');
3633
+ }
3560
3634
 
3561
- if (nextContent === state.documents.draftContent) {
3562
- return false;
3563
- }
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
+ );
3564
3640
 
3565
- state.documents.draftContent = nextContent;
3566
- state.documents.dirty = true;
3567
- state.documents.saveError = null;
3568
- emitChange();
3569
- return true;
3641
+ return String(match?.[1] ?? value).trim();
3570
3642
  }
3571
3643
 
3572
- export async function insertMarkdownIntoLastOpenDocument(markdown) {
3573
- if (state.route.name === 'documents' && state.documents.selectedId) {
3574
- const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
3644
+ function wrapDatabaseInfoMagicSnippet(body = '') {
3645
+ const normalizedBody = String(body ?? '').trim();
3575
3646
 
3576
- return {
3577
- documentId: state.documents.selectedId,
3578
- inserted,
3579
- saved: false,
3580
- };
3647
+ if (!normalizedBody) {
3648
+ return '';
3581
3649
  }
3582
3650
 
3583
- const documentId = lastOpenDocumentId ?? state.documents.selectedId;
3651
+ return [
3652
+ DATABASE_INFO_MAGIC_SNIPPET_START,
3653
+ normalizedBody,
3654
+ DATABASE_INFO_MAGIC_SNIPPET_END,
3655
+ ].join('\n');
3656
+ }
3584
3657
 
3585
- if (!documentId) {
3586
- return {
3587
- documentId: null,
3588
- inserted: false,
3589
- saved: false,
3590
- };
3658
+ function buildDatabaseInfoMagicSnippetBody(context = {}) {
3659
+ const providedMarkdown = unwrapDatabaseInfoMagicSnippet(context.databaseInfoMarkdown);
3660
+
3661
+ if (providedMarkdown) {
3662
+ return providedMarkdown;
3591
3663
  }
3592
3664
 
3593
- const response = await api.getDocument(documentId);
3594
- const document = response.data ?? null;
3665
+ const overview = context.overview;
3595
3666
 
3596
- if (!document?.id) {
3597
- return {
3598
- documentId: null,
3599
- inserted: false,
3600
- saved: false,
3601
- };
3667
+ if (!overview) {
3668
+ return '';
3602
3669
  }
3603
3670
 
3604
- const nextContent = buildDocumentMarkdownInsertion(document.content, markdown);
3671
+ const estimatedSizeBytes = overview.estimatedSizeBytes || overview.file?.sizeBytes || 0;
3605
3672
 
3606
- if (nextContent === document.content) {
3607
- return {
3608
- documentId: document.id,
3609
- inserted: false,
3610
- saved: false,
3611
- };
3612
- }
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');
3681
+ }
3613
3682
 
3614
- const updateResponse = await api.updateDocument(document.id, {
3615
- content: nextContent,
3616
- });
3617
- const updatedDocument = updateResponse.data ?? document;
3683
+ function buildDatabaseInfoMagicSnippet(context = {}) {
3684
+ return wrapDatabaseInfoMagicSnippet(buildDatabaseInfoMagicSnippetBody(context));
3685
+ }
3618
3686
 
3619
- lastOpenDocumentId = updatedDocument.id;
3620
- upsertDocumentSummary(updatedDocument);
3687
+ function buildDatabaseInfoMarkdown(overview = {}) {
3688
+ return buildDatabaseInfoMagicSnippetBody({ overview });
3689
+ }
3690
+
3691
+ function normalizeTableDefinitionSampleRowCount(value) {
3692
+ const numericValue = Number(value);
3693
+
3694
+ return TABLE_DEFINITION_SAMPLE_ROW_COUNTS.has(numericValue)
3695
+ ? numericValue
3696
+ : TABLE_DEFINITION_DEFAULT_SAMPLE_ROW_COUNT;
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);
3621
3704
 
3622
3705
  return {
3623
- documentId: updatedDocument.id,
3624
- inserted: true,
3625
- saved: true,
3706
+ tableName,
3707
+ markdownTable,
3708
+ sqlDefinition,
3709
+ sampleData,
3710
+ sampleRowCount: normalizeTableDefinitionSampleRowCount(options.sampleRowCount),
3626
3711
  };
3627
3712
  }
3628
3713
 
3629
- function getDocumentInsertQueryTitle(query) {
3630
- return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
3714
+ function normalizeTableDefinitionTableKey(tableName) {
3715
+ return String(tableName ?? '').trim().toLowerCase();
3631
3716
  }
3632
3717
 
3633
- async function openDocumentInsertModal(kind, insertionRange = null) {
3634
- if (!state.documents.selectedId) {
3635
- pushToast('Select a document before inserting Markdown.', 'alert');
3636
- return;
3718
+ function getTableDefinitionSnippetKey(options = {}) {
3719
+ const normalized = normalizeTableDefinitionOptions(options);
3720
+
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
+ });
3728
+ }
3729
+
3730
+ function encodeDocumentMagicSnippetMetadata(metadata = {}) {
3731
+ return encodeURIComponent(JSON.stringify(metadata));
3732
+ }
3733
+
3734
+ function decodeDocumentMagicSnippetMetadata(encodedMetadata = '') {
3735
+ try {
3736
+ return JSON.parse(decodeURIComponent(String(encodedMetadata ?? '').trim()));
3737
+ } catch {
3738
+ return null;
3637
3739
  }
3740
+ }
3638
3741
 
3639
- state.modal = {
3640
- kind,
3641
- documentId: state.documents.selectedId,
3642
- insertionRange: normalizeDocumentInsertionRange(insertionRange),
3643
- queries: [],
3644
- selectedHistoryId: '',
3645
- loading: true,
3646
- error: null,
3647
- submitting: false,
3742
+ function buildTableDefinitionMagicSnippetMetadata(options = {}) {
3743
+ const normalized = normalizeTableDefinitionOptions(options);
3744
+
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,
3648
3753
  };
3649
- emitChange();
3754
+ }
3650
3755
 
3651
- try {
3652
- const response = await api.getQueryHistory({
3653
- tab: 'saved',
3654
- onlySaved: true,
3655
- limit: 100,
3656
- });
3657
- const loadedQueries = response.data?.items ?? [];
3658
- const queries =
3659
- kind === 'document-insert-note'
3660
- ? loadedQueries.filter(query => String(query.notes ?? '').trim())
3661
- : loadedQueries;
3756
+ function normalizeMarkdownInlineText(value = '') {
3757
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
3758
+ }
3662
3759
 
3663
- if (state.modal?.kind !== kind) {
3664
- return;
3665
- }
3760
+ function escapeMarkdownTableCell(value = '') {
3761
+ return String(value ?? '')
3762
+ .replace(/\r?\n/g, ' ')
3763
+ .replace(/\|/g, '\\|')
3764
+ .trim();
3765
+ }
3666
3766
 
3667
- state.modal.queries = queries;
3668
- state.modal.selectedHistoryId = String(queries[0]?.id ?? '');
3669
- state.modal.loading = false;
3670
- state.modal.error = null;
3671
- emitChange();
3672
- } catch (error) {
3673
- if (state.modal?.kind !== kind) {
3674
- return;
3675
- }
3767
+ function escapeRegExpText(value = '') {
3768
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3769
+ }
3676
3770
 
3677
- state.modal.loading = false;
3678
- state.modal.error = normalizeError(error);
3679
- emitChange();
3771
+ function expressionReferencesColumn(expression = '', columnName = '') {
3772
+ const normalizedColumn = String(columnName ?? '').trim();
3773
+
3774
+ if (!normalizedColumn) {
3775
+ return false;
3680
3776
  }
3777
+
3778
+ return new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExpText(normalizedColumn)}([^A-Za-z0-9_$]|$)`, 'i').test(
3779
+ String(expression ?? ''),
3780
+ );
3681
3781
  }
3682
3782
 
3683
- export function updateDocumentInsertQuerySelection(historyId) {
3684
- if (state.modal?.kind !== 'document-insert-table' && state.modal?.kind !== 'document-insert-note') {
3685
- return;
3686
- }
3783
+ function tableColumnHasUniqueConstraint(table = {}, columnName = '') {
3784
+ return (table.indexes ?? []).some(index => {
3785
+ if (!index?.unique) {
3786
+ return false;
3787
+ }
3687
3788
 
3688
- state.modal.selectedHistoryId = String(historyId ?? '');
3689
- state.modal.error = null;
3690
- emitChange();
3789
+ const indexColumns = (index.columns ?? [])
3790
+ .map(column => String(column.name ?? '').trim())
3791
+ .filter(Boolean);
3792
+
3793
+ return indexColumns.length === 1 && indexColumns[0] === columnName;
3794
+ });
3691
3795
  }
3692
3796
 
3693
- function getSelectedDocumentInsertQuery(modal) {
3694
- const selectedHistoryId = String(modal?.selectedHistoryId ?? '');
3797
+ function tableColumnHasAutoincrement(table = {}, column = {}) {
3798
+ return Boolean(column.primaryKeyPosition) && /\bAUTOINCREMENT\b/i.test(String(table.ddl ?? ''));
3799
+ }
3695
3800
 
3696
- return (modal?.queries ?? []).find(query => String(query.id) === selectedHistoryId) ?? null;
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})` : '';
3809
+
3810
+ return `REFERENCES ${foreignKey.referencedTable}${columnSuffix}`;
3811
+ }),
3812
+ )
3813
+ .filter(Boolean);
3697
3814
  }
3698
3815
 
3699
- function setDocumentInsertModalError(message, code = 'DOCUMENT_INSERT_UNAVAILABLE') {
3700
- if (!state.modal) {
3701
- return;
3816
+ function buildColumnRestrictionText(table = {}, column = {}) {
3817
+ const restrictions = [];
3818
+
3819
+ if (Number(column.primaryKeyPosition ?? 0) > 0) {
3820
+ restrictions.push('PRIMARY KEY');
3702
3821
  }
3703
3822
 
3704
- state.modal.error = { code, message };
3705
- state.modal.submitting = false;
3706
- emitChange();
3707
- }
3823
+ if (tableColumnHasAutoincrement(table, column)) {
3824
+ restrictions.push('AUTOINCREMENT');
3825
+ }
3708
3826
 
3709
- function canSubmitDocumentInsertModal(modal, kind) {
3710
- if (modal?.kind !== kind) {
3711
- return false;
3827
+ if (column.notNull) {
3828
+ restrictions.push('NOT NULL');
3712
3829
  }
3713
3830
 
3714
- if (String(modal.documentId ?? '') !== String(state.documents.selectedId ?? '')) {
3715
- setDocumentInsertModalError('The selected document changed while the dialog was open.');
3716
- return false;
3831
+ if (tableColumnHasUniqueConstraint(table, column.name)) {
3832
+ restrictions.push('UNIQUE');
3717
3833
  }
3718
3834
 
3719
- return true;
3835
+ if (column.defaultValue !== null && column.defaultValue !== undefined && String(column.defaultValue).trim()) {
3836
+ restrictions.push(`DEFAULT ${String(column.defaultValue).trim()}`);
3837
+ }
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(', ');
3720
3846
  }
3721
3847
 
3722
- export async function openDocumentInsertTableModal(insertionRange = null) {
3723
- await openDocumentInsertModal('document-insert-table', insertionRange);
3848
+ function getVisibleTableDefinitionColumns(table = {}) {
3849
+ return (table.columns ?? []).filter(column => column.visible !== false && !column.generated);
3724
3850
  }
3725
3851
 
3726
- export async function openDocumentInsertNoteModal(insertionRange = null) {
3727
- await openDocumentInsertModal('document-insert-note', insertionRange);
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
+ ]);
3858
+
3859
+ return [
3860
+ '| Column | Type | Restrictions |',
3861
+ '|---|---|---|',
3862
+ ...rows.map(row => `| ${row.join(' | ')} |`),
3863
+ ].join('\n');
3728
3864
  }
3729
3865
 
3730
- export async function submitDocumentInsertTable() {
3731
- const modal = state.modal;
3866
+ function normalizeSqlDefinition(sql = '') {
3867
+ const statement = String(sql ?? '').trim();
3732
3868
 
3733
- if (!canSubmitDocumentInsertModal(modal, 'document-insert-table')) {
3734
- return false;
3869
+ if (!statement) {
3870
+ return '';
3735
3871
  }
3736
3872
 
3737
- const query = getSelectedDocumentInsertQuery(modal);
3873
+ return /;\s*$/.test(statement) ? statement : `${statement};`;
3874
+ }
3738
3875
 
3739
- if (!query) {
3740
- setDocumentInsertModalError('Select a saved query before inserting a table.');
3741
- return false;
3742
- }
3876
+ function buildTableDefinitionSqlBlock(table = {}) {
3877
+ const statement = normalizeSqlDefinition(table.ddl);
3743
3878
 
3744
- startModalSubmission();
3879
+ if (!statement) {
3880
+ return '';
3881
+ }
3745
3882
 
3746
- try {
3747
- const response = await api.getQueryExport(query.rawSql, 'md');
3748
- const markdownTable = String(response.data?.content ?? '').trim();
3883
+ return ['```sql', statement, '```'].join('\n');
3884
+ }
3749
3885
 
3750
- if (!markdownTable) {
3751
- throw new Error('The selected query returned no Markdown table content.');
3752
- }
3886
+ function formatTableDefinitionSampleValue(value) {
3887
+ if (value && typeof value === 'object' && value.__type === 'blob') {
3888
+ return `[BLOB - ${formatBytes(value.sizeBytes ?? 0)}]`;
3889
+ }
3753
3890
 
3754
- const inserted = insertMarkdownIntoCurrentDocumentDraft(markdownTable, modal.insertionRange);
3891
+ if (value === null) {
3892
+ return 'NULL';
3893
+ }
3755
3894
 
3756
- closeModalInternal();
3757
- if (inserted) {
3758
- pushToast(`Inserted table from "${getDocumentInsertQueryTitle(query)}".`, 'success');
3759
- }
3760
- return inserted;
3761
- } catch (error) {
3762
- withModalError(error);
3763
- return false;
3895
+ if (value === undefined) {
3896
+ return '';
3764
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;
3765
3908
  }
3766
3909
 
3767
- export function submitDocumentInsertNote() {
3768
- const modal = state.modal;
3910
+ function isNumericTableDefinitionColumn(table = {}, columnName = '') {
3911
+ const column = (table.columns ?? []).find(candidate => candidate.name === columnName);
3912
+ const affinity = String(column?.affinity ?? column?.declaredType ?? '').toUpperCase();
3769
3913
 
3770
- if (!canSubmitDocumentInsertModal(modal, 'document-insert-note')) {
3771
- return false;
3772
- }
3914
+ return ['INTEGER', 'REAL', 'NUMERIC'].includes(affinity);
3915
+ }
3773
3916
 
3774
- const query = getSelectedDocumentInsertQuery(modal);
3775
- const note = String(query?.notes ?? '').trim();
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));
3776
3921
 
3777
- if (!query || !note) {
3778
- setDocumentInsertModalError('Select a saved query with notes before inserting.');
3779
- return false;
3922
+ if (!rows.length) {
3923
+ return ['### Sample Data', '', '_No rows available._'].join('\n');
3780
3924
  }
3781
3925
 
3782
- startModalSubmission();
3783
- const inserted = insertMarkdownIntoCurrentDocumentDraft(note, modal.insertionRange);
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])));
3784
3932
 
3785
- closeModalInternal();
3786
- if (inserted) {
3787
- pushToast(`Inserted note from "${getDocumentInsertQueryTitle(query)}".`, 'success');
3788
- }
3789
- return inserted;
3933
+ return `| ${cells.join(' | ')} |`;
3934
+ });
3935
+
3936
+ return ['### Sample Data', '', header, alignment, ...rowLines].join('\n');
3790
3937
  }
3791
3938
 
3792
- export async function createDocument(options = {}) {
3793
- state.documents.saving = true;
3794
- state.documents.saveError = null;
3795
- emitChange();
3939
+ function buildMissingTableDefinitionSnippetBody(tableName, message = 'Table no longer exists.') {
3940
+ return [`## Definition: ${normalizeMarkdownInlineText(tableName || 'Table')}`, '', message].join('\n');
3941
+ }
3796
3942
 
3797
- try {
3798
- const response = await api.createDocument({
3799
- title: options.title,
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,
3800
4939
  filename: options.filename,
3801
4940
  content: options.content,
4941
+ folderId: options.folderId,
3802
4942
  });
3803
4943
  const document = response.data ?? null;
3804
4944
 
@@ -3832,13 +4972,19 @@ export async function saveCurrentDocument(options = {}) {
3832
4972
  }
3833
4973
 
3834
4974
  const submittedFilename = state.documents.draftFilename;
3835
- const submittedContent = state.documents.draftContent;
4975
+ const submittedDraftContent = state.documents.draftContent;
3836
4976
 
3837
4977
  state.documents.saving = true;
3838
4978
  state.documents.saveError = null;
3839
- emitChange();
3840
4979
 
3841
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
+ });
3842
4988
  const response = await api.updateDocument(documentId, {
3843
4989
  filename: submittedFilename,
3844
4990
  content: submittedContent,
@@ -3849,7 +4995,7 @@ export async function saveCurrentDocument(options = {}) {
3849
4995
  const draftUnchanged =
3850
4996
  String(state.documents.selectedId ?? '') === String(documentId) &&
3851
4997
  state.documents.draftFilename === submittedFilename &&
3852
- state.documents.draftContent === submittedContent;
4998
+ state.documents.draftContent === submittedDraftContent;
3853
4999
 
3854
5000
  upsertDocumentSummary(document);
3855
5001
  if (draftUnchanged) {
@@ -3913,15 +5059,15 @@ export async function deleteCurrentDocument(options = {}) {
3913
5059
  }
3914
5060
  }
3915
5061
 
3916
- export async function toggleCurrentDocumentTodo(lineIndex) {
5062
+ export function toggleCurrentDocumentTodo(lineIndex) {
3917
5063
  if (!state.documents.selectedId) {
3918
- return null;
5064
+ return false;
3919
5065
  }
3920
5066
 
3921
5067
  const nextContent = toggleMarkdownTodoLine(state.documents.draftContent, lineIndex);
3922
5068
 
3923
5069
  if (nextContent === state.documents.draftContent) {
3924
- return state.documents.selected;
5070
+ return false;
3925
5071
  }
3926
5072
 
3927
5073
  state.documents.draftContent = nextContent;
@@ -3929,7 +5075,7 @@ export async function toggleCurrentDocumentTodo(lineIndex) {
3929
5075
  state.documents.saveError = null;
3930
5076
  emitChange();
3931
5077
 
3932
- return saveCurrentDocument({ toast: false });
5078
+ return true;
3933
5079
  }
3934
5080
 
3935
5081
  export async function createDocumentFromMarkdownExport({ filename, content, title } = {}) {
@@ -3961,6 +5107,136 @@ export async function submitDeleteDocumentConfirmation() {
3961
5107
  return result;
3962
5108
  }
3963
5109
 
5110
+ function normalizeConnectionTagDrafts(tags = []) {
5111
+ const draftsByKey = new Map();
5112
+
5113
+ for (const tag of tags) {
5114
+ const name = normalizeConnectionTagName(tag?.name ?? tag);
5115
+
5116
+ if (!name) {
5117
+ continue;
5118
+ }
5119
+
5120
+ const key = normalizeConnectionTagKey(name);
5121
+
5122
+ if (!draftsByKey.has(key)) {
5123
+ draftsByKey.set(key, {
5124
+ id: tag?.id ?? null,
5125
+ name,
5126
+ });
5127
+ }
5128
+ }
5129
+
5130
+ return [...draftsByKey.values()];
5131
+ }
5132
+
5133
+ function findKnownConnectionTagByName(name) {
5134
+ const key = normalizeConnectionTagKey(name);
5135
+
5136
+ return state.connections.tags.find(tag => normalizeConnectionTagKey(tag.name) === key) ?? null;
5137
+ }
5138
+
5139
+ export function setConnectionSearchQuery(query) {
5140
+ const nextQuery = String(query ?? '');
5141
+
5142
+ if (state.connections.searchQuery === nextQuery) {
5143
+ return;
5144
+ }
5145
+
5146
+ state.connections.searchQuery = nextQuery;
5147
+ emitChange();
5148
+ }
5149
+
5150
+ export function toggleConnectionTagFilter(tagId, selected) {
5151
+ const normalizedTagId = String(tagId ?? '').trim();
5152
+
5153
+ if (!normalizedTagId) {
5154
+ return;
5155
+ }
5156
+
5157
+ const currentIds = new Set(state.connections.selectedTagIds.map(id => String(id)));
5158
+
5159
+ if (selected) {
5160
+ currentIds.add(normalizedTagId);
5161
+ } else {
5162
+ currentIds.delete(normalizedTagId);
5163
+ }
5164
+
5165
+ const nextTagIds = [...currentIds];
5166
+
5167
+ if (JSON.stringify(nextTagIds) === JSON.stringify(state.connections.selectedTagIds)) {
5168
+ return;
5169
+ }
5170
+
5171
+ state.connections.selectedTagIds = nextTagIds;
5172
+ emitChange();
5173
+ }
5174
+
5175
+ export function clearConnectionTagFilters() {
5176
+ if (!state.connections.selectedTagIds.length) {
5177
+ return;
5178
+ }
5179
+
5180
+ state.connections.selectedTagIds = [];
5181
+ emitChange();
5182
+ }
5183
+
5184
+ export function updateEditConnectionTagQuery(query) {
5185
+ if (state.modal?.kind !== 'edit-connection') {
5186
+ return;
5187
+ }
5188
+
5189
+ state.modal.tagQuery = String(query ?? '');
5190
+ state.modal.tagError = null;
5191
+ emitChange();
5192
+ }
5193
+
5194
+ export function addEditConnectionTag(tagName) {
5195
+ if (state.modal?.kind !== 'edit-connection') {
5196
+ return;
5197
+ }
5198
+
5199
+ const name = normalizeConnectionTagName(tagName);
5200
+
5201
+ if (!name) {
5202
+ state.modal.tagError = 'Tag names must be 1-40 characters.';
5203
+ emitChange();
5204
+ return;
5205
+ }
5206
+
5207
+ const currentTags = normalizeConnectionTagDrafts(state.modal.assignedTags);
5208
+ const key = normalizeConnectionTagKey(name);
5209
+
5210
+ if (currentTags.some(tag => normalizeConnectionTagKey(tag.name) === key)) {
5211
+ state.modal.tagQuery = '';
5212
+ state.modal.tagError = null;
5213
+ emitChange();
5214
+ return;
5215
+ }
5216
+
5217
+ const knownTag = findKnownConnectionTagByName(name);
5218
+ state.modal.assignedTags = [
5219
+ ...currentTags,
5220
+ knownTag ? { id: knownTag.id, name: knownTag.name } : { id: null, name },
5221
+ ];
5222
+ state.modal.tagQuery = '';
5223
+ state.modal.tagError = null;
5224
+ emitChange();
5225
+ }
5226
+
5227
+ export function removeEditConnectionTag(tagName) {
5228
+ if (state.modal?.kind !== 'edit-connection') {
5229
+ return;
5230
+ }
5231
+
5232
+ const key = normalizeConnectionTagKey(tagName);
5233
+ state.modal.assignedTags = normalizeConnectionTagDrafts(state.modal.assignedTags).filter(
5234
+ tag => normalizeConnectionTagKey(tag.name) !== key,
5235
+ );
5236
+ state.modal.tagError = null;
5237
+ emitChange();
5238
+ }
5239
+
3964
5240
  export function openEditConnectionModal(id) {
3965
5241
  const connection = state.connections.recent.find(entry => entry.id === id);
3966
5242
 
@@ -3973,6 +5249,9 @@ export function openEditConnectionModal(id) {
3973
5249
  kind: 'edit-connection',
3974
5250
  connectionId: connection.id,
3975
5251
  connection,
5252
+ assignedTags: normalizeConnectionTagDrafts(connection.tags),
5253
+ tagQuery: '',
5254
+ tagError: null,
3976
5255
  error: null,
3977
5256
  submitting: false,
3978
5257
  };
@@ -4158,6 +5437,11 @@ export async function openDataRowUpdatePreview(rowIndex, values, identity = null
4158
5437
  return null;
4159
5438
  }
4160
5439
 
5440
+ if (state.dataBrowser.table?.isShadow) {
5441
+ pushToast('Shadow tables are read-only in Data.', 'alert');
5442
+ return null;
5443
+ }
5444
+
4161
5445
  state.dataBrowser.saving = true;
4162
5446
  state.dataBrowser.saveError = null;
4163
5447
  emitChange();
@@ -5192,6 +6476,19 @@ export async function selectStructureEntry(name) {
5192
6476
  await loadStructureDetail(++routeLoadVersion);
5193
6477
  }
5194
6478
 
6479
+ export function clearStructureSelection() {
6480
+ const hadSelection = Boolean(state.structure.selectedName || state.structure.detail || state.structure.detailLoading);
6481
+
6482
+ routeLoadVersion += 1;
6483
+ state.structure.selectedName = null;
6484
+ state.structure.detail = null;
6485
+ state.structure.detailLoading = false;
6486
+
6487
+ if (hadSelection) {
6488
+ emitChange();
6489
+ }
6490
+ }
6491
+
5195
6492
  function getDefaultTypeGenerationOptions(target = 'typescript') {
5196
6493
  const propertyNaming = target === 'rust' ? 'snake' : 'camel';
5197
6494
 
@@ -5480,6 +6777,14 @@ export function updateCurrentTableDesignerColumnField(columnId, field, value, op
5480
6777
  value,
5481
6778
  getTableDesignerContext(),
5482
6779
  );
6780
+
6781
+ if (field === 'name') {
6782
+ state.tableDesigner.draft = refreshTableDesignerPresetChecksForColumnRename(
6783
+ state.tableDesigner.draft,
6784
+ columnId,
6785
+ );
6786
+ }
6787
+
5483
6788
  state.tableDesigner.saveError = null;
5484
6789
 
5485
6790
  if (options.notify !== false) {
@@ -5507,6 +6812,342 @@ export function updateCurrentTableDesignerConstraintField(constraintKind, constr
5507
6812
  }
5508
6813
  }
5509
6814
 
6815
+ function quoteSqlIdentifier(identifier) {
6816
+ return `"${String(identifier ?? '').replaceAll('"', '""')}"`;
6817
+ }
6818
+
6819
+ function quoteSqlStringLiteral(value) {
6820
+ return `'${String(value ?? '').replaceAll("'", "''")}'`;
6821
+ }
6822
+
6823
+ function getTableDesignerDrawerColumn() {
6824
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
6825
+ return (state.tableDesigner.draft?.columns ?? []).find(column => column.id === drawer.columnId) ?? null;
6826
+ }
6827
+
6828
+ function getTableDesignerCheckPresetFields(seed = {}) {
6829
+ return {
6830
+ minLength: String(seed.minLength ?? '3'),
6831
+ maxLength: String(seed.maxLength ?? '255'),
6832
+ minValue: String(seed.minValue ?? '0'),
6833
+ maxValue: String(seed.maxValue ?? '100'),
6834
+ allowedValues: String(seed.allowedValues ?? 'draft\npublished\narchived'),
6835
+ };
6836
+ }
6837
+
6838
+ export function buildTableDesignerCheckPresetExpression(presetId, column, fields = {}) {
6839
+ const columnSql = quoteSqlIdentifier(column?.name || 'column_name');
6840
+ const presetFields = getTableDesignerCheckPresetFields(fields);
6841
+ const allowedValues = presetFields.allowedValues
6842
+ .split(/\r?\n|,/)
6843
+ .map(value => value.trim())
6844
+ .filter(Boolean);
6845
+
6846
+ switch (presetId) {
6847
+ case 'text-non-empty':
6848
+ return `length(trim(${columnSql})) > 0`;
6849
+ case 'text-min-length':
6850
+ return `length(${columnSql}) >= ${Number(presetFields.minLength) || 0}`;
6851
+ case 'text-max-length':
6852
+ return `length(${columnSql}) <= ${Number(presetFields.maxLength) || 0}`;
6853
+ case 'text-length-range':
6854
+ return `length(${columnSql}) BETWEEN ${Number(presetFields.minLength) || 0} AND ${Number(presetFields.maxLength) || 0}`;
6855
+ case 'text-allowed-values':
6856
+ return `${columnSql} IN (${allowedValues.map(quoteSqlStringLiteral).join(', ')})`;
6857
+ case 'numeric-positive':
6858
+ return `${columnSql} > 0`;
6859
+ case 'numeric-non-negative':
6860
+ return `${columnSql} >= 0`;
6861
+ case 'numeric-min-value':
6862
+ return `${columnSql} >= ${Number(presetFields.minValue) || 0}`;
6863
+ case 'numeric-max-value':
6864
+ return `${columnSql} <= ${Number(presetFields.maxValue) || 0}`;
6865
+ case 'numeric-range':
6866
+ return `${columnSql} BETWEEN ${Number(presetFields.minValue) || 0} AND ${Number(presetFields.maxValue) || 0}`;
6867
+ case 'numeric-non-zero':
6868
+ return `${columnSql} <> 0`;
6869
+ case 'boolean-integer':
6870
+ return `${columnSql} IN (0, 1)`;
6871
+ default:
6872
+ return '';
6873
+ }
6874
+ }
6875
+
6876
+ function refreshTableDesignerPresetChecksForColumnRename(draft, columnId) {
6877
+ const column = (draft.columns ?? []).find(candidate => candidate.id === columnId);
6878
+
6879
+ if (!column) {
6880
+ return draft;
6881
+ }
6882
+
6883
+ let changed = false;
6884
+ const checkConstraints = (draft.checkConstraints ?? []).map(constraint => {
6885
+ if (
6886
+ constraint.deleted ||
6887
+ constraint.columnId !== columnId ||
6888
+ !constraint.presetId ||
6889
+ constraint.source !== 'preset'
6890
+ ) {
6891
+ return constraint;
6892
+ }
6893
+
6894
+ const expression = buildTableDesignerCheckPresetExpression(
6895
+ constraint.presetId,
6896
+ column,
6897
+ constraint.presetFields,
6898
+ );
6899
+
6900
+ if (!expression || expression === constraint.expression) {
6901
+ return constraint;
6902
+ }
6903
+
6904
+ changed = true;
6905
+ return {
6906
+ ...constraint,
6907
+ columns: [{ name: column.name, allowedValues: constraint.columns?.[0]?.allowedValues ?? [] }],
6908
+ expression,
6909
+ };
6910
+ });
6911
+
6912
+ return changed
6913
+ ? recalculateTableDesignerDraft(
6914
+ {
6915
+ ...draft,
6916
+ checkConstraints,
6917
+ },
6918
+ getTableDesignerContext(),
6919
+ )
6920
+ : draft;
6921
+ }
6922
+
6923
+ function createTableDesignerCheckEditor(seed = {}) {
6924
+ return {
6925
+ expression: String(seed.expression ?? ''),
6926
+ presetId: String(seed.presetId ?? ''),
6927
+ presetFields: getTableDesignerCheckPresetFields(seed.presetFields),
6928
+ error: null,
6929
+ validating: false,
6930
+ };
6931
+ }
6932
+
6933
+ export function openTableDesignerConstraintsDrawer(columnId = '', columnName = '') {
6934
+ const column = (state.tableDesigner.draft?.columns ?? []).find(candidate => candidate.id === columnId);
6935
+
6936
+ state.tableDesigner.constraintsDrawer = {
6937
+ visible: true,
6938
+ columnId: String(columnId ?? ''),
6939
+ columnName: String(columnName || column?.name || ''),
6940
+ editingConstraintId: '',
6941
+ editor: null,
6942
+ };
6943
+ emitChange();
6944
+ }
6945
+
6946
+ export function closeTableDesignerConstraintsDrawer() {
6947
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
6948
+ emitChange();
6949
+ }
6950
+
6951
+ export function startTableDesignerCheckEditor(constraintId = '') {
6952
+ const constraint = (state.tableDesigner.draft?.checkConstraints ?? []).find(item => item.id === constraintId);
6953
+
6954
+ state.tableDesigner.constraintsDrawer = {
6955
+ ...(state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer()),
6956
+ visible: true,
6957
+ editingConstraintId: String(constraintId ?? ''),
6958
+ editor: createTableDesignerCheckEditor({
6959
+ expression: constraint?.expression ?? '',
6960
+ presetId: constraint?.presetId ?? '',
6961
+ presetFields: constraint?.presetFields ?? {},
6962
+ }),
6963
+ };
6964
+ emitChange();
6965
+ }
6966
+
6967
+ export function cancelTableDesignerCheckEditor() {
6968
+ state.tableDesigner.constraintsDrawer = {
6969
+ ...(state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer()),
6970
+ editingConstraintId: '',
6971
+ editor: null,
6972
+ };
6973
+ emitChange();
6974
+ }
6975
+
6976
+ export function applyTableDesignerCheckPreset(presetId) {
6977
+ const column = getTableDesignerDrawerColumn();
6978
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
6979
+ const editor = drawer.editor ?? createTableDesignerCheckEditor();
6980
+ const presetFields = getTableDesignerCheckPresetFields(editor.presetFields);
6981
+ const expression = buildTableDesignerCheckPresetExpression(presetId, column, presetFields);
6982
+
6983
+ state.tableDesigner.constraintsDrawer = {
6984
+ ...drawer,
6985
+ editor: {
6986
+ ...editor,
6987
+ presetId,
6988
+ presetFields,
6989
+ expression,
6990
+ error: null,
6991
+ },
6992
+ };
6993
+ emitChange();
6994
+ }
6995
+
6996
+ export function updateTableDesignerCheckEditorField(field, value, options = {}) {
6997
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
6998
+ const editor = drawer.editor ?? createTableDesignerCheckEditor();
6999
+ const nextEditor = { ...editor, error: null };
7000
+
7001
+ if (field in getTableDesignerCheckPresetFields()) {
7002
+ nextEditor.presetFields = {
7003
+ ...getTableDesignerCheckPresetFields(editor.presetFields),
7004
+ [field]: String(value ?? ''),
7005
+ };
7006
+
7007
+ if (editor.presetId) {
7008
+ nextEditor.expression = buildTableDesignerCheckPresetExpression(
7009
+ editor.presetId,
7010
+ getTableDesignerDrawerColumn(),
7011
+ nextEditor.presetFields,
7012
+ );
7013
+ }
7014
+ } else {
7015
+ nextEditor[field] = String(value ?? '');
7016
+
7017
+ if (field === 'expression') {
7018
+ nextEditor.presetId = '';
7019
+ }
7020
+ }
7021
+
7022
+ state.tableDesigner.constraintsDrawer = {
7023
+ ...drawer,
7024
+ editor: nextEditor,
7025
+ };
7026
+
7027
+ if (options.notify !== false) {
7028
+ emitChange();
7029
+ }
7030
+ }
7031
+
7032
+ export async function saveTableDesignerCheckEditor() {
7033
+ const draft = state.tableDesigner.draft;
7034
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
7035
+ const editor = drawer.editor ?? createTableDesignerCheckEditor();
7036
+ const expression = String(editor.expression ?? '').trim();
7037
+ const column = getTableDesignerDrawerColumn();
7038
+
7039
+ if (!draft) {
7040
+ return false;
7041
+ }
7042
+
7043
+ if (!expression) {
7044
+ state.tableDesigner.constraintsDrawer = {
7045
+ ...drawer,
7046
+ editor: {
7047
+ ...editor,
7048
+ error: { code: 'CHECK_EXPRESSION_REQUIRED', message: 'CHECK expression is required.' },
7049
+ },
7050
+ };
7051
+ emitChange();
7052
+ return false;
7053
+ }
7054
+
7055
+ state.tableDesigner.constraintsDrawer = {
7056
+ ...drawer,
7057
+ editor: {
7058
+ ...editor,
7059
+ validating: true,
7060
+ error: null,
7061
+ },
7062
+ };
7063
+ emitChange();
7064
+
7065
+ try {
7066
+ await api.validateTableDesignerCheck({ draft, expression });
7067
+
7068
+ const seed = {
7069
+ expression,
7070
+ columnId: drawer.columnId,
7071
+ columns: column ? [{ name: column.name, allowedValues: [] }] : [],
7072
+ source: editor.presetId ? 'preset' : 'user',
7073
+ presetId: editor.presetId,
7074
+ presetFields: editor.presetFields,
7075
+ };
7076
+
7077
+ state.tableDesigner.draft = drawer.editingConstraintId
7078
+ ? updateTableDesignerConstraintField(
7079
+ draft,
7080
+ 'check',
7081
+ drawer.editingConstraintId,
7082
+ 'expression',
7083
+ expression,
7084
+ getTableDesignerContext(),
7085
+ )
7086
+ : addTableDesignerCheckConstraint(draft, seed, getTableDesignerContext());
7087
+
7088
+ if (drawer.editingConstraintId) {
7089
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
7090
+ state.tableDesigner.draft,
7091
+ 'check',
7092
+ drawer.editingConstraintId,
7093
+ 'presetId',
7094
+ editor.presetId,
7095
+ getTableDesignerContext(),
7096
+ );
7097
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
7098
+ state.tableDesigner.draft,
7099
+ 'check',
7100
+ drawer.editingConstraintId,
7101
+ 'presetFields',
7102
+ editor.presetFields,
7103
+ getTableDesignerContext(),
7104
+ );
7105
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
7106
+ state.tableDesigner.draft,
7107
+ 'check',
7108
+ drawer.editingConstraintId,
7109
+ 'source',
7110
+ editor.presetId ? 'preset' : 'user',
7111
+ getTableDesignerContext(),
7112
+ );
7113
+ }
7114
+
7115
+ state.tableDesigner.saveError = null;
7116
+ state.tableDesigner.constraintsDrawer = {
7117
+ ...drawer,
7118
+ editingConstraintId: '',
7119
+ editor: null,
7120
+ };
7121
+ emitChange();
7122
+ return true;
7123
+ } catch (error) {
7124
+ state.tableDesigner.constraintsDrawer = {
7125
+ ...drawer,
7126
+ editor: {
7127
+ ...editor,
7128
+ validating: false,
7129
+ error: normalizeError(error),
7130
+ },
7131
+ };
7132
+ emitChange();
7133
+ return false;
7134
+ }
7135
+ }
7136
+
7137
+ export function removeCurrentTableDesignerCheckConstraint(constraintId) {
7138
+ if (!state.tableDesigner.draft) {
7139
+ return;
7140
+ }
7141
+
7142
+ state.tableDesigner.draft = removeTableDesignerCheckConstraint(
7143
+ state.tableDesigner.draft,
7144
+ constraintId,
7145
+ getTableDesignerContext(),
7146
+ );
7147
+ state.tableDesigner.saveError = null;
7148
+ emitChange();
7149
+ }
7150
+
5510
7151
  export function addCurrentTableDesignerColumn() {
5511
7152
  if (!state.tableDesigner.draft) {
5512
7153
  return null;
@@ -5522,10 +7163,12 @@ export function addCurrentTableDesignerColumn() {
5522
7163
  return nextColumn?.id ?? null;
5523
7164
  }
5524
7165
 
5525
- export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
7166
+ export function queueTableDesignerImport(fileName, text, options = {}) {
7167
+ const format = String(options.format ?? '').trim().toLowerCase();
7168
+
5526
7169
  try {
5527
- const imported = createTableDesignerDraftFromCsvImport(
5528
- { fileName, csvText },
7170
+ const imported = createTableDesignerDraftFromImport(
7171
+ { fileName, text, format },
5529
7172
  {
5530
7173
  ...getTableDesignerContext(),
5531
7174
  ...(options.context ?? {}),
@@ -5543,11 +7186,15 @@ export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
5543
7186
  throw error;
5544
7187
  }
5545
7188
 
5546
- pushToast(error?.message || 'CSV import failed.', 'alert');
7189
+ pushToast(error?.message || 'Data import failed.', 'alert');
5547
7190
  return null;
5548
7191
  }
5549
7192
  }
5550
7193
 
7194
+ export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
7195
+ return queueTableDesignerImport(fileName, csvText, { ...options, format: 'csv' });
7196
+ }
7197
+
5551
7198
  export function removeCurrentTableDesignerColumn(columnId) {
5552
7199
  if (!state.tableDesigner.draft) {
5553
7200
  return;
@@ -6190,6 +7837,11 @@ export async function submitDataRowUpdate(rowIndex, values, identity = null, opt
6190
7837
  return null;
6191
7838
  }
6192
7839
 
7840
+ if (state.dataBrowser.table?.isShadow) {
7841
+ pushToast('Shadow tables are read-only in Data.', 'alert');
7842
+ return null;
7843
+ }
7844
+
6193
7845
  state.dataBrowser.saving = true;
6194
7846
  state.dataBrowser.saveError = null;
6195
7847
  emitChange();
@@ -6228,6 +7880,11 @@ export async function submitDataRowDelete(rowIndex, options = {}) {
6228
7880
  return null;
6229
7881
  }
6230
7882
 
7883
+ if (state.dataBrowser.table?.isShadow) {
7884
+ pushToast('Shadow tables are read-only in Data.', 'alert');
7885
+ return null;
7886
+ }
7887
+
6231
7888
  const shouldStepBackPage =
6232
7889
  selected.rowIndex !== null && (state.dataBrowser.table?.rows?.length ?? 0) <= 1 && state.dataBrowser.page > 1;
6233
7890