sqlite-hub 2.0.0 → 2.1.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 (89) hide show
  1. package/README.md +15 -10
  2. package/docs/MCP.md +36 -4
  3. package/docs/changelog.md +11 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/settings.js +2 -2
  61. package/frontend/js/views/structure.js +28 -5
  62. package/frontend/js/views/tableAdvisor.js +14 -6
  63. package/frontend/js/views/tableDesigner.js +44 -9
  64. package/frontend/styles/components.css +676 -79
  65. package/frontend/styles/structure-graph.css +49 -0
  66. package/frontend/styles/tailwind.generated.css +1 -1
  67. package/package.json +1 -1
  68. package/server/mcp/httpRouter.js +95 -0
  69. package/server/routes/connections.js +12 -0
  70. package/server/routes/data.js +1 -1
  71. package/server/routes/settings.js +29 -7
  72. package/server/routes/tableDesigner.js +15 -1
  73. package/server/server.js +15 -0
  74. package/server/services/mcpToolService.js +28 -0
  75. package/server/services/sqlite/connectionManager.js +23 -2
  76. package/server/services/sqlite/dataBrowserService.js +21 -0
  77. package/server/services/sqlite/introspection.js +39 -1
  78. package/server/services/sqlite/structureService.js +66 -11
  79. package/server/services/sqlite/tableAdvisor.js +6 -0
  80. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  81. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  82. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  83. package/server/services/sqlite/tableDesignerService.js +110 -5
  84. package/server/services/storage/appStateStore.js +303 -4
  85. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  86. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  87. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  88. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  89. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -2,9 +2,12 @@ import * as api from './api.js';
2
2
  import { formatCellValue, 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,
@@ -465,6 +485,7 @@ const state = {
465
485
  tablesVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true),
466
486
  sqlPreviewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true),
467
487
  pendingImportedDraft: null,
488
+ constraintsDrawer: createClosedTableDesignerConstraintsDrawer(),
468
489
  loading: false,
469
490
  detailLoading: false,
470
491
  saving: false,
