flowspec-mcp 4.1.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -159
- package/dist/db.d.ts +0 -26
- package/dist/db.js +396 -318
- package/dist/db.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/server.js +7 -26
- package/dist/server.js.map +1 -1
- package/dist/tools/createEdge.d.ts +4 -4
- package/dist/tools/createNode.d.ts +2 -2
- package/dist/tools/deleteEdge.js +4 -20
- package/dist/tools/deleteEdge.js.map +1 -1
- package/dist/tools/deleteNode.js +4 -20
- package/dist/tools/deleteNode.js.map +1 -1
- package/dist/tools/updateEdge.d.ts +2 -2
- package/dist/tools/updateEdge.js +3 -19
- package/dist/tools/updateEdge.js.map +1 -1
- package/dist/tools/updateNode.d.ts +2 -2
- package/dist/tools/updateNode.js +5 -21
- package/dist/tools/updateNode.js.map +1 -1
- package/dist/tools/updateProject.d.ts +2 -2
- package/dist/types.d.ts +0 -12
- package/package.json +2 -2
- package/dist/analysis/analysisUtils.d.ts +0 -36
- package/dist/analysis/analysisUtils.js +0 -284
- package/dist/analysis/analysisUtils.js.map +0 -1
- package/dist/export/jsonExporter.d.ts +0 -17
- package/dist/export/jsonExporter.js +0 -176
- package/dist/export/jsonExporter.js.map +0 -1
- package/dist/layout/semanticLayout.d.ts +0 -24
- package/dist/layout/semanticLayout.js +0 -233
- package/dist/layout/semanticLayout.js.map +0 -1
- package/dist/resources/selection.d.ts +0 -5
- package/dist/resources/selection.js +0 -88
- package/dist/resources/selection.js.map +0 -1
- package/dist/tools/captureScreen.d.ts +0 -48
- package/dist/tools/captureScreen.js +0 -135
- package/dist/tools/captureScreen.js.map +0 -1
- package/dist/tools/createSubview.d.ts +0 -50
- package/dist/tools/createSubview.js +0 -29
- package/dist/tools/createSubview.js.map +0 -1
- package/dist/tools/deleteSubview.d.ts +0 -24
- package/dist/tools/deleteSubview.js +0 -19
- package/dist/tools/deleteSubview.js.map +0 -1
- package/dist/tools/generateSpec.d.ts +0 -26
- package/dist/tools/generateSpec.js +0 -336
- package/dist/tools/generateSpec.js.map +0 -1
- package/dist/tools/getJson.d.ts +0 -21
- package/dist/tools/getJson.js +0 -24
- package/dist/tools/getJson.js.map +0 -1
- package/dist/tools/healthCheck.d.ts +0 -8
- package/dist/tools/healthCheck.js +0 -16
- package/dist/tools/healthCheck.js.map +0 -1
- package/dist/tools/ingestCodebase.d.ts +0 -27
- package/dist/tools/ingestCodebase.js +0 -516
- package/dist/tools/ingestCodebase.js.map +0 -1
- package/dist/tools/listSubviews.d.ts +0 -21
- package/dist/tools/listSubviews.js +0 -34
- package/dist/tools/listSubviews.js.map +0 -1
- package/dist/tools/smartLayout.d.ts +0 -30
- package/dist/tools/smartLayout.js +0 -74
- package/dist/tools/smartLayout.js.map +0 -1
- package/dist/tools/updateSubview.d.ts +0 -53
- package/dist/tools/updateSubview.js +0 -33
- package/dist/tools/updateSubview.js.map +0 -1
- package/dist/utils/selectionHelper.d.ts +0 -61
- package/dist/utils/selectionHelper.js +0 -111
- package/dist/utils/selectionHelper.js.map +0 -1
package/dist/db.js
CHANGED
|
@@ -13,6 +13,89 @@ if (MODE === 'cloud') {
|
|
|
13
13
|
'Find your User ID at: https://flowspec.app/account (under "MCP Configuration").');
|
|
14
14
|
sql = neon(DATABASE_URL);
|
|
15
15
|
}
|
|
16
|
+
// ─── Helper: reconstruct canvas_state from normalized rows ──────────
|
|
17
|
+
function buildCanvasState(nodes, edges, screens, regions, regionElements) {
|
|
18
|
+
const canvasNodes = nodes.map(n => ({
|
|
19
|
+
id: n.id,
|
|
20
|
+
type: n.type,
|
|
21
|
+
position: { x: n.position_x, y: n.position_y },
|
|
22
|
+
data: { ...n.data, label: n.label ?? n.data.label }
|
|
23
|
+
}));
|
|
24
|
+
const canvasEdges = edges.map(e => ({
|
|
25
|
+
id: e.id,
|
|
26
|
+
source: e.source,
|
|
27
|
+
target: e.target,
|
|
28
|
+
type: e.type,
|
|
29
|
+
data: e.data
|
|
30
|
+
}));
|
|
31
|
+
const canvasScreens = screens.map(s => {
|
|
32
|
+
const screenRegions = regions
|
|
33
|
+
.filter(r => r.screen_id === s.id)
|
|
34
|
+
.map(r => {
|
|
35
|
+
const elementIds = regionElements
|
|
36
|
+
.filter(re => re.region_id === r.id)
|
|
37
|
+
.sort((a, b) => a.position_order - b.position_order)
|
|
38
|
+
.map(re => re.node_id);
|
|
39
|
+
return {
|
|
40
|
+
id: r.id,
|
|
41
|
+
label: r.label ?? undefined,
|
|
42
|
+
position: { x: r.position_x, y: r.position_y },
|
|
43
|
+
size: { width: r.size_width, height: r.size_height },
|
|
44
|
+
elementIds,
|
|
45
|
+
componentNodeId: r.component_node_id ?? undefined
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
id: s.id,
|
|
50
|
+
name: s.name,
|
|
51
|
+
imageUrl: s.image_url,
|
|
52
|
+
imageWidth: s.image_width,
|
|
53
|
+
imageHeight: s.image_height,
|
|
54
|
+
imageFilename: s.image_filename ?? undefined,
|
|
55
|
+
regions: screenRegions
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
nodes: canvasNodes,
|
|
60
|
+
edges: canvasEdges,
|
|
61
|
+
screens: canvasScreens.length > 0 ? canvasScreens : undefined
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// ─── Helper: fetch full project from normalized tables (cloud) ──────
|
|
65
|
+
async function getProjectFromNormalized(projectId) {
|
|
66
|
+
const projectRows = await sql `
|
|
67
|
+
SELECT id, name, thumbnail_url, user_id, is_public, created_at, updated_at
|
|
68
|
+
FROM projects
|
|
69
|
+
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
70
|
+
`;
|
|
71
|
+
if (projectRows.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
const meta = projectRows[0];
|
|
74
|
+
const [nodesRaw, edgesRaw, screensRaw, regionsRaw, regionElementsRaw] = await Promise.all([
|
|
75
|
+
sql `SELECT id, project_id, type, position_x, position_y, label, data FROM nodes WHERE project_id = ${projectId}`,
|
|
76
|
+
sql `SELECT id, project_id, source, target, type, data FROM edges WHERE project_id = ${projectId}`,
|
|
77
|
+
sql `SELECT id, project_id, name, image_url, local_image_path, image_filename, image_width, image_height FROM screens WHERE project_id = ${projectId}`,
|
|
78
|
+
sql `SELECT id, screen_id, project_id, label, position_x, position_y, size_width, size_height, component_node_id FROM screen_regions WHERE project_id = ${projectId}`,
|
|
79
|
+
sql `
|
|
80
|
+
SELECT re.region_id, re.node_id, re.position_order
|
|
81
|
+
FROM region_elements re
|
|
82
|
+
INNER JOIN screen_regions sr ON re.region_id = sr.id
|
|
83
|
+
WHERE sr.project_id = ${projectId}
|
|
84
|
+
ORDER BY re.region_id, re.position_order
|
|
85
|
+
`
|
|
86
|
+
]);
|
|
87
|
+
const canvas_state = buildCanvasState(nodesRaw, edgesRaw, screensRaw, regionsRaw, regionElementsRaw);
|
|
88
|
+
return {
|
|
89
|
+
id: meta.id,
|
|
90
|
+
name: meta.name,
|
|
91
|
+
canvas_state,
|
|
92
|
+
thumbnail_url: meta.thumbnail_url ?? null,
|
|
93
|
+
user_id: meta.user_id,
|
|
94
|
+
is_public: meta.is_public,
|
|
95
|
+
created_at: meta.created_at,
|
|
96
|
+
updated_at: meta.updated_at
|
|
97
|
+
};
|
|
98
|
+
}
|
|
16
99
|
// ─── Local mode (HTTP to desktop server) ───────────────────────────
|
|
17
100
|
async function fetchLocal(path, options) {
|
|
18
101
|
const token = getLocalAuthToken();
|
|
@@ -48,16 +131,10 @@ export async function getProject(projectId) {
|
|
|
48
131
|
return null;
|
|
49
132
|
return res.json();
|
|
50
133
|
}
|
|
51
|
-
|
|
52
|
-
SELECT id, name, canvas_state, thumbnail_url, user_id, is_public, created_at, updated_at
|
|
53
|
-
FROM projects
|
|
54
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
55
|
-
`;
|
|
56
|
-
return rows[0] ?? null;
|
|
134
|
+
return getProjectFromNormalized(projectId);
|
|
57
135
|
}
|
|
58
136
|
export async function searchNodes(query, nodeType) {
|
|
59
137
|
if (MODE === 'local') {
|
|
60
|
-
// In local mode, fetch all projects and search client-side (same logic as cloud)
|
|
61
138
|
const res = await fetchLocal('/api/projects');
|
|
62
139
|
const summaries = await res.json();
|
|
63
140
|
const results = [];
|
|
@@ -87,36 +164,27 @@ export async function searchNodes(query, nodeType) {
|
|
|
87
164
|
}
|
|
88
165
|
return results;
|
|
89
166
|
}
|
|
167
|
+
// Cloud mode: indexed query on normalized nodes table
|
|
168
|
+
const typeFilter = nodeType ? nodeType : null;
|
|
90
169
|
const rows = await sql `
|
|
91
|
-
SELECT id,
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
SELECT n.id AS node_id, n.type AS node_type, n.label,
|
|
171
|
+
p.id AS project_id, p.name AS project_name
|
|
172
|
+
FROM nodes n
|
|
173
|
+
INNER JOIN projects p ON n.project_id = p.id
|
|
174
|
+
WHERE p.user_id = ${FLOWSPEC_USER_ID}
|
|
175
|
+
AND n.type != 'image'
|
|
176
|
+
AND n.label ILIKE ${'%' + query + '%'}
|
|
177
|
+
AND (${typeFilter}::text IS NULL OR n.type = ${typeFilter})
|
|
94
178
|
`;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
continue;
|
|
103
|
-
if (nodeType && node.type !== nodeType)
|
|
104
|
-
continue;
|
|
105
|
-
const label = node.data?.label ?? '';
|
|
106
|
-
if (label.toLowerCase().includes(lowerQuery)) {
|
|
107
|
-
results.push({
|
|
108
|
-
projectId: project.id,
|
|
109
|
-
projectName: project.name,
|
|
110
|
-
nodeId: node.id,
|
|
111
|
-
nodeType: node.type,
|
|
112
|
-
label,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return results;
|
|
179
|
+
return rows.map(row => ({
|
|
180
|
+
projectId: row.project_id,
|
|
181
|
+
projectName: row.project_name,
|
|
182
|
+
nodeId: row.node_id,
|
|
183
|
+
nodeType: row.node_type,
|
|
184
|
+
label: row.label
|
|
185
|
+
}));
|
|
118
186
|
}
|
|
119
|
-
// ─── Write operations
|
|
187
|
+
// ─── Write operations ───────────────────────────────────────────────
|
|
120
188
|
export async function createProjectViaApi(name, canvasState) {
|
|
121
189
|
if (MODE === 'local') {
|
|
122
190
|
const res = await fetchLocal('/api/projects', {
|
|
@@ -127,13 +195,36 @@ export async function createProjectViaApi(name, canvasState) {
|
|
|
127
195
|
throw new Error(`Failed to create project: ${res.status}`);
|
|
128
196
|
return res.json();
|
|
129
197
|
}
|
|
130
|
-
// Cloud mode:
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
198
|
+
// Cloud mode: insert project metadata, then decompose canvas_state into normalized tables
|
|
199
|
+
const projectId = randomUUID();
|
|
200
|
+
await sql `
|
|
201
|
+
INSERT INTO projects (id, name, user_id, created_at, updated_at)
|
|
202
|
+
VALUES (${projectId}, ${name}, ${FLOWSPEC_USER_ID}, NOW(), NOW())
|
|
135
203
|
`;
|
|
136
|
-
|
|
204
|
+
// If canvas_state provided, decompose into normalized tables
|
|
205
|
+
const cs = (canvasState ?? { nodes: [], edges: [] });
|
|
206
|
+
if (cs.nodes && cs.nodes.length > 0) {
|
|
207
|
+
for (const node of cs.nodes) {
|
|
208
|
+
const nodeId = node.id ?? randomUUID();
|
|
209
|
+
await sql `
|
|
210
|
+
INSERT INTO nodes (id, project_id, type, position_x, position_y, label, data, created_at, updated_at)
|
|
211
|
+
VALUES (${nodeId}, ${projectId}, ${node.type ?? 'datapoint'}, ${node.position?.x ?? 0}, ${node.position?.y ?? 0},
|
|
212
|
+
${node.data?.label ?? null}, ${JSON.stringify(node.data ?? {})}::jsonb, NOW(), NOW())
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (cs.edges && cs.edges.length > 0) {
|
|
217
|
+
for (const edge of cs.edges) {
|
|
218
|
+
const edgeId = edge.id ?? randomUUID();
|
|
219
|
+
await sql `
|
|
220
|
+
INSERT INTO edges (id, project_id, source, target, type, data, created_at, updated_at)
|
|
221
|
+
VALUES (${edgeId}, ${projectId}, ${edge.source}, ${edge.target},
|
|
222
|
+
${edge.data?.edgeType ?? edge.type ?? 'flows-to'}, ${JSON.stringify(edge.data ?? {})}::jsonb, NOW(), NOW())
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const project = await getProjectFromNormalized(projectId);
|
|
227
|
+
return project;
|
|
137
228
|
}
|
|
138
229
|
export async function updateProjectViaApi(projectId, updates) {
|
|
139
230
|
if (MODE === 'local') {
|
|
@@ -145,17 +236,75 @@ export async function updateProjectViaApi(projectId, updates) {
|
|
|
145
236
|
return null;
|
|
146
237
|
return res.json();
|
|
147
238
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
// Cloud mode: update name if provided
|
|
240
|
+
if (updates.name) {
|
|
241
|
+
await sql `
|
|
242
|
+
UPDATE projects SET name = ${updates.name}, updated_at = NOW()
|
|
243
|
+
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
// If canvas_state provided, replace all entities
|
|
247
|
+
if (updates.canvas_state) {
|
|
248
|
+
const cs = updates.canvas_state;
|
|
249
|
+
// Delete existing entities (cascade handles region_elements)
|
|
250
|
+
await sql `DELETE FROM nodes WHERE project_id = ${projectId}`;
|
|
251
|
+
await sql `DELETE FROM edges WHERE project_id = ${projectId}`;
|
|
252
|
+
await sql `DELETE FROM screens WHERE project_id = ${projectId}`;
|
|
253
|
+
// Re-insert nodes
|
|
254
|
+
if (cs.nodes) {
|
|
255
|
+
for (const node of cs.nodes) {
|
|
256
|
+
const nodeId = node.id ?? randomUUID();
|
|
257
|
+
await sql `
|
|
258
|
+
INSERT INTO nodes (id, project_id, type, position_x, position_y, label, data, created_at, updated_at)
|
|
259
|
+
VALUES (${nodeId}, ${projectId}, ${node.type ?? 'datapoint'}, ${node.position?.x ?? 0}, ${node.position?.y ?? 0},
|
|
260
|
+
${node.data?.label ?? null}, ${JSON.stringify(node.data ?? {})}::jsonb, NOW(), NOW())
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Re-insert edges
|
|
265
|
+
if (cs.edges) {
|
|
266
|
+
for (const edge of cs.edges) {
|
|
267
|
+
const edgeId = edge.id ?? randomUUID();
|
|
268
|
+
await sql `
|
|
269
|
+
INSERT INTO edges (id, project_id, source, target, type, data, created_at, updated_at)
|
|
270
|
+
VALUES (${edgeId}, ${projectId}, ${edge.source}, ${edge.target},
|
|
271
|
+
${edge.data?.edgeType ?? edge.type ?? 'flows-to'}, ${JSON.stringify(edge.data ?? {})}::jsonb, NOW(), NOW())
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Re-insert screens + regions + region_elements
|
|
276
|
+
if (cs.screens) {
|
|
277
|
+
for (const screen of cs.screens) {
|
|
278
|
+
const screenId = screen.id ?? randomUUID();
|
|
279
|
+
await sql `
|
|
280
|
+
INSERT INTO screens (id, project_id, name, image_url, image_filename, image_width, image_height, created_at, updated_at)
|
|
281
|
+
VALUES (${screenId}, ${projectId}, ${screen.name}, ${screen.imageUrl ?? ''}, ${screen.imageFilename ?? null},
|
|
282
|
+
${screen.imageWidth ?? 0}, ${screen.imageHeight ?? 0}, NOW(), NOW())
|
|
283
|
+
`;
|
|
284
|
+
if (screen.regions) {
|
|
285
|
+
for (const region of screen.regions) {
|
|
286
|
+
const regionId = region.id ?? randomUUID();
|
|
287
|
+
await sql `
|
|
288
|
+
INSERT INTO screen_regions (id, screen_id, project_id, label, position_x, position_y, size_width, size_height, component_node_id, created_at, updated_at)
|
|
289
|
+
VALUES (${regionId}, ${screenId}, ${projectId}, ${region.label ?? null},
|
|
290
|
+
${region.position?.x ?? 0}, ${region.position?.y ?? 0},
|
|
291
|
+
${region.size?.width ?? 0}, ${region.size?.height ?? 0},
|
|
292
|
+
${region.componentNodeId ?? null}, NOW(), NOW())
|
|
293
|
+
`;
|
|
294
|
+
if (region.elementIds) {
|
|
295
|
+
for (let i = 0; i < region.elementIds.length; i++) {
|
|
296
|
+
await sql `
|
|
297
|
+
INSERT INTO region_elements (region_id, node_id, position_order, created_at)
|
|
298
|
+
VALUES (${regionId}, ${region.elementIds[i]}, ${i}, NOW())
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return getProjectFromNormalized(projectId);
|
|
159
308
|
}
|
|
160
309
|
export async function deleteProjectViaApi(projectId) {
|
|
161
310
|
if (MODE === 'local') {
|
|
@@ -179,19 +328,18 @@ export async function createNodeViaApi(projectId, node) {
|
|
|
179
328
|
return null;
|
|
180
329
|
return res.json();
|
|
181
330
|
}
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
331
|
+
// Verify project ownership
|
|
332
|
+
const check = await sql `SELECT id FROM projects WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}`;
|
|
333
|
+
if (check.length === 0)
|
|
185
334
|
return null;
|
|
186
335
|
const nodeId = randomUUID();
|
|
187
|
-
const
|
|
188
|
-
project.canvas_state.nodes.push(newNode);
|
|
336
|
+
const label = node.data.label ?? null;
|
|
189
337
|
await sql `
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
338
|
+
INSERT INTO nodes (id, project_id, type, position_x, position_y, label, data, created_at, updated_at)
|
|
339
|
+
VALUES (${nodeId}, ${projectId}, ${node.type}, ${node.position.x}, ${node.position.y},
|
|
340
|
+
${label}, ${JSON.stringify(node.data)}::jsonb, NOW(), NOW())
|
|
193
341
|
`;
|
|
194
|
-
return
|
|
342
|
+
return { id: nodeId, type: node.type, position: node.position, data: node.data };
|
|
195
343
|
}
|
|
196
344
|
export async function updateNodeViaApi(projectId, nodeId, updates) {
|
|
197
345
|
if (MODE === 'local') {
|
|
@@ -203,41 +351,47 @@ export async function updateNodeViaApi(projectId, nodeId, updates) {
|
|
|
203
351
|
return null;
|
|
204
352
|
return res.json();
|
|
205
353
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
354
|
+
// Fetch current node
|
|
355
|
+
const nodeRows = await sql `
|
|
356
|
+
SELECT id, type, position_x, position_y, label, data
|
|
357
|
+
FROM nodes WHERE id = ${nodeId} AND project_id = ${projectId}
|
|
358
|
+
`;
|
|
359
|
+
if (nodeRows.length === 0)
|
|
211
360
|
return null;
|
|
212
|
-
const existing =
|
|
213
|
-
const
|
|
214
|
-
|
|
361
|
+
const existing = nodeRows[0];
|
|
362
|
+
const existingData = existing.data;
|
|
363
|
+
const updatedData = updates.data
|
|
364
|
+
? { ...existingData, ...updates.data }
|
|
365
|
+
: existingData;
|
|
366
|
+
const position = updates.position;
|
|
367
|
+
const label = updatedData.label ?? existing.label;
|
|
215
368
|
await sql `
|
|
216
|
-
UPDATE
|
|
217
|
-
SET
|
|
218
|
-
|
|
369
|
+
UPDATE nodes
|
|
370
|
+
SET position_x = COALESCE(${position?.x ?? null}, position_x),
|
|
371
|
+
position_y = COALESCE(${position?.y ?? null}, position_y),
|
|
372
|
+
label = ${label},
|
|
373
|
+
data = ${JSON.stringify(updatedData)}::jsonb,
|
|
374
|
+
updated_at = NOW()
|
|
375
|
+
WHERE id = ${nodeId} AND project_id = ${projectId}
|
|
219
376
|
`;
|
|
220
|
-
return
|
|
377
|
+
return {
|
|
378
|
+
id: nodeId,
|
|
379
|
+
type: updates.type ?? existing.type,
|
|
380
|
+
position: position ?? { x: existing.position_x, y: existing.position_y },
|
|
381
|
+
data: updatedData
|
|
382
|
+
};
|
|
221
383
|
}
|
|
222
384
|
export async function deleteNodeViaApi(projectId, nodeId) {
|
|
223
385
|
if (MODE === 'local') {
|
|
224
386
|
const res = await fetchLocal(`/api/projects/${projectId}/nodes/${nodeId}`, { method: 'DELETE' });
|
|
225
387
|
return res.ok;
|
|
226
388
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (idx === -1)
|
|
232
|
-
return false;
|
|
233
|
-
project.canvas_state.nodes.splice(idx, 1);
|
|
234
|
-
project.canvas_state.edges = project.canvas_state.edges.filter((e) => e.source !== nodeId && e.target !== nodeId);
|
|
235
|
-
await sql `
|
|
236
|
-
UPDATE projects
|
|
237
|
-
SET canvas_state = ${JSON.stringify(project.canvas_state)}::jsonb, updated_at = NOW()
|
|
238
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
389
|
+
// Edges cascade-delete via FK, so just delete the node
|
|
390
|
+
const rows = await sql `
|
|
391
|
+
DELETE FROM nodes WHERE id = ${nodeId} AND project_id = ${projectId}
|
|
392
|
+
RETURNING id
|
|
239
393
|
`;
|
|
240
|
-
return
|
|
394
|
+
return rows.length > 0;
|
|
241
395
|
}
|
|
242
396
|
export async function createEdgeViaApi(projectId, edge) {
|
|
243
397
|
if (MODE === 'local') {
|
|
@@ -249,37 +403,29 @@ export async function createEdgeViaApi(projectId, edge) {
|
|
|
249
403
|
return null;
|
|
250
404
|
return res.json();
|
|
251
405
|
}
|
|
252
|
-
|
|
253
|
-
|
|
406
|
+
// Verify project ownership
|
|
407
|
+
const check = await sql `SELECT id FROM projects WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}`;
|
|
408
|
+
if (check.length === 0)
|
|
254
409
|
return null;
|
|
255
410
|
const edgeId = randomUUID();
|
|
256
|
-
const
|
|
257
|
-
|
|
411
|
+
const edgeType = edge.data?.edgeType ?? edge.type ?? 'flows-to';
|
|
412
|
+
const data = edge.data ?? {};
|
|
258
413
|
await sql `
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
414
|
+
INSERT INTO edges (id, project_id, source, target, type, data, created_at, updated_at)
|
|
415
|
+
VALUES (${edgeId}, ${projectId}, ${edge.source}, ${edge.target}, ${edgeType}, ${JSON.stringify(data)}::jsonb, NOW(), NOW())
|
|
262
416
|
`;
|
|
263
|
-
return
|
|
417
|
+
return { id: edgeId, source: edge.source, target: edge.target, type: edgeType, data };
|
|
264
418
|
}
|
|
265
419
|
export async function deleteEdgeViaApi(projectId, edgeId) {
|
|
266
420
|
if (MODE === 'local') {
|
|
267
421
|
const res = await fetchLocal(`/api/projects/${projectId}/edges/${edgeId}`, { method: 'DELETE' });
|
|
268
422
|
return res.ok;
|
|
269
423
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const idx = project.canvas_state.edges.findIndex((e) => e.id === edgeId);
|
|
274
|
-
if (idx === -1)
|
|
275
|
-
return false;
|
|
276
|
-
project.canvas_state.edges.splice(idx, 1);
|
|
277
|
-
await sql `
|
|
278
|
-
UPDATE projects
|
|
279
|
-
SET canvas_state = ${JSON.stringify(project.canvas_state)}::jsonb, updated_at = NOW()
|
|
280
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
424
|
+
const rows = await sql `
|
|
425
|
+
DELETE FROM edges WHERE id = ${edgeId} AND project_id = ${projectId}
|
|
426
|
+
RETURNING id
|
|
281
427
|
`;
|
|
282
|
-
return
|
|
428
|
+
return rows.length > 0;
|
|
283
429
|
}
|
|
284
430
|
// ─── v3.0 API functions ────────────────────────────────────────────
|
|
285
431
|
export async function cloneProjectViaApi(projectId) {
|
|
@@ -290,15 +436,12 @@ export async function cloneProjectViaApi(projectId) {
|
|
|
290
436
|
const data = await res.json();
|
|
291
437
|
return data.id;
|
|
292
438
|
}
|
|
293
|
-
const project = await
|
|
439
|
+
const project = await getProjectFromNormalized(projectId);
|
|
294
440
|
if (!project)
|
|
295
441
|
return null;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
RETURNING id
|
|
300
|
-
`;
|
|
301
|
-
return rows[0].id;
|
|
442
|
+
// Create new project with cloned canvas_state
|
|
443
|
+
const newProject = await createProjectViaApi(project.name + ' (Copy)', project.canvas_state);
|
|
444
|
+
return newProject.id;
|
|
302
445
|
}
|
|
303
446
|
export async function uploadImageViaApi(base64Data, filename, contentType) {
|
|
304
447
|
if (MODE !== 'local') {
|
|
@@ -322,27 +465,15 @@ export async function createScreenViaApi(projectId, name, imageUrl, imageWidth,
|
|
|
322
465
|
return null;
|
|
323
466
|
return res.json();
|
|
324
467
|
}
|
|
325
|
-
|
|
326
|
-
|
|
468
|
+
// Verify project ownership
|
|
469
|
+
const check = await sql `SELECT id FROM projects WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}`;
|
|
470
|
+
if (check.length === 0)
|
|
327
471
|
return null;
|
|
328
472
|
const screenId = randomUUID();
|
|
329
|
-
const newScreen = {
|
|
330
|
-
id: screenId,
|
|
331
|
-
name,
|
|
332
|
-
imageUrl: imageUrl ?? null,
|
|
333
|
-
imageWidth: imageWidth ?? null,
|
|
334
|
-
imageHeight: imageHeight ?? null,
|
|
335
|
-
imageFilename: imageFilename ?? null,
|
|
336
|
-
regions: [],
|
|
337
|
-
};
|
|
338
|
-
if (!project.canvas_state.screens) {
|
|
339
|
-
project.canvas_state.screens = [];
|
|
340
|
-
}
|
|
341
|
-
project.canvas_state.screens.push(newScreen);
|
|
342
473
|
await sql `
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
474
|
+
INSERT INTO screens (id, project_id, name, image_url, image_filename, image_width, image_height, created_at, updated_at)
|
|
475
|
+
VALUES (${screenId}, ${projectId}, ${name}, ${imageUrl ?? ''}, ${imageFilename ?? null},
|
|
476
|
+
${imageWidth ?? 0}, ${imageHeight ?? 0}, NOW(), NOW())
|
|
346
477
|
`;
|
|
347
478
|
return { id: screenId, name };
|
|
348
479
|
}
|
|
@@ -356,38 +487,35 @@ export async function updateScreenViaApi(projectId, screenId, updates) {
|
|
|
356
487
|
return null;
|
|
357
488
|
return res.json();
|
|
358
489
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
490
|
+
// Verify screen exists in project
|
|
491
|
+
const screenRows = await sql `
|
|
492
|
+
SELECT id, name FROM screens WHERE id = ${screenId} AND project_id = ${projectId}
|
|
493
|
+
`;
|
|
494
|
+
if (screenRows.length === 0)
|
|
364
495
|
return null;
|
|
365
|
-
Object.assign(screen, updates);
|
|
366
496
|
await sql `
|
|
367
|
-
UPDATE
|
|
368
|
-
SET
|
|
369
|
-
|
|
497
|
+
UPDATE screens
|
|
498
|
+
SET name = COALESCE(${updates.name ?? null}, name),
|
|
499
|
+
image_url = COALESCE(${updates.imageUrl ?? null}, image_url),
|
|
500
|
+
image_width = COALESCE(${updates.imageWidth ?? null}, image_width),
|
|
501
|
+
image_height = COALESCE(${updates.imageHeight ?? null}, image_height),
|
|
502
|
+
updated_at = NOW()
|
|
503
|
+
WHERE id = ${screenId} AND project_id = ${projectId}
|
|
370
504
|
`;
|
|
371
|
-
|
|
505
|
+
const updatedName = updates.name ?? screenRows[0].name;
|
|
506
|
+
return { id: screenId, name: updatedName };
|
|
372
507
|
}
|
|
373
508
|
export async function deleteScreenViaApi(projectId, screenId) {
|
|
374
509
|
if (MODE === 'local') {
|
|
375
510
|
const res = await fetchLocal(`/api/projects/${projectId}/screens/${screenId}`, { method: 'DELETE' });
|
|
376
511
|
return res.ok;
|
|
377
512
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (idx === -1)
|
|
383
|
-
return false;
|
|
384
|
-
project.canvas_state.screens.splice(idx, 1);
|
|
385
|
-
await sql `
|
|
386
|
-
UPDATE projects
|
|
387
|
-
SET canvas_state = ${JSON.stringify(project.canvas_state)}::jsonb, updated_at = NOW()
|
|
388
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
513
|
+
// Cascade deletes regions and region_elements via FK
|
|
514
|
+
const rows = await sql `
|
|
515
|
+
DELETE FROM screens WHERE id = ${screenId} AND project_id = ${projectId}
|
|
516
|
+
RETURNING id
|
|
389
517
|
`;
|
|
390
|
-
return
|
|
518
|
+
return rows.length > 0;
|
|
391
519
|
}
|
|
392
520
|
export async function addRegionViaApi(projectId, screenId, region) {
|
|
393
521
|
if (MODE === 'local') {
|
|
@@ -399,29 +527,29 @@ export async function addRegionViaApi(projectId, screenId, region) {
|
|
|
399
527
|
return null;
|
|
400
528
|
return res.json();
|
|
401
529
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
530
|
+
// Verify screen exists
|
|
531
|
+
const screenCheck = await sql `
|
|
532
|
+
SELECT id FROM screens WHERE id = ${screenId} AND project_id = ${projectId}
|
|
533
|
+
`;
|
|
534
|
+
if (screenCheck.length === 0)
|
|
407
535
|
return null;
|
|
408
536
|
const regionId = randomUUID();
|
|
409
|
-
const newRegion = {
|
|
410
|
-
id: regionId,
|
|
411
|
-
label: region.label ?? null,
|
|
412
|
-
position: region.position,
|
|
413
|
-
size: region.size,
|
|
414
|
-
elementIds: region.elementIds ?? [],
|
|
415
|
-
componentNodeId: region.componentNodeId ?? null,
|
|
416
|
-
};
|
|
417
|
-
if (!screen.regions)
|
|
418
|
-
screen.regions = [];
|
|
419
|
-
screen.regions.push(newRegion);
|
|
420
537
|
await sql `
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
538
|
+
INSERT INTO screen_regions (id, screen_id, project_id, label, position_x, position_y, size_width, size_height, component_node_id, created_at, updated_at)
|
|
539
|
+
VALUES (${regionId}, ${screenId}, ${projectId}, ${region.label ?? null},
|
|
540
|
+
${region.position.x}, ${region.position.y},
|
|
541
|
+
${region.size.width}, ${region.size.height},
|
|
542
|
+
${region.componentNodeId ?? null}, NOW(), NOW())
|
|
424
543
|
`;
|
|
544
|
+
// Insert element references
|
|
545
|
+
if (region.elementIds && region.elementIds.length > 0) {
|
|
546
|
+
for (let i = 0; i < region.elementIds.length; i++) {
|
|
547
|
+
await sql `
|
|
548
|
+
INSERT INTO region_elements (region_id, node_id, position_order, created_at)
|
|
549
|
+
VALUES (${regionId}, ${region.elementIds[i]}, ${i}, NOW())
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
425
553
|
return { id: regionId, label: region.label };
|
|
426
554
|
}
|
|
427
555
|
export async function updateRegionViaApi(projectId, screenId, regionId, updates) {
|
|
@@ -434,21 +562,33 @@ export async function updateRegionViaApi(projectId, screenId, regionId, updates)
|
|
|
434
562
|
return null;
|
|
435
563
|
return res.json();
|
|
436
564
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
return null;
|
|
443
|
-
const region = screen.regions.find((r) => r.id === regionId);
|
|
444
|
-
if (!region)
|
|
565
|
+
// Verify region exists
|
|
566
|
+
const regionCheck = await sql `
|
|
567
|
+
SELECT id FROM screen_regions WHERE id = ${regionId} AND screen_id = ${screenId} AND project_id = ${projectId}
|
|
568
|
+
`;
|
|
569
|
+
if (regionCheck.length === 0)
|
|
445
570
|
return null;
|
|
446
|
-
|
|
571
|
+
// Update region fields
|
|
447
572
|
await sql `
|
|
448
|
-
UPDATE
|
|
449
|
-
SET
|
|
450
|
-
|
|
573
|
+
UPDATE screen_regions
|
|
574
|
+
SET label = COALESCE(${updates.label ?? null}, label),
|
|
575
|
+
position_x = COALESCE(${updates.position?.x ?? null}, position_x),
|
|
576
|
+
position_y = COALESCE(${updates.position?.y ?? null}, position_y),
|
|
577
|
+
size_width = COALESCE(${updates.size?.width ?? null}, size_width),
|
|
578
|
+
size_height = COALESCE(${updates.size?.height ?? null}, size_height),
|
|
579
|
+
updated_at = NOW()
|
|
580
|
+
WHERE id = ${regionId}
|
|
451
581
|
`;
|
|
582
|
+
// Replace element IDs if provided
|
|
583
|
+
if (updates.elementIds !== undefined) {
|
|
584
|
+
await sql `DELETE FROM region_elements WHERE region_id = ${regionId}`;
|
|
585
|
+
for (let i = 0; i < updates.elementIds.length; i++) {
|
|
586
|
+
await sql `
|
|
587
|
+
INSERT INTO region_elements (region_id, node_id, position_order, created_at)
|
|
588
|
+
VALUES (${regionId}, ${updates.elementIds[i]}, ${i}, NOW())
|
|
589
|
+
`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
452
592
|
return { id: regionId };
|
|
453
593
|
}
|
|
454
594
|
export async function removeRegionViaApi(projectId, screenId, regionId) {
|
|
@@ -456,22 +596,12 @@ export async function removeRegionViaApi(projectId, screenId, regionId) {
|
|
|
456
596
|
const res = await fetchLocal(`/api/projects/${projectId}/screens/${screenId}/regions/${regionId}`, { method: 'DELETE' });
|
|
457
597
|
return res.ok;
|
|
458
598
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (!screen || !screen.regions)
|
|
464
|
-
return false;
|
|
465
|
-
const idx = screen.regions.findIndex((r) => r.id === regionId);
|
|
466
|
-
if (idx === -1)
|
|
467
|
-
return false;
|
|
468
|
-
screen.regions.splice(idx, 1);
|
|
469
|
-
await sql `
|
|
470
|
-
UPDATE projects
|
|
471
|
-
SET canvas_state = ${JSON.stringify(project.canvas_state)}::jsonb, updated_at = NOW()
|
|
472
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
599
|
+
// Cascade deletes region_elements via FK
|
|
600
|
+
const rows = await sql `
|
|
601
|
+
DELETE FROM screen_regions WHERE id = ${regionId} AND screen_id = ${screenId} AND project_id = ${projectId}
|
|
602
|
+
RETURNING id
|
|
473
603
|
`;
|
|
474
|
-
return
|
|
604
|
+
return rows.length > 0;
|
|
475
605
|
}
|
|
476
606
|
export async function updateEdgeViaApi(projectId, edgeId, updates) {
|
|
477
607
|
if (MODE === 'local') {
|
|
@@ -483,22 +613,22 @@ export async function updateEdgeViaApi(projectId, edgeId, updates) {
|
|
|
483
613
|
return null;
|
|
484
614
|
return res.json();
|
|
485
615
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (
|
|
616
|
+
// Verify edge exists
|
|
617
|
+
const edgeRows = await sql `
|
|
618
|
+
SELECT id, data FROM edges WHERE id = ${edgeId} AND project_id = ${projectId}
|
|
619
|
+
`;
|
|
620
|
+
if (edgeRows.length === 0)
|
|
491
621
|
return null;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
edge.data.label = updates.label;
|
|
497
|
-
}
|
|
622
|
+
const existingData = (edgeRows[0].data ?? {});
|
|
623
|
+
const newData = { ...existingData };
|
|
624
|
+
if (updates.label !== undefined)
|
|
625
|
+
newData.label = updates.label;
|
|
498
626
|
await sql `
|
|
499
|
-
UPDATE
|
|
500
|
-
SET
|
|
501
|
-
|
|
627
|
+
UPDATE edges
|
|
628
|
+
SET type = COALESCE(${updates.type ?? null}, type),
|
|
629
|
+
data = ${JSON.stringify(newData)}::jsonb,
|
|
630
|
+
updated_at = NOW()
|
|
631
|
+
WHERE id = ${edgeId} AND project_id = ${projectId}
|
|
502
632
|
`;
|
|
503
633
|
return { id: edgeId };
|
|
504
634
|
}
|
|
@@ -512,121 +642,69 @@ export async function bulkImportCanvasState(projectId, canvasState, merge) {
|
|
|
512
642
|
throw new Error(`Failed to import: ${res.status}`);
|
|
513
643
|
return res.json();
|
|
514
644
|
}
|
|
515
|
-
|
|
516
|
-
|
|
645
|
+
// Verify project ownership
|
|
646
|
+
const check = await sql `SELECT id FROM projects WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}`;
|
|
647
|
+
if (check.length === 0)
|
|
517
648
|
throw new Error('Project not found');
|
|
518
|
-
if (merge) {
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (!project.canvas_state.screens)
|
|
524
|
-
project.canvas_state.screens = [];
|
|
525
|
-
project.canvas_state.screens.push(...canvasState.screens);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
// Replace mode: replace entire canvas state
|
|
530
|
-
project.canvas_state = canvasState;
|
|
531
|
-
}
|
|
532
|
-
await sql `
|
|
533
|
-
UPDATE projects
|
|
534
|
-
SET canvas_state = ${JSON.stringify(project.canvas_state)}::jsonb, updated_at = NOW()
|
|
535
|
-
WHERE id = ${projectId} AND user_id = ${FLOWSPEC_USER_ID}
|
|
536
|
-
`;
|
|
537
|
-
return {
|
|
538
|
-
nodeCount: canvasState.nodes.length,
|
|
539
|
-
edgeCount: canvasState.edges.length,
|
|
540
|
-
screenCount: canvasState.screens?.length ?? 0,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
// ─── v4.1 Subview operations ───────────────────────────────────
|
|
544
|
-
export async function createSubviewViaApi(projectId, name, description, nodePositions = []) {
|
|
545
|
-
if (MODE === 'local') {
|
|
546
|
-
const res = await fetchLocal(`/api/projects/${projectId}/subviews`, {
|
|
547
|
-
method: 'POST',
|
|
548
|
-
body: JSON.stringify({ name, description, nodePositions }),
|
|
549
|
-
});
|
|
550
|
-
if (!res.ok)
|
|
551
|
-
return null;
|
|
552
|
-
return res.json();
|
|
649
|
+
if (!merge) {
|
|
650
|
+
// Replace mode: clear existing entities first
|
|
651
|
+
await sql `DELETE FROM nodes WHERE project_id = ${projectId}`;
|
|
652
|
+
await sql `DELETE FROM edges WHERE project_id = ${projectId}`;
|
|
653
|
+
await sql `DELETE FROM screens WHERE project_id = ${projectId}`;
|
|
553
654
|
}
|
|
554
|
-
//
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
INSERT INTO subviews (id, project_id, name, description)
|
|
558
|
-
VALUES (${subviewId}, ${projectId}, ${name}, ${description ?? null})
|
|
559
|
-
`;
|
|
560
|
-
if (nodePositions.length > 0) {
|
|
561
|
-
const subviewIds = nodePositions.map(() => subviewId);
|
|
562
|
-
const nodeIds = nodePositions.map((np) => np.nodeId);
|
|
563
|
-
const xPositions = nodePositions.map((np) => np.x);
|
|
564
|
-
const yPositions = nodePositions.map((np) => np.y);
|
|
655
|
+
// Insert nodes
|
|
656
|
+
for (const node of canvasState.nodes) {
|
|
657
|
+
const nodeId = node.id ?? randomUUID();
|
|
565
658
|
await sql `
|
|
566
|
-
INSERT INTO
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
${nodeIds}::text[],
|
|
570
|
-
${xPositions}::real[],
|
|
571
|
-
${yPositions}::real[]
|
|
572
|
-
)
|
|
659
|
+
INSERT INTO nodes (id, project_id, type, position_x, position_y, label, data, created_at, updated_at)
|
|
660
|
+
VALUES (${nodeId}, ${projectId}, ${node.type ?? 'datapoint'}, ${node.position?.x ?? 0}, ${node.position?.y ?? 0},
|
|
661
|
+
${node.data?.label ?? null}, ${JSON.stringify(node.data ?? {})}::jsonb, NOW(), NOW())
|
|
573
662
|
`;
|
|
574
663
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (!res.ok)
|
|
584
|
-
return null;
|
|
585
|
-
return res.json();
|
|
664
|
+
// Insert edges
|
|
665
|
+
for (const edge of canvasState.edges) {
|
|
666
|
+
const edgeId = edge.id ?? randomUUID();
|
|
667
|
+
await sql `
|
|
668
|
+
INSERT INTO edges (id, project_id, source, target, type, data, created_at, updated_at)
|
|
669
|
+
VALUES (${edgeId}, ${projectId}, ${edge.source}, ${edge.target},
|
|
670
|
+
${edge.data?.edgeType ?? edge.type ?? 'flows-to'}, ${JSON.stringify(edge.data ?? {})}::jsonb, NOW(), NOW())
|
|
671
|
+
`;
|
|
586
672
|
}
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
UPDATE subviews
|
|
592
|
-
SET name = COALESCE(${name ?? null}, name),
|
|
593
|
-
description = COALESCE(${description ?? null}, description),
|
|
594
|
-
updated_at = NOW()
|
|
595
|
-
WHERE id = ${subviewId}
|
|
596
|
-
`;
|
|
597
|
-
if (updates.nodePositions !== undefined) {
|
|
598
|
-
// Delete existing positions
|
|
599
|
-
await sql `DELETE FROM subview_nodes WHERE subview_id = ${subviewId}`;
|
|
600
|
-
// Insert new positions
|
|
601
|
-
if (updates.nodePositions.length > 0) {
|
|
602
|
-
const subviewIds = updates.nodePositions.map(() => subviewId);
|
|
603
|
-
const nodeIds = updates.nodePositions.map((np) => np.nodeId);
|
|
604
|
-
const xPositions = updates.nodePositions.map((np) => np.x);
|
|
605
|
-
const yPositions = updates.nodePositions.map((np) => np.y);
|
|
673
|
+
// Insert screens + regions
|
|
674
|
+
if (canvasState.screens) {
|
|
675
|
+
for (const screen of canvasState.screens) {
|
|
676
|
+
const screenId = screen.id ?? randomUUID();
|
|
606
677
|
await sql `
|
|
607
|
-
INSERT INTO
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
${nodeIds}::text[],
|
|
611
|
-
${xPositions}::real[],
|
|
612
|
-
${yPositions}::real[]
|
|
613
|
-
)
|
|
678
|
+
INSERT INTO screens (id, project_id, name, image_url, image_filename, image_width, image_height, created_at, updated_at)
|
|
679
|
+
VALUES (${screenId}, ${projectId}, ${screen.name}, ${screen.imageUrl ?? ''}, ${screen.imageFilename ?? null},
|
|
680
|
+
${screen.imageWidth ?? 0}, ${screen.imageHeight ?? 0}, NOW(), NOW())
|
|
614
681
|
`;
|
|
682
|
+
if (screen.regions) {
|
|
683
|
+
for (const region of screen.regions) {
|
|
684
|
+
const regionId = region.id ?? randomUUID();
|
|
685
|
+
await sql `
|
|
686
|
+
INSERT INTO screen_regions (id, screen_id, project_id, label, position_x, position_y, size_width, size_height, component_node_id, created_at, updated_at)
|
|
687
|
+
VALUES (${regionId}, ${screenId}, ${projectId}, ${region.label ?? null},
|
|
688
|
+
${region.position?.x ?? 0}, ${region.position?.y ?? 0},
|
|
689
|
+
${region.size?.width ?? 0}, ${region.size?.height ?? 0},
|
|
690
|
+
${region.componentNodeId ?? null}, NOW(), NOW())
|
|
691
|
+
`;
|
|
692
|
+
if (region.elementIds) {
|
|
693
|
+
for (let i = 0; i < region.elementIds.length; i++) {
|
|
694
|
+
await sql `
|
|
695
|
+
INSERT INTO region_elements (region_id, node_id, position_order, created_at)
|
|
696
|
+
VALUES (${regionId}, ${region.elementIds[i]}, ${i}, NOW())
|
|
697
|
+
`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
615
702
|
}
|
|
616
703
|
}
|
|
617
|
-
return {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return res.ok;
|
|
623
|
-
}
|
|
624
|
-
// Cloud mode: direct SQL delete (cascades to subview_nodes)
|
|
625
|
-
const rows = await sql `
|
|
626
|
-
DELETE FROM subviews
|
|
627
|
-
WHERE id = ${subviewId}
|
|
628
|
-
RETURNING id
|
|
629
|
-
`;
|
|
630
|
-
return rows.length > 0;
|
|
704
|
+
return {
|
|
705
|
+
nodeCount: canvasState.nodes.length,
|
|
706
|
+
edgeCount: canvasState.edges.length,
|
|
707
|
+
screenCount: canvasState.screens?.length ?? 0,
|
|
708
|
+
};
|
|
631
709
|
}
|
|
632
710
|
//# sourceMappingURL=db.js.map
|