@sqlrooms/cells 0.29.0-rc.1 → 0.29.0-rc.3

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 (144) hide show
  1. package/README.md +8 -8
  2. package/dist/cellsSlice.d.ts.map +1 -1
  3. package/dist/cellsSlice.js +305 -264
  4. package/dist/cellsSlice.js.map +1 -1
  5. package/dist/components/AggregationSelector.d.ts +8 -0
  6. package/dist/components/AggregationSelector.d.ts.map +1 -0
  7. package/dist/components/AggregationSelector.js +13 -0
  8. package/dist/components/AggregationSelector.js.map +1 -0
  9. package/dist/components/CellSourceSelector.d.ts +12 -0
  10. package/dist/components/CellSourceSelector.d.ts.map +1 -0
  11. package/dist/components/CellSourceSelector.js +34 -0
  12. package/dist/components/CellSourceSelector.js.map +1 -0
  13. package/dist/components/ChartTypeSelector.d.ts +8 -0
  14. package/dist/components/ChartTypeSelector.d.ts.map +1 -0
  15. package/dist/components/ChartTypeSelector.js +13 -0
  16. package/dist/components/ChartTypeSelector.js.map +1 -0
  17. package/dist/components/CollapsibleSectionButton.d.ts +10 -0
  18. package/dist/components/CollapsibleSectionButton.d.ts.map +1 -0
  19. package/dist/components/CollapsibleSectionButton.js +10 -0
  20. package/dist/components/CollapsibleSectionButton.js.map +1 -0
  21. package/dist/components/ColorSelector.d.ts +24 -0
  22. package/dist/components/ColorSelector.d.ts.map +1 -0
  23. package/dist/components/ColorSelector.js +17 -0
  24. package/dist/components/ColorSelector.js.map +1 -0
  25. package/dist/components/FieldSelector.d.ts +11 -0
  26. package/dist/components/FieldSelector.d.ts.map +1 -0
  27. package/dist/components/FieldSelector.js +11 -0
  28. package/dist/components/FieldSelector.js.map +1 -0
  29. package/dist/components/FieldTypeIcon.d.ts +8 -0
  30. package/dist/components/FieldTypeIcon.d.ts.map +1 -0
  31. package/dist/components/FieldTypeIcon.js +27 -0
  32. package/dist/components/FieldTypeIcon.js.map +1 -0
  33. package/dist/components/InputCellContent.d.ts.map +1 -1
  34. package/dist/components/InputCellContent.js +3 -3
  35. package/dist/components/InputCellContent.js.map +1 -1
  36. package/dist/components/SelectionListener.d.ts +15 -0
  37. package/dist/components/SelectionListener.d.ts.map +1 -0
  38. package/dist/components/SelectionListener.js +74 -0
  39. package/dist/components/SelectionListener.js.map +1 -0
  40. package/dist/components/SqlCellConnectionSelector.d.ts +9 -0
  41. package/dist/components/SqlCellConnectionSelector.d.ts.map +1 -0
  42. package/dist/components/SqlCellConnectionSelector.js +21 -0
  43. package/dist/components/SqlCellConnectionSelector.js.map +1 -0
  44. package/dist/components/SqlCellContent.d.ts.map +1 -1
  45. package/dist/components/SqlCellContent.js +31 -156
  46. package/dist/components/SqlCellContent.js.map +1 -1
  47. package/dist/components/SqlCellDependentsMenu.d.ts +11 -0
  48. package/dist/components/SqlCellDependentsMenu.d.ts.map +1 -0
  49. package/dist/components/SqlCellDependentsMenu.js +62 -0
  50. package/dist/components/SqlCellDependentsMenu.js.map +1 -0
  51. package/dist/components/SqlCellEditor.d.ts +14 -0
  52. package/dist/components/SqlCellEditor.d.ts.map +1 -0
  53. package/dist/components/SqlCellEditor.js +17 -0
  54. package/dist/components/SqlCellEditor.js.map +1 -0
  55. package/dist/components/SqlCellResultNameEditor.d.ts +8 -0
  56. package/dist/components/SqlCellResultNameEditor.d.ts.map +1 -0
  57. package/dist/components/SqlCellResultNameEditor.js +38 -0
  58. package/dist/components/SqlCellResultNameEditor.js.map +1 -0
  59. package/dist/components/SqlCellResults.d.ts +17 -0
  60. package/dist/components/SqlCellResults.d.ts.map +1 -0
  61. package/dist/components/SqlCellResults.js +45 -0
  62. package/dist/components/SqlCellResults.js.map +1 -0
  63. package/dist/components/SqlCellRunButton.d.ts +5 -6
  64. package/dist/components/SqlCellRunButton.d.ts.map +1 -1
  65. package/dist/components/SqlCellRunButton.js +11 -7
  66. package/dist/components/SqlCellRunButton.js.map +1 -1
  67. package/dist/components/SqlQueryPreview.d.ts +9 -0
  68. package/dist/components/SqlQueryPreview.d.ts.map +1 -0
  69. package/dist/components/SqlQueryPreview.js +14 -0
  70. package/dist/components/SqlQueryPreview.js.map +1 -0
  71. package/dist/components/TextCellContent.d.ts.map +1 -1
  72. package/dist/components/TextCellContent.js +24 -7
  73. package/dist/components/TextCellContent.js.map +1 -1
  74. package/dist/components/VegaCellContent.d.ts +0 -1
  75. package/dist/components/VegaCellContent.d.ts.map +1 -1
  76. package/dist/components/VegaCellContent.js +61 -47
  77. package/dist/components/VegaCellContent.js.map +1 -1
  78. package/dist/components/VegaCellHeader.d.ts +11 -0
  79. package/dist/components/VegaCellHeader.d.ts.map +1 -0
  80. package/dist/components/VegaCellHeader.js +32 -0
  81. package/dist/components/VegaCellHeader.js.map +1 -0
  82. package/dist/components/VegaConfigPanel.d.ts +5 -0
  83. package/dist/components/VegaConfigPanel.d.ts.map +1 -1
  84. package/dist/components/VegaConfigPanel.js +53 -63
  85. package/dist/components/VegaConfigPanel.js.map +1 -1
  86. package/dist/dagUtils.d.ts +10 -9
  87. package/dist/dagUtils.d.ts.map +1 -1
  88. package/dist/dagUtils.js +76 -50
  89. package/dist/dagUtils.js.map +1 -1
  90. package/dist/defaultCellRegistry.d.ts.map +1 -1
  91. package/dist/defaultCellRegistry.js +79 -36
  92. package/dist/defaultCellRegistry.js.map +1 -1
  93. package/dist/execution.d.ts.map +1 -1
  94. package/dist/execution.js +24 -17
  95. package/dist/execution.js.map +1 -1
  96. package/dist/helpers.d.ts +20 -16
  97. package/dist/helpers.d.ts.map +1 -1
  98. package/dist/helpers.js +79 -44
  99. package/dist/helpers.js.map +1 -1
  100. package/dist/hooks/useVegaCellDataSource.d.ts +18 -0
  101. package/dist/hooks/useVegaCellDataSource.d.ts.map +1 -0
  102. package/dist/hooks/useVegaCellDataSource.js +40 -0
  103. package/dist/hooks/useVegaCellDataSource.js.map +1 -0
  104. package/dist/hooks/useVegaCellQuery.d.ts +16 -0
  105. package/dist/hooks/useVegaCellQuery.d.ts.map +1 -0
  106. package/dist/hooks/useVegaCellQuery.js +25 -0
  107. package/dist/hooks/useVegaCellQuery.js.map +1 -0
  108. package/dist/index.d.ts +3 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +3 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/sqlHelpers.d.ts +8 -8
  113. package/dist/sqlHelpers.d.ts.map +1 -1
  114. package/dist/sqlHelpers.js +16 -12
  115. package/dist/sqlHelpers.js.map +1 -1
  116. package/dist/types.d.ts +113 -70
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/types.js +22 -13
  119. package/dist/types.js.map +1 -1
  120. package/dist/useValidateResultName.d.ts +34 -0
  121. package/dist/useValidateResultName.d.ts.map +1 -0
  122. package/dist/useValidateResultName.js +111 -0
  123. package/dist/useValidateResultName.js.map +1 -0
  124. package/dist/utils.d.ts +10 -2
  125. package/dist/utils.d.ts.map +1 -1
  126. package/dist/utils.js +20 -4
  127. package/dist/utils.js.map +1 -1
  128. package/dist/vegaSelectionUtils.d.ts +14 -0
  129. package/dist/vegaSelectionUtils.d.ts.map +1 -0
  130. package/dist/vegaSelectionUtils.js +97 -0
  131. package/dist/vegaSelectionUtils.js.map +1 -0
  132. package/dist/vegaSpecBuilder.d.ts +34 -0
  133. package/dist/vegaSpecBuilder.d.ts.map +1 -0
  134. package/dist/vegaSpecBuilder.js +90 -0
  135. package/dist/vegaSpecBuilder.js.map +1 -0
  136. package/package.json +12 -13
  137. package/dist/components/DeleteSheetModal.d.ts +0 -9
  138. package/dist/components/DeleteSheetModal.d.ts.map +0 -1
  139. package/dist/components/DeleteSheetModal.js +0 -6
  140. package/dist/components/DeleteSheetModal.js.map +0 -1
  141. package/dist/components/SheetsTabBar.d.ts +0 -6
  142. package/dist/components/SheetsTabBar.d.ts.map +0 -1
  143. package/dist/components/SheetsTabBar.js +0 -57
  144. package/dist/components/SheetsTabBar.js.map +0 -1