@@ -1531,13 +1552,17 @@ async function refreshConnectionsState() {
1531
1552
  emitChange();
1532
1553
 
1533
1554
  try {
1534
- const [recentResponse, activeResponse] = await Promise.all([
1555
+ const [recentResponse, activeResponse, tagsResponse] = await Promise.all([
1535
1556
  api.getRecentConnections(),
1536
1557
  api.getActiveConnection(),
1558
+ api.getConnectionTags(),
1537
1559
  ]);
1538
1560
 
1539
1561
  state.connections.recent = recentResponse.data ?? [];
1540
1562
  state.connections.active = activeResponse.data ?? null;
1563
+ state.connections.tags = tagsResponse.data ?? [];
1564
+ const availableTagIds = new Set(state.connections.tags.map(tag => String(tag.id)));
1565
+ state.connections.selectedTagIds = state.connections.selectedTagIds.filter(tagId => availableTagIds.has(String(tagId)));
1541
1566
  state.connections.error = null;
1542
1567
  } catch (error) {
1543
1568
  state.connections.error = normalizeError(error);
@@ -2474,7 +2499,7 @@ async function loadTableAdvisor(version, route) {
2474
2499
  return;
2475
2500
  }
2476
2501
 
2477
- const tables = response.data?.tables ?? [];
2502
+ const tables = (response.data?.tables ?? []).filter(table => !table?.isShadow);
2478
2503
  const requestedTableName = route.params?.tableName ?? null;
2479
2504
  const selectedTableName =
2480
2505
  requestedTableName && tables.some(table => table.name === requestedTableName)
@@ -2613,6 +2638,7 @@ async function loadTableDesignerDetail(version, tableName) {
2613
2638
  }
2614
2639
 
2615
2640
  state.tableDesigner.selectedTableName = tableName;
2641
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
2616
2642
  state.tableDesigner.draft = decorateTableDesignerDraft(response.data?.draft ?? null);
2617
2643
  } catch (error) {
2618
2644
  if (version !== routeLoadVersion) {
@@ -2649,6 +2675,7 @@ async function loadTableDesigner(version, route) {
2649
2675
  state.tableDesigner.selectedTableName = null;
2650
2676
  state.tableDesigner.detailLoading = false;
2651
2677
  state.tableDesigner.pendingImportedDraft = null;
2678
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
2652
2679
  state.tableDesigner.draft = decorateTableDesignerDraft(importedDraft ?? createNewTableDesignerDraft());
2653
2680
  return;
2654
2681
  }
@@ -2665,6 +2692,7 @@ async function loadTableDesigner(version, route) {
2665
2692
  if (!tableName) {
2666
2693
  state.tableDesigner.selectedTableName = null;
2667
2694
  state.tableDesigner.detailLoading = false;
2695
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
2668
2696
  state.tableDesigner.draft = null;
2669
2697
  return;
2670
2698
  }
@@ -3221,6 +3249,11 @@ export function openGenerateDataModal() {
3221
3249
  return;
3222
3250
  }
3223
3251
 
3252
+ if (table.isShadow) {
3253
+ pushToast('Shadow tables are read-only in Data.', 'alert');
3254
+ return;
3255
+ }
3256
+
3224
3257
  const columns = (table.columnMeta ?? []).filter(column => column.visible && !column.generated);
3225
3258
 
3226
3259
  state.modal = {
@@ -3961,6 +3994,136 @@ export async function submitDeleteDocumentConfirmation() {
3961
3994
  return result;
3962
3995
  }
3963
3996
 
3997
+ function normalizeConnectionTagDrafts(tags = []) {
3998
+ const draftsByKey = new Map();
3999
+
4000
+ for (const tag of tags) {
4001
+ const name = normalizeConnectionTagName(tag?.name ?? tag);
4002
+
4003
+ if (!name) {
4004
+ continue;
4005
+ }
4006
+
4007
+ const key = normalizeConnectionTagKey(name);
4008
+
4009
+ if (!draftsByKey.has(key)) {
4010
+ draftsByKey.set(key, {
4011
+ id: tag?.id ?? null,
4012
+ name,
4013
+ });
4014
+ }
4015
+ }
4016
+
4017
+ return [...draftsByKey.values()];
4018
+ }
4019
+
4020
+ function findKnownConnectionTagByName(name) {
4021
+ const key = normalizeConnectionTagKey(name);
4022
+
4023
+ return state.connections.tags.find(tag => normalizeConnectionTagKey(tag.name) === key) ?? null;
4024
+ }
4025
+
4026
+ export function setConnectionSearchQuery(query) {
4027
+ const nextQuery = String(query ?? '');
4028
+
4029
+ if (state.connections.searchQuery === nextQuery) {
4030
+ return;
4031
+ }
4032
+
4033
+ state.connections.searchQuery = nextQuery;
4034
+ emitChange();
4035
+ }
4036
+
4037
+ export function toggleConnectionTagFilter(tagId, selected) {
4038
+ const normalizedTagId = String(tagId ?? '').trim();
4039
+
4040
+ if (!normalizedTagId) {
4041
+ return;
4042
+ }
4043
+
4044
+ const currentIds = new Set(state.connections.selectedTagIds.map(id => String(id)));
4045
+
4046
+ if (selected) {
4047
+ currentIds.add(normalizedTagId);
4048
+ } else {
4049
+ currentIds.delete(normalizedTagId);
4050
+ }
4051
+
4052
+ const nextTagIds = [...currentIds];
4053
+
4054
+ if (JSON.stringify(nextTagIds) === JSON.stringify(state.connections.selectedTagIds)) {
4055
+ return;
4056
+ }
4057
+
4058
+ state.connections.selectedTagIds = nextTagIds;
4059
+ emitChange();
4060
+ }
4061
+
4062
+ export function clearConnectionTagFilters() {
4063
+ if (!state.connections.selectedTagIds.length) {
4064
+ return;
4065
+ }
4066
+
4067
+ state.connections.selectedTagIds = [];
4068
+ emitChange();
4069
+ }
4070
+
4071
+ export function updateEditConnectionTagQuery(query) {
4072
+ if (state.modal?.kind !== 'edit-connection') {
4073
+ return;
4074
+ }
4075
+
4076
+ state.modal.tagQuery = String(query ?? '');
4077
+ state.modal.tagError = null;
4078
+ emitChange();
4079
+ }
4080
+
4081
+ export function addEditConnectionTag(tagName) {
4082
+ if (state.modal?.kind !== 'edit-connection') {
4083
+ return;
4084
+ }
4085
+
4086
+ const name = normalizeConnectionTagName(tagName);
4087
+
4088
+ if (!name) {
4089
+ state.modal.tagError = 'Tag names must be 1-40 characters.';
4090
+ emitChange();
4091
+ return;
4092
+ }
4093
+
4094
+ const currentTags = normalizeConnectionTagDrafts(state.modal.assignedTags);
4095
+ const key = normalizeConnectionTagKey(name);
4096
+
4097
+ if (currentTags.some(tag => normalizeConnectionTagKey(tag.name) === key)) {
4098
+ state.modal.tagQuery = '';
4099
+ state.modal.tagError = null;
4100
+ emitChange();
4101
+ return;
4102
+ }
4103
+
4104
+ const knownTag = findKnownConnectionTagByName(name);
4105
+ state.modal.assignedTags = [
4106
+ ...currentTags,
4107
+ knownTag ? { id: knownTag.id, name: knownTag.name } : { id: null, name },
4108
+ ];
4109
+ state.modal.tagQuery = '';
4110
+ state.modal.tagError = null;
4111
+ emitChange();
4112
+ }
4113
+
4114
+ export function removeEditConnectionTag(tagName) {
4115
+ if (state.modal?.kind !== 'edit-connection') {
4116
+ return;
4117
+ }
4118
+
4119
+ const key = normalizeConnectionTagKey(tagName);
4120
+ state.modal.assignedTags = normalizeConnectionTagDrafts(state.modal.assignedTags).filter(
4121
+ tag => normalizeConnectionTagKey(tag.name) !== key,
4122
+ );
4123
+ state.modal.tagError = null;
4124
+ emitChange();
4125
+ }
4126
+
3964
4127
  export function openEditConnectionModal(id) {
3965
4128
  const connection = state.connections.recent.find(entry => entry.id === id);
3966
4129
 
@@ -3973,6 +4136,9 @@ export function openEditConnectionModal(id) {
3973
4136
  kind: 'edit-connection',
3974
4137
  connectionId: connection.id,
3975
4138
  connection,
4139
+ assignedTags: normalizeConnectionTagDrafts(connection.tags),
4140
+ tagQuery: '',
4141
+ tagError: null,
3976
4142
  error: null,
3977
4143
  submitting: false,
3978
4144
  };
@@ -4158,6 +4324,11 @@ export async function openDataRowUpdatePreview(rowIndex, values, identity = null
4158
4324
  return null;
4159
4325
  }
4160
4326
 
4327
+ if (state.dataBrowser.table?.isShadow) {
4328
+ pushToast('Shadow tables are read-only in Data.', 'alert');
4329
+ return null;
4330
+ }
4331
+
4161
4332
  state.dataBrowser.saving = true;
4162
4333
  state.dataBrowser.saveError = null;
4163
4334
  emitChange();
@@ -5192,6 +5363,19 @@ export async function selectStructureEntry(name) {
5192
5363
  await loadStructureDetail(++routeLoadVersion);
5193
5364
  }
5194
5365
 
5366
+ export function clearStructureSelection() {
5367
+ const hadSelection = Boolean(state.structure.selectedName || state.structure.detail || state.structure.detailLoading);
5368
+
5369
+ routeLoadVersion += 1;
5370
+ state.structure.selectedName = null;
5371
+ state.structure.detail = null;
5372
+ state.structure.detailLoading = false;
5373
+
5374
+ if (hadSelection) {
5375
+ emitChange();
5376
+ }
5377
+ }
5378
+
5195
5379
  function getDefaultTypeGenerationOptions(target = 'typescript') {
5196
5380
  const propertyNaming = target === 'rust' ? 'snake' : 'camel';
5197
5381
 
@@ -5480,6 +5664,14 @@ export function updateCurrentTableDesignerColumnField(columnId, field, value, op
5480
5664
  value,
5481
5665
  getTableDesignerContext(),
5482
5666
  );
5667
+
5668
+ if (field === 'name') {
5669
+ state.tableDesigner.draft = refreshTableDesignerPresetChecksForColumnRename(
5670
+ state.tableDesigner.draft,
5671
+ columnId,
5672
+ );
5673
+ }
5674
+
5483
5675
  state.tableDesigner.saveError = null;
5484
5676
 
5485
5677
  if (options.notify !== false) {
@@ -5507,6 +5699,342 @@ export function updateCurrentTableDesignerConstraintField(constraintKind, constr
5507
5699
  }
5508
5700
  }
5509
5701
 
5702
+ function quoteSqlIdentifier(identifier) {
5703
+ return `"${String(identifier ?? '').replaceAll('"', '""')}"`;
5704
+ }
5705
+
5706
+ function quoteSqlStringLiteral(value) {
5707
+ return `'${String(value ?? '').replaceAll("'", "''")}'`;
5708
+ }
5709
+
5710
+ function getTableDesignerDrawerColumn() {
5711
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
5712
+ return (state.tableDesigner.draft?.columns ?? []).find(column => column.id === drawer.columnId) ?? null;
5713
+ }
5714
+
5715
+ function getTableDesignerCheckPresetFields(seed = {}) {
5716
+ return {
5717
+ minLength: String(seed.minLength ?? '3'),
5718
+ maxLength: String(seed.maxLength ?? '255'),
5719
+ minValue: String(seed.minValue ?? '0'),
5720
+ maxValue: String(seed.maxValue ?? '100'),
5721
+ allowedValues: String(seed.allowedValues ?? 'draft\npublished\narchived'),
5722
+ };
5723
+ }
5724
+
5725
+ export function buildTableDesignerCheckPresetExpression(presetId, column, fields = {}) {
5726
+ const columnSql = quoteSqlIdentifier(column?.name || 'column_name');
5727
+ const presetFields = getTableDesignerCheckPresetFields(fields);
5728
+ const allowedValues = presetFields.allowedValues
5729
+ .split(/\r?\n|,/)
5730
+ .map(value => value.trim())
5731
+ .filter(Boolean);
5732
+
5733
+ switch (presetId) {
5734
+ case 'text-non-empty':
5735
+ return `length(trim(${columnSql})) > 0`;
5736
+ case 'text-min-length':
5737
+ return `length(${columnSql}) >= ${Number(presetFields.minLength) || 0}`;
5738
+ case 'text-max-length':
5739
+ return `length(${columnSql}) <= ${Number(presetFields.maxLength) || 0}`;
5740
+ case 'text-length-range':
5741
+ return `length(${columnSql}) BETWEEN ${Number(presetFields.minLength) || 0} AND ${Number(presetFields.maxLength) || 0}`;
5742
+ case 'text-allowed-values':
5743
+ return `${columnSql} IN (${allowedValues.map(quoteSqlStringLiteral).join(', ')})`;
5744
+ case 'numeric-positive':
5745
+ return `${columnSql} > 0`;
5746
+ case 'numeric-non-negative':
5747
+ return `${columnSql} >= 0`;
5748
+ case 'numeric-min-value':
5749
+ return `${columnSql} >= ${Number(presetFields.minValue) || 0}`;
5750
+ case 'numeric-max-value':
5751
+ return `${columnSql} <= ${Number(presetFields.maxValue) || 0}`;
5752
+ case 'numeric-range':
5753
+ return `${columnSql} BETWEEN ${Number(presetFields.minValue) || 0} AND ${Number(presetFields.maxValue) || 0}`;
5754
+ case 'numeric-non-zero':
5755
+ return `${columnSql} <> 0`;
5756
+ case 'boolean-integer':
5757
+ return `${columnSql} IN (0, 1)`;
5758
+ default:
5759
+ return '';
5760
+ }
5761
+ }
5762
+
5763
+ function refreshTableDesignerPresetChecksForColumnRename(draft, columnId) {
5764
+ const column = (draft.columns ?? []).find(candidate => candidate.id === columnId);
5765
+
5766
+ if (!column) {
5767
+ return draft;
5768
+ }
5769
+
5770
+ let changed = false;
5771
+ const checkConstraints = (draft.checkConstraints ?? []).map(constraint => {
5772
+ if (
5773
+ constraint.deleted ||
5774
+ constraint.columnId !== columnId ||
5775
+ !constraint.presetId ||
5776
+ constraint.source !== 'preset'
5777
+ ) {
5778
+ return constraint;
5779
+ }
5780
+
5781
+ const expression = buildTableDesignerCheckPresetExpression(
5782
+ constraint.presetId,
5783
+ column,
5784
+ constraint.presetFields,
5785
+ );
5786
+
5787
+ if (!expression || expression === constraint.expression) {
5788
+ return constraint;
5789
+ }
5790
+
5791
+ changed = true;
5792
+ return {
5793
+ ...constraint,
5794
+ columns: [{ name: column.name, allowedValues: constraint.columns?.[0]?.allowedValues ?? [] }],
5795
+ expression,
5796
+ };
5797
+ });
5798
+
5799
+ return changed
5800
+ ? recalculateTableDesignerDraft(
5801
+ {
5802
+ ...draft,
5803
+ checkConstraints,
5804
+ },
5805
+ getTableDesignerContext(),
5806
+ )
5807
+ : draft;
5808
+ }
5809
+
5810
+ function createTableDesignerCheckEditor(seed = {}) {
5811
+ return {
5812
+ expression: String(seed.expression ?? ''),
5813
+ presetId: String(seed.presetId ?? ''),
5814
+ presetFields: getTableDesignerCheckPresetFields(seed.presetFields),
5815
+ error: null,
5816
+ validating: false,
5817
+ };
5818
+ }
5819
+
5820
+ export function openTableDesignerConstraintsDrawer(columnId = '', columnName = '') {
5821
+ const column = (state.tableDesigner.draft?.columns ?? []).find(candidate => candidate.id === columnId);
5822
+
5823
+ state.tableDesigner.constraintsDrawer = {
5824
+ visible: true,
5825
+ columnId: String(columnId ?? ''),
5826
+ columnName: String(columnName || column?.name || ''),
5827
+ editingConstraintId: '',
5828
+ editor: null,
5829
+ };
5830
+ emitChange();
5831
+ }
5832
+
5833
+ export function closeTableDesignerConstraintsDrawer() {
5834
+ state.tableDesigner.constraintsDrawer = createClosedTableDesignerConstraintsDrawer();
5835
+ emitChange();
5836
+ }
5837
+
5838
+ export function startTableDesignerCheckEditor(constraintId = '') {
5839
+ const constraint = (state.tableDesigner.draft?.checkConstraints ?? []).find(item => item.id === constraintId);
5840
+
5841
+ state.tableDesigner.constraintsDrawer = {
5842
+ ...(state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer()),
5843
+ visible: true,
5844
+ editingConstraintId: String(constraintId ?? ''),
5845
+ editor: createTableDesignerCheckEditor({
5846
+ expression: constraint?.expression ?? '',
5847
+ presetId: constraint?.presetId ?? '',
5848
+ presetFields: constraint?.presetFields ?? {},
5849
+ }),
5850
+ };
5851
+ emitChange();
5852
+ }
5853
+
5854
+ export function cancelTableDesignerCheckEditor() {
5855
+ state.tableDesigner.constraintsDrawer = {
5856
+ ...(state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer()),
5857
+ editingConstraintId: '',
5858
+ editor: null,
5859
+ };
5860
+ emitChange();
5861
+ }
5862
+
5863
+ export function applyTableDesignerCheckPreset(presetId) {
5864
+ const column = getTableDesignerDrawerColumn();
5865
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
5866
+ const editor = drawer.editor ?? createTableDesignerCheckEditor();
5867
+ const presetFields = getTableDesignerCheckPresetFields(editor.presetFields);
5868
+ const expression = buildTableDesignerCheckPresetExpression(presetId, column, presetFields);
5869
+
5870
+ state.tableDesigner.constraintsDrawer = {
5871
+ ...drawer,
5872
+ editor: {
5873
+ ...editor,
5874
+ presetId,
5875
+ presetFields,
5876
+ expression,
5877
+ error: null,
5878
+ },
5879
+ };
5880
+ emitChange();
5881
+ }
5882
+
5883
+ export function updateTableDesignerCheckEditorField(field, value, options = {}) {
5884
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
5885
+ const editor = drawer.editor ?? createTableDesignerCheckEditor();
5886
+ const nextEditor = { ...editor, error: null };
5887
+
5888
+ if (field in getTableDesignerCheckPresetFields()) {
5889
+ nextEditor.presetFields = {
5890
+ ...getTableDesignerCheckPresetFields(editor.presetFields),
5891
+ [field]: String(value ?? ''),
5892
+ };
5893
+
5894
+ if (editor.presetId) {
5895
+ nextEditor.expression = buildTableDesignerCheckPresetExpression(
5896
+ editor.presetId,
5897
+ getTableDesignerDrawerColumn(),
5898
+ nextEditor.presetFields,
5899
+ );
5900
+ }
5901
+ } else {
5902
+ nextEditor[field] = String(value ?? '');
5903
+
5904
+ if (field === 'expression') {
5905
+ nextEditor.presetId = '';
5906
+ }
5907
+ }
5908
+
5909
+ state.tableDesigner.constraintsDrawer = {
5910
+ ...drawer,
5911
+ editor: nextEditor,
5912
+ };
5913
+
5914
+ if (options.notify !== false) {
5915
+ emitChange();
5916
+ }
5917
+ }
5918
+
5919
+ export async function saveTableDesignerCheckEditor() {
5920
+ const draft = state.tableDesigner.draft;
5921
+ const drawer = state.tableDesigner.constraintsDrawer ?? createClosedTableDesignerConstraintsDrawer();
5922
+ const editor = drawer.editor ?? createTableDesignerCheckEditor();
5923
+ const expression = String(editor.expression ?? '').trim();
5924
+ const column = getTableDesignerDrawerColumn();
5925
+
5926
+ if (!draft) {
5927
+ return false;
5928
+ }
5929
+
5930
+ if (!expression) {
5931
+ state.tableDesigner.constraintsDrawer = {
5932
+ ...drawer,
5933
+ editor: {
5934
+ ...editor,
5935
+ error: { code: 'CHECK_EXPRESSION_REQUIRED', message: 'CHECK expression is required.' },
5936
+ },
5937
+ };
5938
+ emitChange();
5939
+ return false;
5940
+ }
5941
+
5942
+ state.tableDesigner.constraintsDrawer = {
5943
+ ...drawer,
5944
+ editor: {
5945
+ ...editor,
5946
+ validating: true,
5947
+ error: null,
5948
+ },
5949
+ };
5950
+ emitChange();
5951
+
5952
+ try {
5953
+ await api.validateTableDesignerCheck({ draft, expression });
5954
+
5955
+ const seed = {
5956
+ expression,
5957
+ columnId: drawer.columnId,
5958
+ columns: column ? [{ name: column.name, allowedValues: [] }] : [],
5959
+ source: editor.presetId ? 'preset' : 'user',
5960
+ presetId: editor.presetId,
5961
+ presetFields: editor.presetFields,
5962
+ };
5963
+
5964
+ state.tableDesigner.draft = drawer.editingConstraintId
5965
+ ? updateTableDesignerConstraintField(
5966
+ draft,
5967
+ 'check',
5968
+ drawer.editingConstraintId,
5969
+ 'expression',
5970
+ expression,
5971
+ getTableDesignerContext(),
5972
+ )
5973
+ : addTableDesignerCheckConstraint(draft, seed, getTableDesignerContext());
5974
+
5975
+ if (drawer.editingConstraintId) {
5976
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
5977
+ state.tableDesigner.draft,
5978
+ 'check',
5979
+ drawer.editingConstraintId,
5980
+ 'presetId',
5981
+ editor.presetId,
5982
+ getTableDesignerContext(),
5983
+ );
5984
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
5985
+ state.tableDesigner.draft,
5986
+ 'check',
5987
+ drawer.editingConstraintId,
5988
+ 'presetFields',
5989
+ editor.presetFields,
5990
+ getTableDesignerContext(),
5991
+ );
5992
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
5993
+ state.tableDesigner.draft,
5994
+ 'check',
5995
+ drawer.editingConstraintId,
5996
+ 'source',
5997
+ editor.presetId ? 'preset' : 'user',
5998
+ getTableDesignerContext(),
5999
+ );
6000
+ }
6001
+
6002
+ state.tableDesigner.saveError = null;
6003
+ state.tableDesigner.constraintsDrawer = {
6004
+ ...drawer,
6005
+ editingConstraintId: '',
6006
+ editor: null,
6007
+ };
6008
+ emitChange();
6009
+ return true;
6010
+ } catch (error) {
6011
+ state.tableDesigner.constraintsDrawer = {
6012
+ ...drawer,
6013
+ editor: {
6014
+ ...editor,
6015
+ validating: false,
6016
+ error: normalizeError(error),
6017
+ },
6018
+ };
6019
+ emitChange();
6020
+ return false;
6021
+ }
6022
+ }
6023
+
6024
+ export function removeCurrentTableDesignerCheckConstraint(constraintId) {
6025
+ if (!state.tableDesigner.draft) {
6026
+ return;
6027
+ }
6028
+
6029
+ state.tableDesigner.draft = removeTableDesignerCheckConstraint(
6030
+ state.tableDesigner.draft,
6031
+ constraintId,
6032
+ getTableDesignerContext(),
6033
+ );
6034
+ state.tableDesigner.saveError = null;
6035
+ emitChange();
6036
+ }
6037
+
5510
6038
  export function addCurrentTableDesignerColumn() {
5511
6039
  if (!state.tableDesigner.draft) {
5512
6040
  return null;
@@ -5522,10 +6050,12 @@ export function addCurrentTableDesignerColumn() {
5522
6050
  return nextColumn?.id ?? null;
5523
6051
  }
5524
6052
 
5525
- export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
6053
+ export function queueTableDesignerImport(fileName, text, options = {}) {
6054
+ const format = String(options.format ?? '').trim().toLowerCase();
6055
+
5526
6056
  try {
5527
- const imported = createTableDesignerDraftFromCsvImport(
5528
- { fileName, csvText },
6057
+ const imported = createTableDesignerDraftFromImport(
6058
+ { fileName, text, format },
5529
6059
  {
5530
6060
  ...getTableDesignerContext(),
5531
6061
  ...(options.context ?? {}),
@@ -5543,11 +6073,15 @@ export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
5543
6073
  throw error;
5544
6074
  }
5545
6075
 
5546
- pushToast(error?.message || 'CSV import failed.', 'alert');
6076
+ pushToast(error?.message || 'Data import failed.', 'alert');
5547
6077
  return null;
5548
6078
  }
5549
6079
  }
5550
6080
 
6081
+ export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
6082
+ return queueTableDesignerImport(fileName, csvText, { ...options, format: 'csv' });
6083
+ }
6084
+
5551
6085
  export function removeCurrentTableDesignerColumn(columnId) {
5552
6086
  if (!state.tableDesigner.draft) {
5553
6087
  return;
@@ -6190,6 +6724,11 @@ export async function submitDataRowUpdate(rowIndex, values, identity = null, opt
6190
6724
  return null;
6191
6725
  }
6192
6726
 
6727
+ if (state.dataBrowser.table?.isShadow) {
6728
+ pushToast('Shadow tables are read-only in Data.', 'alert');
6729
+ return null;
6730
+ }
6731
+
6193
6732
  state.dataBrowser.saving = true;
6194
6733
  state.dataBrowser.saveError = null;
6195
6734
  emitChange();
@@ -6228,6 +6767,11 @@ export async function submitDataRowDelete(rowIndex, options = {}) {
6228
6767
  return null;
6229
6768
  }
6230
6769
 
6770
+ if (state.dataBrowser.table?.isShadow) {
6771
+ pushToast('Shadow tables are read-only in Data.', 'alert');
6772
+ return null;
6773
+ }
6774
+
6231
6775
  const shouldStepBackPage =
6232
6776
  selected.rowIndex !== null && (state.dataBrowser.table?.rows?.length ?? 0) <= 1 && state.dataBrowser.page > 1;
6233
6777