@@ -1,147 +1,224 @@
1
- import { createId } from '@paralleldrive/cuid2';
2
1
  import { makePagedQuery } from '@sqlrooms/data-table';
3
2
  import { sanitizeQuery } from '@sqlrooms/duckdb';
4
3
  import { createSlice } from '@sqlrooms/room-store';
5
- import { generateUniqueName } from '@sqlrooms/utils';
4
+ import { convertToValidColumnOrTableName, generateUniqueName, } from '@sqlrooms/utils';
6
5
  import { produce } from 'immer';
7
- import { buildGraphCacheFromEdges, buildDependencyGraph, buildDependencyGraphAsync, collectReachable, ensureGraphCache, removeCellFromCache, replaceCellDependenciesInCache, topologicalOrder, } from './dagUtils';
8
- import { findSheetIdForCell, getRequiredSqlSelectToJson, normalizeCellsConfigStructure, resolveDependencies, } from './helpers';
6
+ import { buildDependencyGraph, buildDependencyGraphAsync, buildGraphCacheFromEdges, collectReachable, removeCellFromCache, replaceCellDependenciesInCache, topologicalOrder, } from './dagUtils';
7
+ import { findArtifactIdForCell, getRequiredSqlSelectToJson, normalizeCellsConfigStructure, resolveDependencies, } from './helpers';
9
8
  import { dropResultRelation } from './resultRelationPolicy';
10
9
  import { isInputCell, isSqlCell } from './types';
11
- import { getSheetSchemaName, isDefined } from './utils';
10
+ import { getArtifactSchemaName, getEffectiveResultName, isDefined, } from './utils';
11
+ import { buildCrossFilterPredicate } from './vegaSelectionUtils';
12
12
  function createDefaultCellsConfig(config) {
13
- const sheetId = createId();
14
13
  const defaultConfig = {
15
14
  data: {},
16
- sheets: {
17
- [sheetId]: {
18
- id: sheetId,
19
- type: 'notebook',
20
- title: 'Sheet',
21
- schemaName: getSheetSchemaName(sheetId),
22
- cellIds: [],
23
- edges: [],
24
- graphCache: {
25
- dependencies: {},
26
- dependents: {},
27
- contentHashByCell: {},
28
- },
29
- },
30
- },
31
- sheetOrder: [sheetId],
32
- currentSheetId: sheetId,
15
+ artifacts: {},
16
+ tableDepSchemas: ['main'],
33
17
  };
34
18
  if (!config) {
35
19
  return defaultConfig;
36
20
  }
37
- const { sheets, sheetOrder, currentSheetId } = normalizeCellsConfigStructure(config, defaultConfig);
21
+ const { artifacts } = normalizeCellsConfigStructure(config);
38
22
  return {
39
23
  ...defaultConfig,
40
24
  ...config,
41
- sheets,
42
- sheetOrder,
43
- currentSheetId,
25
+ artifacts,
44
26
  };
45
27
  }
46
28
  // --- Slice Implementation ---
47
29
  export function createCellsSlice(props) {
48
- const { cellRegistry, supportedSheetTypes = ['notebook', 'canvas'] } = props;
30
+ const { cellRegistry } = props;
49
31
  const initialConfig = createDefaultCellsConfig(props?.config);
50
32
  // Keep result data outside Immer drafts, but scoped per slice instance.
51
33
  const cellResultCache = new Map();
52
34
  return createSlice((set, get, store) => {
53
- const dropSqlResultRelation = async (resultName) => {
35
+ const dropRelationBestEffort = async (relationName) => {
54
36
  try {
55
37
  const connector = await get().db.getConnector();
56
- await dropResultRelation({ connector, relationName: resultName });
38
+ await dropResultRelation({ connector, relationName });
57
39
  }
58
40
  catch {
59
- // Best-effort cleanup: relation might already be gone or connector
60
- // might be unavailable during teardown.
41
+ // Best-effort cleanup
42
+ }
43
+ };
44
+ const dropRelationsForStatus = async (cellType, status) => {
45
+ if (!status)
46
+ return;
47
+ const registryItem = cellRegistry[cellType];
48
+ const names = registryItem?.getRelationsToDrop?.(status) ?? [];
49
+ for (const name of names) {
50
+ await dropRelationBestEffort(name);
61
51
  }
62
52
  };
53
+ // Mark cells stale when their dependent database tables change (schema-level).
54
+ store.subscribe((state, prevState) => {
55
+ if (state.db.tables === prevState.db.tables)
56
+ return;
57
+ const toName = (t) => `${t.table.schema ?? 'main'}.${t.table.table}`;
58
+ const oldNames = new Set(prevState.db.tables.map(toName));
59
+ const newNames = new Set(state.db.tables.map(toName));
60
+ const changed = new Set();
61
+ for (const n of oldNames) {
62
+ if (!newNames.has(n))
63
+ changed.add(n);
64
+ }
65
+ for (const n of newNames) {
66
+ if (!oldNames.has(n))
67
+ changed.add(n);
68
+ }
69
+ if (!changed.size)
70
+ return;
71
+ for (const artifact of Object.values(state.cells.config.artifacts)) {
72
+ const cache = artifact.graphCache;
73
+ if (!cache?.tableDependencies)
74
+ continue;
75
+ for (const [cellId, tables] of Object.entries(cache.tableDependencies)) {
76
+ if (tables.some((t) => changed.has(t))) {
77
+ state.cells.invalidateCellStatus(cellId);
78
+ }
79
+ }
80
+ }
81
+ });
63
82
  return {
64
83
  cells: {
65
84
  cellRegistry,
66
- supportedSheetTypes,
67
85
  config: initialConfig,
68
86
  status: {},
69
87
  activeAbortControllers: {},
70
88
  resultVersion: {},
71
89
  pageVersion: {},
72
- addCell: async (sheetId, cell, index) => {
90
+ async initialize() {
91
+ for (const [cellId, cell] of Object.entries(get().cells.config.data)) {
92
+ const registryItem = cellRegistry[cell.type];
93
+ if (registryItem?.onInitialize) {
94
+ registryItem.onInitialize({
95
+ id: cellId,
96
+ status: get().cells.status[cellId],
97
+ get,
98
+ set,
99
+ });
100
+ }
101
+ }
102
+ },
103
+ setConfig(config) {
104
+ set((state) => produce(state, (draft) => {
105
+ draft.cells.config = config;
106
+ }));
107
+ },
108
+ crossFilterSelections: {},
109
+ setCrossFilterSelection: (chartCellId, sqlId, selection) => {
110
+ set((state) => produce(state, (draft) => {
111
+ if (!draft.cells.crossFilterSelections[sqlId]) {
112
+ draft.cells.crossFilterSelections[sqlId] = {};
113
+ }
114
+ draft.cells.crossFilterSelections[sqlId][chartCellId] = selection;
115
+ }));
116
+ },
117
+ getCrossFilterPredicate: (chartCellId, sqlId) => {
118
+ const group = get().cells.crossFilterSelections[sqlId];
119
+ if (!group)
120
+ return null;
121
+ const siblingSelections = Object.entries(group)
122
+ .filter(([id]) => id !== chartCellId)
123
+ .map(([, sel]) => sel);
124
+ return buildCrossFilterPredicate(siblingSelections);
125
+ },
126
+ clearCrossFilterGroup: (sqlId) => {
127
+ set((state) => produce(state, (draft) => {
128
+ delete draft.cells.crossFilterSelections[sqlId];
129
+ }));
130
+ },
131
+ ensureArtifact: (artifactId, options) => {
132
+ set((state) => produce(state, (draft) => {
133
+ if (!draft.cells.config.artifacts[artifactId]) {
134
+ draft.cells.config.artifacts[artifactId] = {
135
+ id: artifactId,
136
+ schemaName: options?.schemaName ?? getArtifactSchemaName(artifactId),
137
+ cellIds: [],
138
+ edges: [],
139
+ graphCache: {
140
+ dependencies: {},
141
+ dependents: {},
142
+ contentHashByCell: {},
143
+ tableDependencies: {},
144
+ },
145
+ };
146
+ }
147
+ else if (options?.schemaName &&
148
+ !draft.cells.config.artifacts[artifactId]?.schemaName) {
149
+ draft.cells.config.artifacts[artifactId].schemaName =
150
+ options.schemaName;
151
+ }
152
+ }));
153
+ },
154
+ addCell: async (artifactId, cell, index) => {
73
155
  // Pre-compute dependencies outside produce() to support async
74
156
  const sqlSelectToJson = getRequiredSqlSelectToJson(get());
75
- const targetSheet = get().cells.config.sheets[sheetId];
76
- const scopedCells = Object.fromEntries((targetSheet?.cellIds ?? [])
157
+ const targetArtifact = get().cells.config.artifacts[artifactId];
158
+ const scopedCells = Object.fromEntries((targetArtifact?.cellIds ?? [])
77
159
  .map((id) => get().cells.config.data[id])
78
160
  .filter(isDefined)
79
161
  .map((candidate) => [candidate.id, candidate]));
80
162
  scopedCells[cell.id] = cell;
81
- const deps = await resolveDependencies(cell, scopedCells, sheetId, cellRegistry, sqlSelectToJson);
163
+ // Generate a unique result name for SQL cells if not set
164
+ if (cell.type === 'sql' && !cell.data.resultName) {
165
+ const existingNames = Object.values(scopedCells)
166
+ .filter((c) => c.type === 'sql' && c.id !== cell.id)
167
+ .map((c) => getEffectiveResultName(c.data, convertToValidColumnOrTableName));
168
+ const baseName = getEffectiveResultName(cell.data, convertToValidColumnOrTableName);
169
+ const uniqueName = generateUniqueName(baseName, existingNames);
170
+ cell.data.resultName = uniqueName;
171
+ }
172
+ const deps = await resolveDependencies(cell, scopedCells, artifactId, cellRegistry, sqlSelectToJson);
82
173
  set((state) => produce(state, (draft) => {
83
174
  draft.cells.config.data[cell.id] = cell;
84
- if (cell.type === 'sql') {
85
- draft.cells.status[cell.id] = {
86
- type: 'sql',
87
- status: 'idle',
88
- referencedTables: [],
89
- };
90
- }
91
- else {
92
- draft.cells.status[cell.id] = { type: 'other' };
93
- }
94
- // Single-owner invariant: a cell can belong to one sheet only.
95
- for (const [existingSheetId, existingSheet] of Object.entries(draft.cells.config.sheets)) {
96
- if (existingSheetId === sheetId)
175
+ const registryItem = cellRegistry[cell.type];
176
+ draft.cells.status[cell.id] = registryItem?.createStatus?.(cell.id) ?? { type: 'other' };
177
+ // Single-owner invariant: a cell can belong to one artifact only.
178
+ for (const [existingArtifactId, existingArtifact,] of Object.entries(draft.cells.config.artifacts)) {
179
+ if (existingArtifactId === artifactId)
97
180
  continue;
98
- existingSheet.cellIds = existingSheet.cellIds.filter((cid) => cid !== cell.id);
99
- existingSheet.edges = existingSheet.edges.filter((e) => e.source !== cell.id && e.target !== cell.id);
100
- removeCellFromCache(existingSheet, cell.id);
181
+ existingArtifact.cellIds = existingArtifact.cellIds.filter((cid) => cid !== cell.id);
182
+ existingArtifact.edges = existingArtifact.edges.filter((e) => e.source !== cell.id && e.target !== cell.id);
183
+ removeCellFromCache(existingArtifact, cell.id);
101
184
  }
102
- let sheet = draft.cells.config.sheets[sheetId];
103
- if (!sheet) {
104
- sheet = {
105
- id: sheetId,
106
- type: 'notebook',
107
- title: 'Sheet',
108
- schemaName: getSheetSchemaName(sheetId),
185
+ let artifact = draft.cells.config.artifacts[artifactId];
186
+ if (!artifact) {
187
+ artifact = {
188
+ id: artifactId,
189
+ schemaName: getArtifactSchemaName(artifactId),
109
190
  cellIds: [],
110
191
  edges: [],
111
192
  graphCache: {
112
193
  dependencies: {},
113
194
  dependents: {},
114
195
  contentHashByCell: {},
196
+ tableDependencies: {},
115
197
  },
116
198
  };
117
- draft.cells.config.sheets[sheetId] = sheet;
118
- if (!draft.cells.config.sheetOrder.includes(sheetId)) {
119
- draft.cells.config.sheetOrder.push(sheetId);
120
- }
121
- if (!draft.cells.config.currentSheetId) {
122
- draft.cells.config.currentSheetId = sheetId;
123
- }
199
+ draft.cells.config.artifacts[artifactId] = artifact;
124
200
  }
125
- const newIndex = index !== undefined ? index : sheet.cellIds.length;
126
- sheet.cellIds = sheet.cellIds.filter((cid) => cid !== cell.id);
127
- sheet.cellIds.splice(newIndex, 0, cell.id);
201
+ const newIndex = index !== undefined ? index : artifact.cellIds.length;
202
+ artifact.cellIds = artifact.cellIds.filter((cid) => cid !== cell.id);
203
+ artifact.cellIds.splice(newIndex, 0, cell.id);
128
204
  // Add edges from pre-computed dependencies
129
- sheet.edges = sheet.edges.filter((edge) => edge.target !== cell.id &&
205
+ artifact.edges = artifact.edges.filter((edge) => edge.target !== cell.id &&
130
206
  edge.id !== `${edge.source}-${cell.id}`);
131
- const localCellIds = new Set(sheet.cellIds);
132
- for (const depId of deps) {
207
+ const localCellIds = new Set(artifact.cellIds);
208
+ for (const depId of deps.cellIds) {
133
209
  if (!localCellIds.has(depId) || depId === cell.id)
134
210
  continue;
135
- sheet.edges.push({
211
+ artifact.edges.push({
136
212
  id: `${depId}-${cell.id}`,
137
213
  source: depId,
138
214
  target: cell.id,
139
215
  });
140
216
  }
141
- replaceCellDependenciesInCache(sheet, cell.id, deps.filter((depId) => localCellIds.has(depId)));
217
+ replaceCellDependenciesInCache(artifact, cell.id, deps.cellIds.filter((depId) => localCellIds.has(depId)), deps.tableNames);
142
218
  }));
143
219
  },
144
220
  removeCell: (id) => {
221
+ const cell = get().cells.config.data[id];
145
222
  const previousStatus = get().cells.status[id];
146
223
  cellResultCache.delete(id);
147
224
  set((state) => produce(state, (draft) => {
@@ -154,39 +231,46 @@ export function createCellsSlice(props) {
154
231
  controller.abort();
155
232
  delete draft.cells.activeAbortControllers[id];
156
233
  }
157
- // Remove from all sheets
158
- for (const sheet of Object.values(draft.cells.config.sheets)) {
159
- sheet.cellIds = sheet.cellIds.filter((cid) => cid !== id);
160
- sheet.edges = sheet.edges.filter((e) => e.source !== id && e.target !== id);
161
- removeCellFromCache(sheet, id);
234
+ // Remove from all artifacts
235
+ for (const artifact of Object.values(draft.cells.config.artifacts)) {
236
+ artifact.cellIds = artifact.cellIds.filter((cid) => cid !== id);
237
+ artifact.edges = artifact.edges.filter((e) => e.source !== id && e.target !== id);
238
+ removeCellFromCache(artifact, id);
162
239
  }
163
240
  }));
164
- if (previousStatus?.type === 'sql') {
165
- void dropSqlResultRelation(previousStatus.resultView);
241
+ if (cell) {
242
+ const registryItem = cellRegistry[cell.type];
243
+ if (registryItem?.onRemove) {
244
+ void registryItem.onRemove({
245
+ id,
246
+ status: previousStatus,
247
+ get,
248
+ set,
249
+ });
250
+ }
251
+ else {
252
+ void dropRelationsForStatus(cell.type, previousStatus);
253
+ }
166
254
  }
167
255
  },
168
256
  updateCell: async (id, updater, opts) => {
169
257
  const cell = get().cells.config.data[id];
170
258
  if (!cell)
171
259
  return;
172
- const ownerSheetId = findSheetIdForCell(get(), id);
173
- const scopedCells = Object.fromEntries(((ownerSheetId &&
174
- get().cells.config.sheets[ownerSheetId]?.cellIds) ||
260
+ const ownerArtifactId = findArtifactIdForCell(get(), id);
261
+ const scopedCells = Object.fromEntries(((ownerArtifactId &&
262
+ get().cells.config.artifacts[ownerArtifactId]?.cellIds) ||
175
263
  [])
176
264
  .map((cellId) => get().cells.config.data[cellId])
177
265
  .filter(isDefined)
178
266
  .map((candidate) => [candidate.id, candidate]));
179
267
  const updatedCell = updater(cell);
180
268
  scopedCells[id] = updatedCell;
181
- // Check if resultName changed for SQL cells with successful execution
182
- const resultNameChanged = isSqlCell(cell) &&
183
- isSqlCell(updatedCell) &&
184
- cell.data.resultName !== updatedCell.data.resultName;
269
+ const registryItem = cellRegistry[cell.type];
185
270
  const existingStatus = get().cells.status[id];
186
- const hasExistingView = resultNameChanged &&
187
- existingStatus?.type === 'sql' &&
188
- existingStatus.status === 'success' &&
189
- existingStatus.resultView;
271
+ const oldResultRelation = existingStatus && registryItem?.getResultRelation
272
+ ? registryItem.getResultRelation(existingStatus)
273
+ : undefined;
190
274
  const semanticInputChanged = isInputCell(cell) &&
191
275
  isInputCell(updatedCell) &&
192
276
  (cell.data.input.varName !== updatedCell.data.input.varName ||
@@ -194,12 +278,20 @@ export function createCellsSlice(props) {
194
278
  const semanticSqlChanged = isSqlCell(cell) &&
195
279
  isSqlCell(updatedCell) &&
196
280
  cell.data.sql !== updatedCell.data.sql;
281
+ const semanticChanged = registryItem?.hasSemanticChange?.(cell, updatedCell) ?? false;
197
282
  // Apply cell data to the store immediately so that other
198
283
  // actions (e.g. runCell triggered via Cmd+Enter) always see
199
284
  // the latest value. Edge/dependency updates follow
200
285
  // asynchronously below.
201
286
  set((state) => produce(state, (draft) => {
202
287
  draft.cells.config.data[id] = updatedCell;
288
+ if (semanticChanged) {
289
+ const status = draft.cells.status[id];
290
+ if (status && registryItem?.invalidateStatus) {
291
+ draft.cells.status[id] =
292
+ registryItem.invalidateStatus(status);
293
+ }
294
+ }
203
295
  }));
204
296
  // Avoid expensive dependency graph recomputation on each SQL keystroke.
205
297
  // We recompute immediately for non-SQL edits, or when callers explicitly
@@ -208,13 +300,13 @@ export function createCellsSlice(props) {
208
300
  if (shouldRecomputeDependencies) {
209
301
  // Pre-compute dependencies outside produce() to support async
210
302
  const sqlSelectToJson = getRequiredSqlSelectToJson(get());
211
- const newDeps = await resolveDependencies(updatedCell, scopedCells, ownerSheetId || '', cellRegistry, sqlSelectToJson);
303
+ const newDeps = await resolveDependencies(updatedCell, scopedCells, ownerArtifactId || '', cellRegistry, sqlSelectToJson);
212
304
  set((state) => produce(state, (draft) => {
213
- const ownerSheet = Object.values(draft.cells.config.sheets).find((sheet) => sheet.cellIds.includes(id));
305
+ const ownerSheet = Object.values(draft.cells.config.artifacts).find((artifact) => artifact.cellIds.includes(id));
214
306
  if (ownerSheet) {
215
307
  const localCellIds = new Set(ownerSheet.cellIds);
216
308
  ownerSheet.edges = ownerSheet.edges.filter((e) => e.target !== id);
217
- for (const depId of newDeps) {
309
+ for (const depId of newDeps.cellIds) {
218
310
  if (!localCellIds.has(depId) || depId === id)
219
311
  continue;
220
312
  ownerSheet.edges.push({
@@ -223,65 +315,45 @@ export function createCellsSlice(props) {
223
315
  target: id,
224
316
  });
225
317
  }
226
- replaceCellDependenciesInCache(ownerSheet, id, newDeps.filter((depId) => localCellIds.has(depId)));
318
+ replaceCellDependenciesInCache(ownerSheet, id, newDeps.cellIds.filter((depId) => localCellIds.has(depId)), newDeps.tableNames);
227
319
  }
228
320
  }));
229
321
  }
230
- // If we have an existing view, rename it instead of invalidating
231
- if (hasExistingView && existingStatus?.resultView) {
232
- const registryItem = cellRegistry[cell.type];
233
- if (registryItem?.renameResult) {
234
- void registryItem.renameResult({
235
- id,
236
- oldResultView: existingStatus.resultView,
237
- get,
238
- set,
239
- });
240
- }
322
+ // If the result relation changed name (e.g. SQL resultName changed), rename it
323
+ const newStatus = get().cells.status[id];
324
+ const newResultRelation = newStatus && registryItem?.getResultRelation
325
+ ? registryItem.getResultRelation(newStatus)
326
+ : undefined;
327
+ const resultRelationChanged = oldResultRelation && newResultRelation !== oldResultRelation;
328
+ if (resultRelationChanged && registryItem?.renameResult) {
329
+ void registryItem.renameResult({
330
+ id,
331
+ oldResultView: oldResultRelation,
332
+ get,
333
+ set,
334
+ });
241
335
  }
242
336
  // After update, trigger cascade only if explicitly requested
243
337
  // or if semantic execution inputs changed.
244
- const shouldCascade = opts?.cascade || resultNameChanged || semanticInputChanged;
338
+ const shouldCascade = opts?.cascade || resultRelationChanged || semanticInputChanged;
245
339
  if (shouldCascade) {
246
- if (ownerSheetId) {
247
- void get().cells.runDownstreamCascade(ownerSheetId, id);
340
+ if (ownerArtifactId) {
341
+ void get().cells.runDownstreamCascade(ownerArtifactId, id);
248
342
  }
249
343
  }
250
344
  },
251
- addSheet: (title, type = 'notebook') => {
252
- const id = createId();
253
- set((state) => produce(state, (draft) => {
254
- const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
255
- const existingTitles = Object.values(draft.cells.config.sheets).map((s) => s.title);
256
- const defaultTitle = generateUniqueName(`${typeLabel} 1`, existingTitles, ' ');
257
- draft.cells.config.sheets[id] = {
258
- id,
259
- type,
260
- title: title || defaultTitle,
261
- schemaName: getSheetSchemaName(id),
262
- cellIds: [],
263
- edges: [],
264
- graphCache: {
265
- dependencies: {},
266
- dependents: {},
267
- contentHashByCell: {},
268
- },
269
- };
270
- draft.cells.config.sheetOrder.push(id);
271
- draft.cells.config.currentSheetId = id;
272
- }));
273
- return id;
274
- },
275
- removeSheet: (sheetId) => {
276
- const sheet = get().cells.config.sheets[sheetId];
277
- if (!sheet)
345
+ removeArtifact: (artifactId) => {
346
+ const artifact = get().cells.config.artifacts[artifactId];
347
+ if (!artifact)
278
348
  return;
279
- const ownedCellIds = [...sheet.cellIds];
349
+ const ownedCellIds = [...artifact.cellIds];
280
350
  const relationNamesToDrop = ownedCellIds.flatMap((cellId) => {
351
+ const cell = get().cells.config.data[cellId];
281
352
  const status = get().cells.status[cellId];
282
- return status?.type === 'sql' && status.resultView
283
- ? [status.resultView]
284
- : [];
353
+ if (!cell || !status)
354
+ return [];
355
+ const registryItem = cellRegistry[cell.type];
356
+ return registryItem?.getRelationsToDrop?.(status) ?? [];
285
357
  });
286
358
  set((state) => produce(state, (draft) => {
287
359
  for (const cellId of ownedCellIds) {
@@ -296,7 +368,7 @@ export function createCellsSlice(props) {
296
368
  }
297
369
  delete draft.cells.activeAbortControllers[cellId];
298
370
  }
299
- for (const existingSheet of Object.values(draft.cells.config.sheets)) {
371
+ for (const existingSheet of Object.values(draft.cells.config.artifacts)) {
300
372
  existingSheet.cellIds = existingSheet.cellIds.filter((cid) => !ownedCellIds.includes(cid));
301
373
  existingSheet.edges = existingSheet.edges.filter((e) => !ownedCellIds.includes(e.source) &&
302
374
  !ownedCellIds.includes(e.target));
@@ -304,124 +376,72 @@ export function createCellsSlice(props) {
304
376
  removeCellFromCache(existingSheet, cellId);
305
377
  }
306
378
  }
307
- delete draft.cells.config.sheets[sheetId];
308
- draft.cells.config.sheetOrder =
309
- draft.cells.config.sheetOrder.filter((id) => id !== sheetId);
310
- if (draft.cells.config.currentSheetId === sheetId) {
311
- draft.cells.config.currentSheetId =
312
- draft.cells.config.sheetOrder[0];
313
- }
379
+ delete draft.cells.config.artifacts[artifactId];
314
380
  }));
315
381
  for (const relationName of relationNamesToDrop) {
316
- void dropSqlResultRelation(relationName);
382
+ void dropRelationBestEffort(relationName);
317
383
  }
318
384
  },
319
- closeSheet: (sheetId) => {
320
- set((state) => produce(state, (draft) => {
321
- draft.cells.config.sheetOrder =
322
- draft.cells.config.sheetOrder.filter((id) => id !== sheetId);
323
- if (draft.cells.config.currentSheetId === sheetId) {
324
- draft.cells.config.currentSheetId =
325
- draft.cells.config.sheetOrder[0];
326
- }
327
- }));
328
- },
329
- openSheet: (sheetId) => {
330
- set((state) => produce(state, (draft) => {
331
- if (!draft.cells.config.sheetOrder.includes(sheetId)) {
332
- draft.cells.config.sheetOrder.push(sheetId);
333
- }
334
- draft.cells.config.currentSheetId = sheetId;
335
- }));
336
- },
337
- setSheetOrder: (sheetOrder) => {
385
+ addEdge: (artifactId, edge) => {
338
386
  set((state) => produce(state, (draft) => {
339
- draft.cells.config.sheetOrder = sheetOrder;
340
- }));
341
- },
342
- renameSheet: (sheetId, title) => {
343
- set((state) => produce(state, (draft) => {
344
- const sheet = draft.cells.config.sheets[sheetId];
345
- if (sheet) {
346
- // We could add validation here, but usually UI handles it.
347
- // For now, we'll just allow it as the primary logic is in view slices.
348
- sheet.title = title;
349
- }
350
- }));
351
- },
352
- setCurrentSheet: (sheetId) => {
353
- set((state) => produce(state, (draft) => {
354
- draft.cells.config.currentSheetId = sheetId;
355
- }));
356
- },
357
- addEdge: (sheetId, edge) => {
358
- set((state) => produce(state, (draft) => {
359
- let sheet = draft.cells.config.sheets[sheetId];
360
- if (!sheet) {
361
- sheet = {
362
- id: sheetId,
363
- type: 'notebook',
364
- title: 'Sheet',
365
- schemaName: getSheetSchemaName(sheetId),
387
+ let artifact = draft.cells.config.artifacts[artifactId];
388
+ if (!artifact) {
389
+ artifact = {
390
+ id: artifactId,
391
+ schemaName: getArtifactSchemaName(artifactId),
366
392
  cellIds: [],
367
393
  edges: [],
368
394
  graphCache: {
369
395
  dependencies: {},
370
396
  dependents: {},
371
397
  contentHashByCell: {},
398
+ tableDependencies: {},
372
399
  },
373
400
  };
374
- draft.cells.config.sheets[sheetId] = sheet;
375
- if (!draft.cells.config.sheetOrder.includes(sheetId)) {
376
- draft.cells.config.sheetOrder.push(sheetId);
377
- }
378
- if (!draft.cells.config.currentSheetId) {
379
- draft.cells.config.currentSheetId = sheetId;
380
- }
401
+ draft.cells.config.artifacts[artifactId] = artifact;
381
402
  }
382
- if (!sheet.cellIds.includes(edge.source) ||
383
- !sheet.cellIds.includes(edge.target)) {
403
+ if (!artifact.cellIds.includes(edge.source) ||
404
+ !artifact.cellIds.includes(edge.target)) {
384
405
  return;
385
406
  }
386
407
  const id = `${edge.source}-${edge.target}`;
387
- if (!sheet.edges.find((e) => e.id === id)) {
388
- sheet.edges.push({ ...edge, id });
389
- ensureGraphCache(sheet);
390
- const deps = sheet.graphCache?.dependencies[edge.target] || [];
391
- replaceCellDependenciesInCache(sheet, edge.target, [
408
+ if (!artifact.edges.find((e) => e.id === id)) {
409
+ artifact.edges.push({ ...edge, id });
410
+ const deps = artifact.graphCache?.dependencies[edge.target] || [];
411
+ replaceCellDependenciesInCache(artifact, edge.target, [
392
412
  ...deps,
393
413
  edge.source,
394
414
  ]);
395
415
  }
396
416
  }));
397
417
  },
398
- removeEdge: (sheetId, edgeId) => {
418
+ removeEdge: (artifactId, edgeId) => {
399
419
  set((state) => produce(state, (draft) => {
400
- const sheet = draft.cells.config.sheets[sheetId];
401
- if (sheet) {
402
- sheet.edges = sheet.edges.filter((e) => e.id !== edgeId);
403
- delete sheet.graphCache;
420
+ const artifact = draft.cells.config.artifacts[artifactId];
421
+ if (artifact) {
422
+ artifact.edges = artifact.edges.filter((e) => e.id !== edgeId);
423
+ delete artifact.graphCache;
404
424
  }
405
425
  }));
406
426
  },
407
- updateEdgesFromSql: async (sheetId, cellId) => {
427
+ updateEdgesFromSql: async (artifactId, cellId) => {
408
428
  const cell = get().cells.config.data[cellId];
409
- const sheet = get().cells.config.sheets[sheetId];
410
- if (!cell || !sheet)
429
+ const artifact = get().cells.config.artifacts[artifactId];
430
+ if (!cell || !artifact)
411
431
  return;
412
432
  // Pre-compute dependencies outside produce() to support async
413
433
  const sqlSelectToJson = getRequiredSqlSelectToJson(get());
414
- const scopedCells = Object.fromEntries(sheet.cellIds
434
+ const scopedCells = Object.fromEntries(artifact.cellIds
415
435
  .map((id) => get().cells.config.data[id])
416
436
  .filter(isDefined)
417
437
  .map((candidate) => [candidate.id, candidate]));
418
- const deps = await resolveDependencies(cell, scopedCells, sheetId, cellRegistry, sqlSelectToJson);
438
+ const deps = await resolveDependencies(cell, scopedCells, artifactId, cellRegistry, sqlSelectToJson);
419
439
  set((state) => produce(state, (draft) => {
420
- const draftSheet = draft.cells.config.sheets[sheetId];
440
+ const draftSheet = draft.cells.config.artifacts[artifactId];
421
441
  if (draftSheet) {
422
442
  draftSheet.edges = draftSheet.edges.filter((e) => e.target !== cellId);
423
443
  const localCellIds = new Set(draftSheet.cellIds);
424
- for (const depId of deps) {
444
+ for (const depId of deps.cellIds) {
425
445
  if (!localCellIds.has(depId) || depId === cellId)
426
446
  continue;
427
447
  draftSheet.edges.push({
@@ -430,7 +450,7 @@ export function createCellsSlice(props) {
430
450
  target: cellId,
431
451
  });
432
452
  }
433
- replaceCellDependenciesInCache(draftSheet, cellId, deps.filter((depId) => localCellIds.has(depId)));
453
+ replaceCellDependenciesInCache(draftSheet, cellId, deps.cellIds.filter((depId) => localCellIds.has(depId)), deps.tableNames);
434
454
  }
435
455
  }));
436
456
  },
@@ -457,15 +477,14 @@ export function createCellsSlice(props) {
457
477
  }
458
478
  },
459
479
  invalidateCellStatus: (id) => {
480
+ const cell = get().cells.config.data[id];
481
+ if (!cell)
482
+ return;
483
+ const registryItem = cellRegistry[cell.type];
460
484
  set((state) => produce(state, (draft) => {
461
485
  const status = draft.cells.status[id];
462
- if (status?.type === 'sql') {
463
- // Reset to idle, clearing the result so user knows to re-run
464
- draft.cells.status[id] = {
465
- type: 'sql',
466
- status: 'idle',
467
- referencedTables: status.referencedTables || [],
468
- };
486
+ if (status && registryItem?.invalidateStatus) {
487
+ draft.cells.status[id] = registryItem.invalidateStatus(status);
469
488
  }
470
489
  }));
471
490
  },
@@ -496,25 +515,35 @@ export function createCellsSlice(props) {
496
515
  },
497
516
  fetchCellResultPage: async (id, pagination, sorting) => {
498
517
  const state = get();
518
+ const cell = state.cells.config.data[id];
519
+ if (!cell)
520
+ return;
521
+ const registryItem = cellRegistry[cell.type];
499
522
  const status = state.cells.status[id];
500
- if (status?.type !== 'sql' || !status.resultView)
523
+ const resultRelation = status && registryItem?.getResultRelation
524
+ ? registryItem.getResultRelation(status)
525
+ : undefined;
526
+ if (!resultRelation)
501
527
  return;
502
528
  const connector = await state.db.getConnector();
503
- const baseQuery = sanitizeQuery(`SELECT * FROM ${status.resultView}`);
529
+ const baseQuery = sanitizeQuery(`SELECT * FROM ${resultRelation}`);
504
530
  const pagedQuery = makePagedQuery(baseQuery, sorting ?? [], pagination);
505
531
  const arrowTable = await connector.query(pagedQuery);
506
- const countResult = await connector.query(`SELECT COUNT(*)::int AS count FROM ${status.resultView}`);
532
+ const countResult = await connector.query(`SELECT COUNT(*)::int AS count FROM ${resultRelation}`);
507
533
  const totalRows = countResult.toArray()[0]?.count ?? 0;
508
534
  get().cells.setCellResultPage(id, { arrowTable, totalRows });
509
535
  },
510
536
  // DAG methods (sync versions for UI usage)
511
- getRootCells: (sheetId) => {
512
- const { dependencies } = buildDependencyGraph(sheetId, get());
537
+ getArtifactIdForCell: (cellId) => {
538
+ return findArtifactIdForCell(get(), cellId);
539
+ },
540
+ getRootCells: (artifactId) => {
541
+ const { dependencies } = buildDependencyGraph(artifactId, get());
513
542
  const ids = Object.keys(dependencies);
514
543
  return ids.filter((id) => (dependencies[id]?.length ?? 0) === 0);
515
544
  },
516
- getDownstream: (sheetId, sourceCellId) => {
517
- const { dependencies, dependents } = buildDependencyGraph(sheetId, get());
545
+ getDownstream: (artifactId, sourceCellId) => {
546
+ const { dependencies, dependents } = buildDependencyGraph(artifactId, get());
518
547
  const reachable = collectReachable(sourceCellId, dependents);
519
548
  if (!reachable.size)
520
549
  return [];
@@ -526,19 +555,19 @@ export function createCellsSlice(props) {
526
555
  return topologicalOrder(rootsWithinScope, dependencies, dependents, reachable);
527
556
  },
528
557
  // Async cascade execution using AST-based dependency resolution
529
- runAllCellsCascade: async (sheetId) => {
530
- const cached = buildDependencyGraph(sheetId, get());
558
+ runAllCellsCascade: async (artifactId) => {
559
+ const cached = buildDependencyGraph(artifactId, get());
531
560
  const hasCachedNodes = Object.keys(cached.dependencies).length > 0;
532
561
  if (!hasCachedNodes) {
533
562
  set((state) => produce(state, (draft) => {
534
- const sheet = draft.cells.config.sheets[sheetId];
535
- if (sheet) {
536
- sheet.graphCache = buildGraphCacheFromEdges(sheet);
563
+ const artifact = draft.cells.config.artifacts[artifactId];
564
+ if (artifact) {
565
+ artifact.graphCache = buildGraphCacheFromEdges(artifact);
537
566
  }
538
567
  }));
539
568
  }
540
569
  const sqlSelectToJson = getRequiredSqlSelectToJson(get());
541
- const { dependencies, dependents } = await buildDependencyGraphAsync(sheetId, get(), sqlSelectToJson);
570
+ const { dependencies, dependents } = await buildDependencyGraphAsync(artifactId, get(), sqlSelectToJson);
542
571
  const roots = Object.keys(dependencies).filter((id) => (dependencies[id]?.length ?? 0) === 0);
543
572
  const order = topologicalOrder(roots, dependencies, dependents);
544
573
  for (const cellId of order) {
@@ -547,29 +576,35 @@ export function createCellsSlice(props) {
547
576
  }
548
577
  catch (error) {
549
578
  const message = error instanceof Error ? error.message : String(error);
550
- set((state) => produce(state, (draft) => {
551
- const status = draft.cells.status[cellId];
552
- if (status?.type === 'sql') {
553
- status.status = 'error';
554
- status.lastError = message;
555
- }
556
- }));
579
+ const failedCell = get().cells.config.data[cellId];
580
+ const failedRegistryItem = failedCell
581
+ ? cellRegistry[failedCell.type]
582
+ : undefined;
583
+ if (failedRegistryItem?.recordError) {
584
+ set((state) => produce(state, (draft) => {
585
+ const status = draft.cells.status[cellId];
586
+ if (status) {
587
+ draft.cells.status[cellId] =
588
+ failedRegistryItem.recordError(status, message);
589
+ }
590
+ }));
591
+ }
557
592
  }
558
593
  }
559
594
  },
560
- runDownstreamCascade: async (sheetId, sourceCellId) => {
561
- const cached = buildDependencyGraph(sheetId, get());
595
+ runDownstreamCascade: async (artifactId, sourceCellId) => {
596
+ const cached = buildDependencyGraph(artifactId, get());
562
597
  const hasCachedNodes = Object.keys(cached.dependencies).length > 0;
563
598
  if (!hasCachedNodes) {
564
599
  set((state) => produce(state, (draft) => {
565
- const sheet = draft.cells.config.sheets[sheetId];
566
- if (sheet) {
567
- sheet.graphCache = buildGraphCacheFromEdges(sheet);
600
+ const artifact = draft.cells.config.artifacts[artifactId];
601
+ if (artifact) {
602
+ artifact.graphCache = buildGraphCacheFromEdges(artifact);
568
603
  }
569
604
  }));
570
605
  }
571
606
  const sqlSelectToJson = getRequiredSqlSelectToJson(get());
572
- const { dependencies, dependents } = await buildDependencyGraphAsync(sheetId, get(), sqlSelectToJson);
607
+ const { dependencies, dependents } = await buildDependencyGraphAsync(artifactId, get(), sqlSelectToJson);
573
608
  const reachable = collectReachable(sourceCellId, dependents);
574
609
  if (!reachable.size)
575
610
  return;
@@ -585,13 +620,19 @@ export function createCellsSlice(props) {
585
620
  }
586
621
  catch (error) {
587
622
  const message = error instanceof Error ? error.message : String(error);
588
- set((state) => produce(state, (draft) => {
589
- const status = draft.cells.status[cellId];
590
- if (status?.type === 'sql') {
591
- status.status = 'error';
592
- status.lastError = message;
593
- }
594
- }));
623
+ const failedCell = get().cells.config.data[cellId];
624
+ const failedRegistryItem = failedCell
625
+ ? cellRegistry[failedCell.type]
626
+ : undefined;
627
+ if (failedRegistryItem?.recordError) {
628
+ set((state) => produce(state, (draft) => {
629
+ const status = draft.cells.status[cellId];
630
+ if (status) {
631
+ draft.cells.status[cellId] =
632
+ failedRegistryItem.recordError(status, message);
633
+ }
634
+ }));
635
+ }
595
636
  }
596
637
  }
597
638
  },