drowai-mcp 0.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.
- package/README.md +48 -0
- package/dist/cli.js +3126 -0
- package/npm-shrinkwrap.json +1378 -0
- package/package.json +27 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
import Fastify from "fastify";
|
|
9
|
+
|
|
10
|
+
var error = (code, message, options = {}) => {
|
|
11
|
+
const result = {
|
|
12
|
+
code,
|
|
13
|
+
message,
|
|
14
|
+
severity: "error"
|
|
15
|
+
};
|
|
16
|
+
if (options.fieldPath !== void 0) {
|
|
17
|
+
result.fieldPath = options.fieldPath;
|
|
18
|
+
}
|
|
19
|
+
if (options.objectId !== void 0) {
|
|
20
|
+
result.objectId = options.objectId;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
var warning = (code, message, options = {}) => {
|
|
25
|
+
const result = {
|
|
26
|
+
code,
|
|
27
|
+
message,
|
|
28
|
+
severity: "warning"
|
|
29
|
+
};
|
|
30
|
+
if (options.fieldPath !== void 0) {
|
|
31
|
+
result.fieldPath = options.fieldPath;
|
|
32
|
+
}
|
|
33
|
+
if (options.objectId !== void 0) {
|
|
34
|
+
result.objectId = options.objectId;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
import { ulid } from "ulid";
|
|
40
|
+
|
|
41
|
+
var DOCUMENT_ID_PREFIX = "doc_";
|
|
42
|
+
var NODE_ID_PREFIX = "node_";
|
|
43
|
+
var EDGE_ID_PREFIX = "edge_";
|
|
44
|
+
var NODE_TYPES = [
|
|
45
|
+
"rectangle",
|
|
46
|
+
"roundedRectangle",
|
|
47
|
+
"ellipse",
|
|
48
|
+
"diamond",
|
|
49
|
+
"text"
|
|
50
|
+
];
|
|
51
|
+
var EDGE_PATH_TYPES = ["straight", "orthogonal"];
|
|
52
|
+
var STYLE_PRESETS = ["default", "emphasis", "warning", "muted"];
|
|
53
|
+
var STROKE_WIDTHS = [1, 2, 3, 4, 6];
|
|
54
|
+
var FONT_SIZES = [12, 14, 16, 20, 24, 32];
|
|
55
|
+
var EDGE_FONT_SIZES = [12, 14, 16, 20, 24];
|
|
56
|
+
var FONT_WEIGHTS = ["normal", "bold"];
|
|
57
|
+
var TEXT_ALIGNS = ["left", "center", "right"];
|
|
58
|
+
var VERTICAL_ALIGNS = ["top", "middle", "bottom"];
|
|
59
|
+
var ARROW_TYPES = ["none", "classic", "block", "open"];
|
|
60
|
+
var ANCHOR_SIDES = ["top", "right", "bottom", "left"];
|
|
61
|
+
|
|
62
|
+
var ID_PATTERNS = {
|
|
63
|
+
[DOCUMENT_ID_PREFIX]: /^doc_[a-z0-9][a-z0-9_-]{2,63}$/,
|
|
64
|
+
[NODE_ID_PREFIX]: /^node_[a-z0-9][a-z0-9_-]{2,63}$/,
|
|
65
|
+
[EDGE_ID_PREFIX]: /^edge_[a-z0-9][a-z0-9_-]{2,63}$/
|
|
66
|
+
};
|
|
67
|
+
var generateDocumentId = () => `${DOCUMENT_ID_PREFIX}${ulid().toLowerCase()}`;
|
|
68
|
+
var generateNodeId = () => `${NODE_ID_PREFIX}${ulid().toLowerCase()}`;
|
|
69
|
+
var generateEdgeId = () => `${EDGE_ID_PREFIX}${ulid().toLowerCase()}`;
|
|
70
|
+
|
|
71
|
+
var BASE_NODE_PRESETS = {
|
|
72
|
+
default: {
|
|
73
|
+
fillColor: "#ffffff",
|
|
74
|
+
strokeColor: "#1f2937",
|
|
75
|
+
strokeWidth: 1,
|
|
76
|
+
dashed: false,
|
|
77
|
+
rounded: false,
|
|
78
|
+
textColor: "#111827",
|
|
79
|
+
fontSize: 14,
|
|
80
|
+
fontWeight: "normal",
|
|
81
|
+
textAlign: "center",
|
|
82
|
+
verticalAlign: "middle",
|
|
83
|
+
wrap: true
|
|
84
|
+
},
|
|
85
|
+
emphasis: {
|
|
86
|
+
fillColor: "#dbeafe",
|
|
87
|
+
strokeColor: "#1d4ed8",
|
|
88
|
+
strokeWidth: 2,
|
|
89
|
+
dashed: false,
|
|
90
|
+
rounded: false,
|
|
91
|
+
textColor: "#1e3a8a",
|
|
92
|
+
fontSize: 14,
|
|
93
|
+
fontWeight: "bold",
|
|
94
|
+
textAlign: "center",
|
|
95
|
+
verticalAlign: "middle",
|
|
96
|
+
wrap: true
|
|
97
|
+
},
|
|
98
|
+
warning: {
|
|
99
|
+
fillColor: "#fef3c7",
|
|
100
|
+
strokeColor: "#d97706",
|
|
101
|
+
strokeWidth: 2,
|
|
102
|
+
dashed: false,
|
|
103
|
+
rounded: false,
|
|
104
|
+
textColor: "#92400e",
|
|
105
|
+
fontSize: 14,
|
|
106
|
+
fontWeight: "bold",
|
|
107
|
+
textAlign: "center",
|
|
108
|
+
verticalAlign: "middle",
|
|
109
|
+
wrap: true
|
|
110
|
+
},
|
|
111
|
+
muted: {
|
|
112
|
+
fillColor: "#f3f4f6",
|
|
113
|
+
strokeColor: "#9ca3af",
|
|
114
|
+
strokeWidth: 1,
|
|
115
|
+
dashed: true,
|
|
116
|
+
rounded: false,
|
|
117
|
+
textColor: "#6b7280",
|
|
118
|
+
fontSize: 14,
|
|
119
|
+
fontWeight: "normal",
|
|
120
|
+
textAlign: "center",
|
|
121
|
+
verticalAlign: "middle",
|
|
122
|
+
wrap: true
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var BASE_EDGE_PRESETS = {
|
|
126
|
+
default: {
|
|
127
|
+
strokeColor: "#1f2937",
|
|
128
|
+
strokeWidth: 1,
|
|
129
|
+
dashed: false,
|
|
130
|
+
textColor: "#111827",
|
|
131
|
+
fontSize: 14,
|
|
132
|
+
fontWeight: "normal",
|
|
133
|
+
textAlign: "center",
|
|
134
|
+
verticalAlign: "middle",
|
|
135
|
+
wrap: true,
|
|
136
|
+
arrowStart: "none",
|
|
137
|
+
arrowEnd: "classic"
|
|
138
|
+
},
|
|
139
|
+
emphasis: {
|
|
140
|
+
strokeColor: "#1d4ed8",
|
|
141
|
+
strokeWidth: 2,
|
|
142
|
+
dashed: false,
|
|
143
|
+
textColor: "#1e3a8a",
|
|
144
|
+
fontSize: 14,
|
|
145
|
+
fontWeight: "bold",
|
|
146
|
+
textAlign: "center",
|
|
147
|
+
verticalAlign: "middle",
|
|
148
|
+
wrap: true,
|
|
149
|
+
arrowStart: "none",
|
|
150
|
+
arrowEnd: "classic"
|
|
151
|
+
},
|
|
152
|
+
warning: {
|
|
153
|
+
strokeColor: "#d97706",
|
|
154
|
+
strokeWidth: 2,
|
|
155
|
+
dashed: false,
|
|
156
|
+
textColor: "#92400e",
|
|
157
|
+
fontSize: 14,
|
|
158
|
+
fontWeight: "bold",
|
|
159
|
+
textAlign: "center",
|
|
160
|
+
verticalAlign: "middle",
|
|
161
|
+
wrap: true,
|
|
162
|
+
arrowStart: "none",
|
|
163
|
+
arrowEnd: "classic"
|
|
164
|
+
},
|
|
165
|
+
muted: {
|
|
166
|
+
strokeColor: "#9ca3af",
|
|
167
|
+
strokeWidth: 1,
|
|
168
|
+
dashed: true,
|
|
169
|
+
textColor: "#6b7280",
|
|
170
|
+
fontSize: 14,
|
|
171
|
+
fontWeight: "normal",
|
|
172
|
+
textAlign: "center",
|
|
173
|
+
verticalAlign: "middle",
|
|
174
|
+
wrap: true,
|
|
175
|
+
arrowStart: "none",
|
|
176
|
+
arrowEnd: "open"
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var NODE_STYLE_KEYS = [
|
|
180
|
+
"preset",
|
|
181
|
+
"fillColor",
|
|
182
|
+
"strokeColor",
|
|
183
|
+
"strokeWidth",
|
|
184
|
+
"dashed",
|
|
185
|
+
"rounded",
|
|
186
|
+
"textColor",
|
|
187
|
+
"fontSize",
|
|
188
|
+
"fontWeight",
|
|
189
|
+
"textAlign",
|
|
190
|
+
"verticalAlign",
|
|
191
|
+
"wrap"
|
|
192
|
+
];
|
|
193
|
+
var EDGE_STYLE_KEYS = [
|
|
194
|
+
"preset",
|
|
195
|
+
"strokeColor",
|
|
196
|
+
"strokeWidth",
|
|
197
|
+
"dashed",
|
|
198
|
+
"textColor",
|
|
199
|
+
"fontSize",
|
|
200
|
+
"fontWeight",
|
|
201
|
+
"textAlign",
|
|
202
|
+
"verticalAlign",
|
|
203
|
+
"wrap",
|
|
204
|
+
"arrowStart",
|
|
205
|
+
"arrowEnd"
|
|
206
|
+
];
|
|
207
|
+
var resolveNodeStyle = (style, forceRounded) => {
|
|
208
|
+
const preset = style?.preset ?? "default";
|
|
209
|
+
const resolved = {
|
|
210
|
+
preset,
|
|
211
|
+
...BASE_NODE_PRESETS[preset],
|
|
212
|
+
...style
|
|
213
|
+
};
|
|
214
|
+
if (forceRounded) {
|
|
215
|
+
resolved.rounded = true;
|
|
216
|
+
}
|
|
217
|
+
return resolved;
|
|
218
|
+
};
|
|
219
|
+
var resolveEdgeStyle = (style) => {
|
|
220
|
+
const preset = style?.preset ?? "default";
|
|
221
|
+
return {
|
|
222
|
+
preset,
|
|
223
|
+
...BASE_EDGE_PRESETS[preset],
|
|
224
|
+
...style
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
var ANCHOR_ORDER = ["right", "bottom", "left", "top"];
|
|
229
|
+
var nodeCenter = (node) => ({
|
|
230
|
+
x: node.x + Math.round(node.width / 2),
|
|
231
|
+
y: node.y + Math.round(node.height / 2)
|
|
232
|
+
});
|
|
233
|
+
var axisOf = (side) => side === "left" || side === "right" ? "horizontal" : "vertical";
|
|
234
|
+
var anchorRank = {
|
|
235
|
+
right: 0,
|
|
236
|
+
bottom: 1,
|
|
237
|
+
left: 2,
|
|
238
|
+
top: 3
|
|
239
|
+
};
|
|
240
|
+
var getAnchorPoint = (node, side) => {
|
|
241
|
+
switch (side) {
|
|
242
|
+
case "top":
|
|
243
|
+
return { x: node.x + Math.round(node.width / 2), y: node.y };
|
|
244
|
+
case "right":
|
|
245
|
+
return { x: node.x + node.width, y: node.y + Math.round(node.height / 2) };
|
|
246
|
+
case "bottom":
|
|
247
|
+
return { x: node.x + Math.round(node.width / 2), y: node.y + node.height };
|
|
248
|
+
case "left":
|
|
249
|
+
return { x: node.x, y: node.y + Math.round(node.height / 2) };
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
var fallbackAnchors = (source, target) => {
|
|
253
|
+
const sourceCenter = nodeCenter(source);
|
|
254
|
+
const targetCenter = nodeCenter(target);
|
|
255
|
+
const dx = targetCenter.x - sourceCenter.x;
|
|
256
|
+
const dy = targetCenter.y - sourceCenter.y;
|
|
257
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
258
|
+
return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
|
|
259
|
+
}
|
|
260
|
+
return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
|
|
261
|
+
};
|
|
262
|
+
var roundPoint = (point) => ({
|
|
263
|
+
x: Math.round(point.x),
|
|
264
|
+
y: Math.round(point.y)
|
|
265
|
+
});
|
|
266
|
+
var dedupeRoutePoints = (points) => {
|
|
267
|
+
const result = [];
|
|
268
|
+
for (const point of points) {
|
|
269
|
+
const previous = result.at(-1);
|
|
270
|
+
const rounded = roundPoint(point);
|
|
271
|
+
if (!previous || previous.x !== rounded.x || previous.y !== rounded.y) {
|
|
272
|
+
result.push(rounded);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
};
|
|
277
|
+
var preferredAxis = (source, target) => {
|
|
278
|
+
const horizontalGap = Math.max(target.x - (source.x + source.width), source.x - (target.x + target.width), 0);
|
|
279
|
+
const verticalGap = Math.max(target.y - (source.y + source.height), source.y - (target.y + target.height), 0);
|
|
280
|
+
if (horizontalGap !== verticalGap) {
|
|
281
|
+
return horizontalGap > verticalGap ? "horizontal" : "vertical";
|
|
282
|
+
}
|
|
283
|
+
const sourceCenter = nodeCenter(source);
|
|
284
|
+
const targetCenter = nodeCenter(target);
|
|
285
|
+
const dx = Math.abs(targetCenter.x - sourceCenter.x);
|
|
286
|
+
const dy = Math.abs(targetCenter.y - sourceCenter.y);
|
|
287
|
+
if (dx === dy) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return dx > dy ? "horizontal" : "vertical";
|
|
291
|
+
};
|
|
292
|
+
var buildRawRoutePoints = (sourcePoint, targetPoint, sourceAnchor, targetAnchor) => {
|
|
293
|
+
const sourceAxis = axisOf(sourceAnchor);
|
|
294
|
+
const targetAxis = axisOf(targetAnchor);
|
|
295
|
+
if (sourceAxis !== targetAxis) {
|
|
296
|
+
return sourceAxis === "horizontal" ? [{ x: targetPoint.x, y: sourcePoint.y }] : [{ x: sourcePoint.x, y: targetPoint.y }];
|
|
297
|
+
}
|
|
298
|
+
if (sourceAxis === "horizontal") {
|
|
299
|
+
const midpointX = Math.round((sourcePoint.x + targetPoint.x) / 2);
|
|
300
|
+
if (sourcePoint.y === targetPoint.y) {
|
|
301
|
+
return [{ x: midpointX, y: sourcePoint.y }];
|
|
302
|
+
}
|
|
303
|
+
return [
|
|
304
|
+
{ x: midpointX, y: sourcePoint.y },
|
|
305
|
+
{ x: midpointX, y: targetPoint.y }
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const midpointY = Math.round((sourcePoint.y + targetPoint.y) / 2);
|
|
309
|
+
if (sourcePoint.x === targetPoint.x) {
|
|
310
|
+
return [{ x: sourcePoint.x, y: midpointY }];
|
|
311
|
+
}
|
|
312
|
+
return [
|
|
313
|
+
{ x: sourcePoint.x, y: midpointY },
|
|
314
|
+
{ x: targetPoint.x, y: midpointY }
|
|
315
|
+
];
|
|
316
|
+
};
|
|
317
|
+
var normalizeCandidatePoints = (sourcePoint, targetPoint, points) => {
|
|
318
|
+
const deduped = dedupeRoutePoints(points);
|
|
319
|
+
if (deduped.length <= 1) {
|
|
320
|
+
return deduped;
|
|
321
|
+
}
|
|
322
|
+
const fullPath = [sourcePoint, ...deduped, targetPoint];
|
|
323
|
+
const simplified = [];
|
|
324
|
+
for (let index = 1; index < fullPath.length - 1; index += 1) {
|
|
325
|
+
const previous = fullPath[index - 1];
|
|
326
|
+
const current = fullPath[index];
|
|
327
|
+
const next = fullPath[index + 1];
|
|
328
|
+
const isCollinear = previous.x === current.x && current.x === next.x || previous.y === current.y && current.y === next.y;
|
|
329
|
+
if (!isCollinear) {
|
|
330
|
+
simplified.push(current);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return simplified;
|
|
334
|
+
};
|
|
335
|
+
var directionOf = (from, to) => {
|
|
336
|
+
if (from.x === to.x && from.y === to.y) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
if (from.x !== to.x && from.y !== to.y) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
if (to.x > from.x) {
|
|
343
|
+
return "right";
|
|
344
|
+
}
|
|
345
|
+
if (to.x < from.x) {
|
|
346
|
+
return "left";
|
|
347
|
+
}
|
|
348
|
+
return to.y > from.y ? "bottom" : "top";
|
|
349
|
+
};
|
|
350
|
+
var countBends = (path) => {
|
|
351
|
+
let bends = 0;
|
|
352
|
+
let previousDirection = null;
|
|
353
|
+
for (let index = 1; index < path.length; index += 1) {
|
|
354
|
+
const direction = directionOf(path[index - 1], path[index]);
|
|
355
|
+
if (!direction) {
|
|
356
|
+
return Number.POSITIVE_INFINITY;
|
|
357
|
+
}
|
|
358
|
+
if (previousDirection && previousDirection !== direction) {
|
|
359
|
+
bends += 1;
|
|
360
|
+
}
|
|
361
|
+
previousDirection = direction;
|
|
362
|
+
}
|
|
363
|
+
return bends;
|
|
364
|
+
};
|
|
365
|
+
var totalLength = (path) => path.slice(1).reduce((sum, point, index) => {
|
|
366
|
+
const previous = path[index];
|
|
367
|
+
return sum + Math.abs(point.x - previous.x) + Math.abs(point.y - previous.y);
|
|
368
|
+
}, 0);
|
|
369
|
+
var facingPenalty = (anchor, from, to) => {
|
|
370
|
+
switch (anchor) {
|
|
371
|
+
case "right":
|
|
372
|
+
return to.x >= from.x ? 0 : 2;
|
|
373
|
+
case "left":
|
|
374
|
+
return to.x <= from.x ? 0 : 2;
|
|
375
|
+
case "bottom":
|
|
376
|
+
return to.y >= from.y ? 0 : 2;
|
|
377
|
+
case "top":
|
|
378
|
+
return to.y <= from.y ? 0 : 2;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
var stickinessPenalty = (sourceAnchor, targetAnchor, preferred) => {
|
|
382
|
+
if (!preferred?.sourceAnchor && !preferred?.targetAnchor) {
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
let penalty = 0;
|
|
386
|
+
if (preferred.sourceAnchor && preferred.sourceAnchor !== sourceAnchor) {
|
|
387
|
+
penalty += 1;
|
|
388
|
+
}
|
|
389
|
+
if (preferred.targetAnchor && preferred.targetAnchor !== targetAnchor) {
|
|
390
|
+
penalty += 1;
|
|
391
|
+
}
|
|
392
|
+
return penalty;
|
|
393
|
+
};
|
|
394
|
+
var buildCandidate = (source, target, sourceAnchor, targetAnchor, preferred, preferredRouteAxis) => {
|
|
395
|
+
const sourceCenter = nodeCenter(source);
|
|
396
|
+
const targetCenter = nodeCenter(target);
|
|
397
|
+
const sourcePoint = getAnchorPoint(source, sourceAnchor);
|
|
398
|
+
const targetPoint = getAnchorPoint(target, targetAnchor);
|
|
399
|
+
const points = normalizeCandidatePoints(
|
|
400
|
+
sourcePoint,
|
|
401
|
+
targetPoint,
|
|
402
|
+
buildRawRoutePoints(sourcePoint, targetPoint, sourceAnchor, targetAnchor)
|
|
403
|
+
);
|
|
404
|
+
const path = [sourcePoint, ...points, targetPoint];
|
|
405
|
+
const startDirection = directionOf(sourcePoint, path[1]);
|
|
406
|
+
const targetEntryDirection = directionOf(targetPoint, path[path.length - 2]);
|
|
407
|
+
if (!startDirection || !targetEntryDirection) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
if (startDirection !== sourceAnchor || targetEntryDirection !== targetAnchor) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const bends = countBends(path);
|
|
414
|
+
if (!Number.isFinite(bends)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
sourceAnchor,
|
|
419
|
+
targetAnchor,
|
|
420
|
+
points,
|
|
421
|
+
score: [
|
|
422
|
+
bends,
|
|
423
|
+
facingPenalty(sourceAnchor, sourceCenter, targetCenter) + facingPenalty(targetAnchor, targetCenter, sourceCenter),
|
|
424
|
+
stickinessPenalty(sourceAnchor, targetAnchor, preferred),
|
|
425
|
+
preferredRouteAxis && axisOf(sourceAnchor) !== preferredRouteAxis ? 1 : 0,
|
|
426
|
+
totalLength(path),
|
|
427
|
+
anchorRank[sourceAnchor],
|
|
428
|
+
anchorRank[targetAnchor]
|
|
429
|
+
]
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
var candidateOrder = (left, right) => {
|
|
433
|
+
for (let index = 0; index < left.score.length; index += 1) {
|
|
434
|
+
const delta = left.score[index] - right.score[index];
|
|
435
|
+
if (delta !== 0) {
|
|
436
|
+
return delta;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return 0;
|
|
440
|
+
};
|
|
441
|
+
var selectRouteCandidate = (source, target, preferred) => {
|
|
442
|
+
const axisPreference = preferredAxis(source, target);
|
|
443
|
+
let selected = null;
|
|
444
|
+
for (const sourceAnchor of ANCHOR_ORDER) {
|
|
445
|
+
for (const targetAnchor of ANCHOR_ORDER) {
|
|
446
|
+
const candidate = buildCandidate(source, target, sourceAnchor, targetAnchor, preferred, axisPreference);
|
|
447
|
+
if (!candidate) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!selected || candidateOrder(candidate, selected) < 0) {
|
|
451
|
+
selected = candidate;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return selected;
|
|
456
|
+
};
|
|
457
|
+
var deriveAnchors = (source, target) => {
|
|
458
|
+
const candidate = selectRouteCandidate(source, target);
|
|
459
|
+
return candidate ? {
|
|
460
|
+
sourceAnchor: candidate.sourceAnchor,
|
|
461
|
+
targetAnchor: candidate.targetAnchor
|
|
462
|
+
} : fallbackAnchors(source, target);
|
|
463
|
+
};
|
|
464
|
+
var calculateOrthogonalRoute = (source, target, revision, preferred) => {
|
|
465
|
+
const candidate = selectRouteCandidate(source, target, preferred);
|
|
466
|
+
if (candidate) {
|
|
467
|
+
return {
|
|
468
|
+
sourceAnchor: candidate.sourceAnchor,
|
|
469
|
+
targetAnchor: candidate.targetAnchor,
|
|
470
|
+
points: candidate.points,
|
|
471
|
+
calculatedAtRevision: revision
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const { sourceAnchor, targetAnchor } = fallbackAnchors(source, target);
|
|
475
|
+
const sourcePoint = getAnchorPoint(source, sourceAnchor);
|
|
476
|
+
const targetPoint = getAnchorPoint(target, targetAnchor);
|
|
477
|
+
const points = sourceAnchor === "left" || sourceAnchor === "right" ? dedupeRoutePoints([
|
|
478
|
+
{ x: Math.round((sourcePoint.x + targetPoint.x) / 2), y: sourcePoint.y },
|
|
479
|
+
{ x: Math.round((sourcePoint.x + targetPoint.x) / 2), y: targetPoint.y }
|
|
480
|
+
]) : dedupeRoutePoints([
|
|
481
|
+
{ x: sourcePoint.x, y: Math.round((sourcePoint.y + targetPoint.y) / 2) },
|
|
482
|
+
{ x: targetPoint.x, y: Math.round((sourcePoint.y + targetPoint.y) / 2) }
|
|
483
|
+
]);
|
|
484
|
+
return {
|
|
485
|
+
sourceAnchor,
|
|
486
|
+
targetAnchor,
|
|
487
|
+
points,
|
|
488
|
+
calculatedAtRevision: revision
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
var recalculateEdgePaths = (document, edgeIds, nextRevision) => {
|
|
492
|
+
const allowed = edgeIds ? new Set(edgeIds) : null;
|
|
493
|
+
const nodeMap = new Map(document.nodes.map((node) => [node.id, node]));
|
|
494
|
+
return {
|
|
495
|
+
...document,
|
|
496
|
+
edges: document.edges.map((edge) => {
|
|
497
|
+
if (edge.pathType !== "orthogonal") {
|
|
498
|
+
return edge;
|
|
499
|
+
}
|
|
500
|
+
if (allowed && !allowed.has(edge.id)) {
|
|
501
|
+
return edge;
|
|
502
|
+
}
|
|
503
|
+
const source = nodeMap.get(edge.sourceNodeId);
|
|
504
|
+
const target = nodeMap.get(edge.targetNodeId);
|
|
505
|
+
if (!source || !target) {
|
|
506
|
+
return edge;
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...edge,
|
|
510
|
+
route: calculateOrthogonalRoute(source, target, nextRevision, edge.route)
|
|
511
|
+
};
|
|
512
|
+
})
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
var DEFAULT_CANVAS = {
|
|
517
|
+
width: 1600,
|
|
518
|
+
height: 900,
|
|
519
|
+
backgroundColor: "#ffffff",
|
|
520
|
+
padding: 80,
|
|
521
|
+
autoExpand: true
|
|
522
|
+
};
|
|
523
|
+
var round = (value) => Math.round(value);
|
|
524
|
+
var normalizePoint = (point) => ({
|
|
525
|
+
x: round(point.x),
|
|
526
|
+
y: round(point.y)
|
|
527
|
+
});
|
|
528
|
+
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
529
|
+
var normalizeCanvas = (canvas) => ({
|
|
530
|
+
width: round(canvas?.width ?? DEFAULT_CANVAS.width),
|
|
531
|
+
height: round(canvas?.height ?? DEFAULT_CANVAS.height),
|
|
532
|
+
backgroundColor: canvas?.backgroundColor ?? DEFAULT_CANVAS.backgroundColor,
|
|
533
|
+
padding: round(canvas?.padding ?? DEFAULT_CANVAS.padding),
|
|
534
|
+
autoExpand: canvas?.autoExpand ?? DEFAULT_CANVAS.autoExpand
|
|
535
|
+
});
|
|
536
|
+
var createDocument = (input, actor, timestamp = nowIso()) => {
|
|
537
|
+
return normalizeDocument({
|
|
538
|
+
id: input?.id ?? generateDocumentId(),
|
|
539
|
+
title: input?.title ?? "Untitled Diagram",
|
|
540
|
+
revision: 1,
|
|
541
|
+
metadata: {
|
|
542
|
+
createdBy: actor.actorId,
|
|
543
|
+
createdAt: timestamp,
|
|
544
|
+
updatedAt: timestamp,
|
|
545
|
+
ownerId: actor.ownerId,
|
|
546
|
+
tenantId: actor.tenantId
|
|
547
|
+
},
|
|
548
|
+
canvas: normalizeCanvas(input?.canvas),
|
|
549
|
+
nodes: [],
|
|
550
|
+
edges: []
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
var normalizeNode = (input) => ({
|
|
554
|
+
id: input.id ?? generateNodeId(),
|
|
555
|
+
type: input.type,
|
|
556
|
+
x: round(input.x),
|
|
557
|
+
y: round(input.y),
|
|
558
|
+
width: round(input.width),
|
|
559
|
+
height: round(input.height),
|
|
560
|
+
text: input.text ?? "",
|
|
561
|
+
style: resolveNodeStyle(input.style, input.type === "roundedRectangle"),
|
|
562
|
+
zIndex: round(input.zIndex ?? 0),
|
|
563
|
+
metadata: { ...input.metadata ?? {} }
|
|
564
|
+
});
|
|
565
|
+
var normalizeEdge = (input, nodeMap) => {
|
|
566
|
+
const sourceNode = nodeMap.get(input.sourceNodeId);
|
|
567
|
+
const targetNode = nodeMap.get(input.targetNodeId);
|
|
568
|
+
const anchors = sourceNode && targetNode ? deriveAnchors(sourceNode, targetNode) : { sourceAnchor: "right", targetAnchor: "left" };
|
|
569
|
+
return {
|
|
570
|
+
id: input.id ?? generateEdgeId(),
|
|
571
|
+
sourceNodeId: input.sourceNodeId,
|
|
572
|
+
targetNodeId: input.targetNodeId,
|
|
573
|
+
label: input.label ?? "",
|
|
574
|
+
pathType: input.pathType ?? "straight",
|
|
575
|
+
route: {
|
|
576
|
+
sourceAnchor: input.route?.sourceAnchor ?? anchors.sourceAnchor,
|
|
577
|
+
targetAnchor: input.route?.targetAnchor ?? anchors.targetAnchor,
|
|
578
|
+
points: (input.route?.points ?? []).map(normalizePoint),
|
|
579
|
+
calculatedAtRevision: input.route?.calculatedAtRevision ?? null
|
|
580
|
+
},
|
|
581
|
+
style: resolveEdgeStyle(input.style),
|
|
582
|
+
metadata: { ...input.metadata ?? {} }
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
var autoExpandCanvas = (document) => {
|
|
586
|
+
if (!document.canvas.autoExpand) {
|
|
587
|
+
return document;
|
|
588
|
+
}
|
|
589
|
+
let maxX = document.canvas.width;
|
|
590
|
+
let maxY = document.canvas.height;
|
|
591
|
+
for (const node of document.nodes) {
|
|
592
|
+
maxX = Math.max(maxX, node.x + node.width + document.canvas.padding);
|
|
593
|
+
maxY = Math.max(maxY, node.y + node.height + document.canvas.padding);
|
|
594
|
+
}
|
|
595
|
+
for (const edge of document.edges) {
|
|
596
|
+
for (const point of edge.route.points) {
|
|
597
|
+
maxX = Math.max(maxX, point.x + document.canvas.padding);
|
|
598
|
+
maxY = Math.max(maxY, point.y + document.canvas.padding);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
...document,
|
|
603
|
+
canvas: {
|
|
604
|
+
...document.canvas,
|
|
605
|
+
width: Math.max(320, round(maxX)),
|
|
606
|
+
height: Math.max(240, round(maxY))
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
var normalizeDocument = (input) => {
|
|
611
|
+
const normalizedNodes = input.nodes.map(normalizeNode).sort((a, b) => a.zIndex - b.zIndex || a.id.localeCompare(b.id));
|
|
612
|
+
const nodeMap = new Map(normalizedNodes.map((node) => [node.id, node]));
|
|
613
|
+
const normalizedEdges = input.edges.map((edge) => normalizeEdge(edge, nodeMap)).sort((a, b) => a.id.localeCompare(b.id));
|
|
614
|
+
return autoExpandCanvas({
|
|
615
|
+
id: input.id,
|
|
616
|
+
title: input.title,
|
|
617
|
+
revision: round(input.revision),
|
|
618
|
+
metadata: { ...input.metadata },
|
|
619
|
+
canvas: normalizeCanvas(input.canvas),
|
|
620
|
+
nodes: normalizedNodes,
|
|
621
|
+
edges: normalizedEdges
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
var nodeMapOf = (document) => new Map(document.nodes.map((node) => [node.id, node]));
|
|
626
|
+
var edgeMapOf = (document) => new Map(document.edges.map((edge) => [edge.id, edge]));
|
|
627
|
+
var withRevision = (document, revision) => ({
|
|
628
|
+
...document,
|
|
629
|
+
revision,
|
|
630
|
+
metadata: {
|
|
631
|
+
...document.metadata,
|
|
632
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
var addNode = (document, input) => {
|
|
636
|
+
const node = normalizeNode(input);
|
|
637
|
+
return {
|
|
638
|
+
document: normalizeDocument({
|
|
639
|
+
...document,
|
|
640
|
+
nodes: [...document.nodes, node]
|
|
641
|
+
}),
|
|
642
|
+
changedObjectIds: [node.id]
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
var updateNode = (document, nodeId, changes) => {
|
|
646
|
+
let changed = false;
|
|
647
|
+
const nodes = document.nodes.map((node) => {
|
|
648
|
+
if (node.id !== nodeId) {
|
|
649
|
+
return node;
|
|
650
|
+
}
|
|
651
|
+
changed = true;
|
|
652
|
+
return normalizeNode({
|
|
653
|
+
...node,
|
|
654
|
+
...changes,
|
|
655
|
+
metadata: changes.metadata ?? node.metadata,
|
|
656
|
+
style: {
|
|
657
|
+
...node.style,
|
|
658
|
+
...changes.style
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
document: normalizeDocument({ ...document, nodes }),
|
|
664
|
+
changedObjectIds: changed ? [nodeId] : []
|
|
665
|
+
};
|
|
666
|
+
};
|
|
667
|
+
var deleteNode = (document, nodeId) => {
|
|
668
|
+
const edgesToDelete = document.edges.filter((edge) => edge.sourceNodeId === nodeId || edge.targetNodeId === nodeId).map((edge) => edge.id);
|
|
669
|
+
return {
|
|
670
|
+
document: normalizeDocument({
|
|
671
|
+
...document,
|
|
672
|
+
nodes: document.nodes.filter((node) => node.id !== nodeId),
|
|
673
|
+
edges: document.edges.filter((edge) => !edgesToDelete.includes(edge.id))
|
|
674
|
+
}),
|
|
675
|
+
changedObjectIds: [nodeId, ...edgesToDelete]
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
var addEdge = (document, input) => {
|
|
679
|
+
const edge = normalizeEdge(input, nodeMapOf(document));
|
|
680
|
+
return {
|
|
681
|
+
document: normalizeDocument({
|
|
682
|
+
...document,
|
|
683
|
+
edges: [...document.edges, edge]
|
|
684
|
+
}),
|
|
685
|
+
changedObjectIds: [edge.id]
|
|
686
|
+
};
|
|
687
|
+
};
|
|
688
|
+
var updateEdge = (document, edgeId, changes) => {
|
|
689
|
+
let changed = false;
|
|
690
|
+
const nodes = nodeMapOf(document);
|
|
691
|
+
const edges = document.edges.map((edge) => {
|
|
692
|
+
if (edge.id !== edgeId) {
|
|
693
|
+
return edge;
|
|
694
|
+
}
|
|
695
|
+
changed = true;
|
|
696
|
+
return normalizeEdge(
|
|
697
|
+
{
|
|
698
|
+
...edge,
|
|
699
|
+
...changes,
|
|
700
|
+
metadata: changes.metadata ?? edge.metadata,
|
|
701
|
+
route: {
|
|
702
|
+
...edge.route,
|
|
703
|
+
...changes.route
|
|
704
|
+
},
|
|
705
|
+
style: {
|
|
706
|
+
...edge.style,
|
|
707
|
+
...changes.style
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
nodes
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
return {
|
|
714
|
+
document: normalizeDocument({ ...document, edges }),
|
|
715
|
+
changedObjectIds: changed ? [edgeId] : []
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
var deleteEdge = (document, edgeId) => ({
|
|
719
|
+
document: normalizeDocument({
|
|
720
|
+
...document,
|
|
721
|
+
edges: document.edges.filter((edge) => edge.id !== edgeId)
|
|
722
|
+
}),
|
|
723
|
+
changedObjectIds: [edgeId]
|
|
724
|
+
});
|
|
725
|
+
var replaceDocument = (document, replacement) => ({
|
|
726
|
+
document: normalizeDocument({
|
|
727
|
+
...replacement,
|
|
728
|
+
id: document.id,
|
|
729
|
+
revision: document.revision,
|
|
730
|
+
metadata: {
|
|
731
|
+
...document.metadata,
|
|
732
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
733
|
+
}
|
|
734
|
+
}),
|
|
735
|
+
changedObjectIds: [
|
|
736
|
+
replacement.id,
|
|
737
|
+
...replacement.nodes.map((node) => node.id),
|
|
738
|
+
...replacement.edges.map((edge) => edge.id)
|
|
739
|
+
]
|
|
740
|
+
});
|
|
741
|
+
var recalculatePaths = (document, edgeIds, nextRevision) => {
|
|
742
|
+
const before = edgeMapOf(document);
|
|
743
|
+
const next = recalculateEdgePaths(document, edgeIds, nextRevision);
|
|
744
|
+
const changedObjectIds = next.edges.filter((edge) => JSON.stringify(before.get(edge.id)?.route) !== JSON.stringify(edge.route)).map((edge) => edge.id);
|
|
745
|
+
return {
|
|
746
|
+
document: normalizeDocument(next),
|
|
747
|
+
changedObjectIds
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
var applyPatchOperations = (document, operations, nextRevision) => {
|
|
751
|
+
let current = document;
|
|
752
|
+
const changedObjectIds = /* @__PURE__ */ new Set();
|
|
753
|
+
for (const operation of operations) {
|
|
754
|
+
let result;
|
|
755
|
+
switch (operation.op) {
|
|
756
|
+
case "addNode":
|
|
757
|
+
result = addNode(current, operation.node);
|
|
758
|
+
break;
|
|
759
|
+
case "updateNode":
|
|
760
|
+
result = updateNode(current, operation.nodeId, operation.changes);
|
|
761
|
+
break;
|
|
762
|
+
case "deleteNode":
|
|
763
|
+
result = deleteNode(current, operation.nodeId);
|
|
764
|
+
break;
|
|
765
|
+
case "addEdge":
|
|
766
|
+
result = addEdge(current, operation.edge);
|
|
767
|
+
break;
|
|
768
|
+
case "updateEdge":
|
|
769
|
+
result = updateEdge(current, operation.edgeId, operation.changes);
|
|
770
|
+
break;
|
|
771
|
+
case "deleteEdge":
|
|
772
|
+
result = deleteEdge(current, operation.edgeId);
|
|
773
|
+
break;
|
|
774
|
+
case "replaceDocument":
|
|
775
|
+
result = replaceDocument(current, operation.document);
|
|
776
|
+
break;
|
|
777
|
+
case "recalculateEdgePaths":
|
|
778
|
+
result = recalculatePaths(current, operation.edgeIds, nextRevision);
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
current = result.document;
|
|
782
|
+
for (const objectId of result.changedObjectIds) {
|
|
783
|
+
changedObjectIds.add(objectId);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
document: current,
|
|
788
|
+
changedObjectIds: [...changedObjectIds]
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
var DEFAULT_INSECURE_DEV_AUTH_TOKEN = "local-dev-token";
|
|
793
|
+
var INSECURE_DEV_ACTOR = {
|
|
794
|
+
actorId: "local-dev",
|
|
795
|
+
ownerId: "local-dev",
|
|
796
|
+
tenantId: "local"
|
|
797
|
+
};
|
|
798
|
+
var authFailure = (code, message) => ({
|
|
799
|
+
success: false,
|
|
800
|
+
errors: [error(code, message)]
|
|
801
|
+
});
|
|
802
|
+
var resolveAuthOptions = (options = {}) => ({
|
|
803
|
+
tokenRegistry: options.tokenRegistry ?? {},
|
|
804
|
+
allowInsecureDevBypass: options.allowInsecureDevBypass ?? false,
|
|
805
|
+
insecureDevToken: options.insecureDevToken?.trim() || DEFAULT_INSECURE_DEV_AUTH_TOKEN
|
|
806
|
+
});
|
|
807
|
+
var resolveActorFromAuthorizationHeader = (authorizationHeader, options) => {
|
|
808
|
+
if (authorizationHeader === void 0) {
|
|
809
|
+
return authFailure("AUTH_REQUIRED", "Authorization header is required.");
|
|
810
|
+
}
|
|
811
|
+
if (Array.isArray(authorizationHeader)) {
|
|
812
|
+
return authFailure("INVALID_AUTH_TOKEN", "Authorization header must be a single Bearer token.");
|
|
813
|
+
}
|
|
814
|
+
const match = authorizationHeader.match(/^Bearer\s+(.+)$/i);
|
|
815
|
+
if (!match) {
|
|
816
|
+
return authFailure("INVALID_AUTH_TOKEN", "Authorization header must use Bearer <token>.");
|
|
817
|
+
}
|
|
818
|
+
const token = match[1]?.trim();
|
|
819
|
+
if (!token) {
|
|
820
|
+
return authFailure("INVALID_AUTH_TOKEN", "Authorization bearer token must not be empty.");
|
|
821
|
+
}
|
|
822
|
+
if (options.allowInsecureDevBypass && token === options.insecureDevToken) {
|
|
823
|
+
return {
|
|
824
|
+
success: true,
|
|
825
|
+
actor: { ...INSECURE_DEV_ACTOR }
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
const actor = options.tokenRegistry[token];
|
|
829
|
+
if (!actor) {
|
|
830
|
+
return authFailure("INVALID_AUTH_TOKEN", "Authorization token is not recognized.");
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
success: true,
|
|
834
|
+
actor: { ...actor }
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
import Database from "better-sqlite3";
|
|
839
|
+
var MIGRATIONS = [
|
|
840
|
+
`
|
|
841
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
842
|
+
id TEXT PRIMARY KEY,
|
|
843
|
+
tenant_id TEXT NOT NULL,
|
|
844
|
+
owner_id TEXT NOT NULL,
|
|
845
|
+
title TEXT NOT NULL,
|
|
846
|
+
revision INTEGER NOT NULL,
|
|
847
|
+
document_json TEXT NOT NULL,
|
|
848
|
+
created_at TEXT NOT NULL,
|
|
849
|
+
updated_at TEXT NOT NULL
|
|
850
|
+
);
|
|
851
|
+
`,
|
|
852
|
+
`
|
|
853
|
+
CREATE INDEX IF NOT EXISTS idx_documents_tenant_updated
|
|
854
|
+
ON documents (tenant_id, updated_at DESC);
|
|
855
|
+
`,
|
|
856
|
+
`
|
|
857
|
+
CREATE TABLE IF NOT EXISTS document_operation_logs (
|
|
858
|
+
request_id TEXT PRIMARY KEY,
|
|
859
|
+
document_id TEXT NOT NULL,
|
|
860
|
+
tenant_id TEXT NOT NULL,
|
|
861
|
+
actor_id TEXT NOT NULL,
|
|
862
|
+
operation_type TEXT NOT NULL,
|
|
863
|
+
success INTEGER NOT NULL,
|
|
864
|
+
revision INTEGER,
|
|
865
|
+
changed_object_ids_json TEXT NOT NULL,
|
|
866
|
+
errors_json TEXT NOT NULL,
|
|
867
|
+
warnings_json TEXT NOT NULL,
|
|
868
|
+
created_at TEXT NOT NULL
|
|
869
|
+
);
|
|
870
|
+
`,
|
|
871
|
+
`
|
|
872
|
+
CREATE INDEX IF NOT EXISTS idx_document_operation_logs_document
|
|
873
|
+
ON document_operation_logs (document_id, created_at DESC);
|
|
874
|
+
`
|
|
875
|
+
];
|
|
876
|
+
var isSqliteConstraintError = (cause) => cause instanceof Error && "code" in cause && typeof cause.code === "string" && cause.code.startsWith("SQLITE_CONSTRAINT");
|
|
877
|
+
var documentParams = (document) => ({
|
|
878
|
+
id: document.id,
|
|
879
|
+
tenant_id: document.metadata.tenantId,
|
|
880
|
+
owner_id: document.metadata.ownerId,
|
|
881
|
+
title: document.title,
|
|
882
|
+
revision: document.revision,
|
|
883
|
+
document_json: JSON.stringify(document),
|
|
884
|
+
created_at: document.metadata.createdAt,
|
|
885
|
+
updated_at: document.metadata.updatedAt
|
|
886
|
+
});
|
|
887
|
+
var DocumentAlreadyExistsError = class extends Error {
|
|
888
|
+
constructor(documentId) {
|
|
889
|
+
super(`Document already exists: ${documentId}`);
|
|
890
|
+
this.name = "DocumentAlreadyExistsError";
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var SQLiteDocumentRepository = class {
|
|
894
|
+
db;
|
|
895
|
+
constructor(filename = ":memory:") {
|
|
896
|
+
this.db = new Database(filename);
|
|
897
|
+
this.db.pragma("foreign_keys = ON");
|
|
898
|
+
if (filename !== ":memory:") {
|
|
899
|
+
this.db.pragma("journal_mode = WAL");
|
|
900
|
+
}
|
|
901
|
+
this.db.pragma("busy_timeout = 5000");
|
|
902
|
+
for (const migration of MIGRATIONS) {
|
|
903
|
+
this.db.exec(migration);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
close() {
|
|
907
|
+
this.db.close();
|
|
908
|
+
}
|
|
909
|
+
transaction(callback) {
|
|
910
|
+
return this.db.transaction(callback)();
|
|
911
|
+
}
|
|
912
|
+
getDocument(id) {
|
|
913
|
+
const row = this.db.prepare("SELECT document_json FROM documents WHERE id = ?").get(id);
|
|
914
|
+
return row ? JSON.parse(row.document_json) : null;
|
|
915
|
+
}
|
|
916
|
+
insertDocument(document) {
|
|
917
|
+
try {
|
|
918
|
+
this.db.prepare(
|
|
919
|
+
`
|
|
920
|
+
INSERT INTO documents (
|
|
921
|
+
id, tenant_id, owner_id, title, revision, document_json, created_at, updated_at
|
|
922
|
+
) VALUES (
|
|
923
|
+
@id, @tenant_id, @owner_id, @title, @revision, @document_json, @created_at, @updated_at
|
|
924
|
+
)
|
|
925
|
+
`
|
|
926
|
+
).run(documentParams(document));
|
|
927
|
+
} catch (cause) {
|
|
928
|
+
if (isSqliteConstraintError(cause)) {
|
|
929
|
+
throw new DocumentAlreadyExistsError(document.id);
|
|
930
|
+
}
|
|
931
|
+
throw cause;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
updateDocument(document) {
|
|
935
|
+
const result = this.db.prepare(
|
|
936
|
+
`
|
|
937
|
+
UPDATE documents
|
|
938
|
+
SET
|
|
939
|
+
tenant_id = @tenant_id,
|
|
940
|
+
owner_id = @owner_id,
|
|
941
|
+
title = @title,
|
|
942
|
+
revision = @revision,
|
|
943
|
+
document_json = @document_json,
|
|
944
|
+
created_at = @created_at,
|
|
945
|
+
updated_at = @updated_at
|
|
946
|
+
WHERE id = @id
|
|
947
|
+
`
|
|
948
|
+
).run(documentParams(document));
|
|
949
|
+
if (result.changes === 0) {
|
|
950
|
+
throw new Error(`Document not found for update: ${document.id}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
insertOperationLog(entry) {
|
|
954
|
+
this.db.prepare(
|
|
955
|
+
`
|
|
956
|
+
INSERT INTO document_operation_logs (
|
|
957
|
+
request_id, document_id, tenant_id, actor_id, operation_type, success, revision,
|
|
958
|
+
changed_object_ids_json, errors_json, warnings_json, created_at
|
|
959
|
+
) VALUES (
|
|
960
|
+
@request_id, @document_id, @tenant_id, @actor_id, @operation_type, @success, @revision,
|
|
961
|
+
@changed_object_ids_json, @errors_json, @warnings_json, @created_at
|
|
962
|
+
)
|
|
963
|
+
`
|
|
964
|
+
).run({
|
|
965
|
+
request_id: entry.requestId,
|
|
966
|
+
document_id: entry.documentId,
|
|
967
|
+
tenant_id: entry.tenantId,
|
|
968
|
+
actor_id: entry.actorId,
|
|
969
|
+
operation_type: entry.operationType,
|
|
970
|
+
success: entry.success ? 1 : 0,
|
|
971
|
+
revision: entry.revision,
|
|
972
|
+
changed_object_ids_json: JSON.stringify(entry.changedObjectIds),
|
|
973
|
+
errors_json: JSON.stringify(entry.errors),
|
|
974
|
+
warnings_json: JSON.stringify(entry.warnings),
|
|
975
|
+
created_at: entry.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
var SqliteDocumentRepository = class extends SQLiteDocumentRepository {
|
|
980
|
+
insertDocument(document) {
|
|
981
|
+
super.insertDocument(document);
|
|
982
|
+
}
|
|
983
|
+
updateDocument(document) {
|
|
984
|
+
super.updateDocument(document);
|
|
985
|
+
}
|
|
986
|
+
logOperation(entry) {
|
|
987
|
+
this.insertOperationLog({
|
|
988
|
+
...entry,
|
|
989
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
995
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
996
|
+
"mxfile",
|
|
997
|
+
"diagram",
|
|
998
|
+
"mxGraphModel",
|
|
999
|
+
"root",
|
|
1000
|
+
"mxCell",
|
|
1001
|
+
"mxGeometry",
|
|
1002
|
+
"Array",
|
|
1003
|
+
"mxPoint"
|
|
1004
|
+
]);
|
|
1005
|
+
var childElements = (node) => {
|
|
1006
|
+
const elements = [];
|
|
1007
|
+
for (let index = 0; index < node.childNodes.length; index += 1) {
|
|
1008
|
+
const child = node.childNodes.item(index);
|
|
1009
|
+
if (child && child.nodeType === child.ELEMENT_NODE) {
|
|
1010
|
+
elements.push(child);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return elements;
|
|
1014
|
+
};
|
|
1015
|
+
var getSingleChild = (node, tagName) => childElements(node).find((child) => child.tagName === tagName);
|
|
1016
|
+
var validateDrawioXml = (xml) => {
|
|
1017
|
+
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
|
1018
|
+
const root = doc.documentElement;
|
|
1019
|
+
if (!root) {
|
|
1020
|
+
return { errors: [error("DRAWIO_INVALID_XML", "XML could not be parsed")], warnings: [] };
|
|
1021
|
+
}
|
|
1022
|
+
const errors = [];
|
|
1023
|
+
const visit = (element) => {
|
|
1024
|
+
if (!ALLOWED_TAGS.has(element.tagName)) {
|
|
1025
|
+
errors.push(error("DRAWIO_INVALID_TAG", `Unsupported draw.io tag: ${element.tagName}`));
|
|
1026
|
+
}
|
|
1027
|
+
for (const child of childElements(element)) {
|
|
1028
|
+
visit(child);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
visit(root);
|
|
1032
|
+
let graphModel;
|
|
1033
|
+
if (root.tagName === "mxfile") {
|
|
1034
|
+
const diagram = getSingleChild(root, "diagram");
|
|
1035
|
+
graphModel = diagram ? getSingleChild(diagram, "mxGraphModel") : void 0;
|
|
1036
|
+
} else if (root.tagName === "mxGraphModel") {
|
|
1037
|
+
graphModel = root;
|
|
1038
|
+
} else {
|
|
1039
|
+
errors.push(error("DRAWIO_INVALID_ROOT", "Root element must be mxfile or mxGraphModel"));
|
|
1040
|
+
}
|
|
1041
|
+
if (!graphModel) {
|
|
1042
|
+
errors.push(error("DRAWIO_INVALID_STRUCTURE", "mxGraphModel element is required"));
|
|
1043
|
+
return { errors, warnings: [] };
|
|
1044
|
+
}
|
|
1045
|
+
const graphRoot = getSingleChild(graphModel, "root");
|
|
1046
|
+
if (!graphRoot) {
|
|
1047
|
+
errors.push(error("DRAWIO_INVALID_STRUCTURE", "mxGraphModel must contain root"));
|
|
1048
|
+
return { errors, warnings: [] };
|
|
1049
|
+
}
|
|
1050
|
+
const cells = childElements(graphRoot).filter((element) => element.tagName === "mxCell");
|
|
1051
|
+
const cellIds = new Set(cells.map((cell) => cell.getAttribute("id")).filter(Boolean));
|
|
1052
|
+
if (!cellIds.has("0") || !cells.some((cell) => cell.getAttribute("id") === "1" && cell.getAttribute("parent") === "0")) {
|
|
1053
|
+
errors.push(error("DRAWIO_INVALID_STRUCTURE", "Missing required mxCell skeleton"));
|
|
1054
|
+
}
|
|
1055
|
+
for (const cell of cells) {
|
|
1056
|
+
const cellId = cell.getAttribute("id") ?? void 0;
|
|
1057
|
+
const isEdge = cell.getAttribute("edge") === "1";
|
|
1058
|
+
const parent = cell.getAttribute("parent");
|
|
1059
|
+
if (cellId !== "0" && parent && !cellIds.has(parent)) {
|
|
1060
|
+
errors.push(error("DRAWIO_REFERENCE_MISSING", "mxCell parent reference is invalid", { objectId: cellId }));
|
|
1061
|
+
}
|
|
1062
|
+
if (isEdge) {
|
|
1063
|
+
const source = cell.getAttribute("source");
|
|
1064
|
+
const target = cell.getAttribute("target");
|
|
1065
|
+
if (!source || !target || !parent) {
|
|
1066
|
+
errors.push(error("DRAWIO_INVALID_EDGE", "Edge cells require source, target, and parent", { objectId: cellId }));
|
|
1067
|
+
}
|
|
1068
|
+
if (source && !cellIds.has(source)) {
|
|
1069
|
+
errors.push(error("DRAWIO_REFERENCE_MISSING", "Edge source reference is invalid", { objectId: cellId }));
|
|
1070
|
+
}
|
|
1071
|
+
if (target && !cellIds.has(target)) {
|
|
1072
|
+
errors.push(error("DRAWIO_REFERENCE_MISSING", "Edge target reference is invalid", { objectId: cellId }));
|
|
1073
|
+
}
|
|
1074
|
+
const geometry = childElements(cell).find(
|
|
1075
|
+
(child) => child.tagName === "mxGeometry" && child.getAttribute("relative") === "1" && child.getAttribute("as") === "geometry"
|
|
1076
|
+
);
|
|
1077
|
+
if (!geometry) {
|
|
1078
|
+
errors.push(
|
|
1079
|
+
error("DRAWIO_INVALID_EDGE", 'Edge cells require <mxGeometry relative="1" as="geometry" />', {
|
|
1080
|
+
objectId: cellId
|
|
1081
|
+
})
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { errors, warnings: [] };
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
import { z } from "zod";
|
|
1090
|
+
var idPattern = (prefix) => new RegExp(`^${prefix}[a-z0-9][a-z0-9_-]{2,63}$`);
|
|
1091
|
+
var literalUnion = (values) => z.union(values.map((value) => z.literal(value)));
|
|
1092
|
+
var hexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, "Expected six-digit hex color");
|
|
1093
|
+
var metadataSchema = z.record(z.string(), z.string());
|
|
1094
|
+
var documentMetadataSchema = z.object({
|
|
1095
|
+
createdBy: z.string().min(1).max(64),
|
|
1096
|
+
createdAt: z.string().datetime(),
|
|
1097
|
+
updatedAt: z.string().datetime(),
|
|
1098
|
+
ownerId: z.string().min(1).max(64),
|
|
1099
|
+
tenantId: z.string().min(1).max(64)
|
|
1100
|
+
}).strict();
|
|
1101
|
+
var canvasSchema = z.object({
|
|
1102
|
+
width: z.number().int().min(320).max(1e4),
|
|
1103
|
+
height: z.number().int().min(240).max(1e4),
|
|
1104
|
+
backgroundColor: hexColorSchema,
|
|
1105
|
+
padding: z.number().int().min(0).max(400),
|
|
1106
|
+
autoExpand: z.boolean()
|
|
1107
|
+
}).strict();
|
|
1108
|
+
var nodeStyleInputSchema = z.object({
|
|
1109
|
+
preset: z.enum(STYLE_PRESETS).optional(),
|
|
1110
|
+
fillColor: hexColorSchema.optional(),
|
|
1111
|
+
strokeColor: hexColorSchema.optional(),
|
|
1112
|
+
strokeWidth: literalUnion(STROKE_WIDTHS).optional(),
|
|
1113
|
+
dashed: z.boolean().optional(),
|
|
1114
|
+
rounded: z.boolean().optional(),
|
|
1115
|
+
textColor: hexColorSchema.optional(),
|
|
1116
|
+
fontSize: literalUnion(FONT_SIZES).optional(),
|
|
1117
|
+
fontWeight: z.enum(FONT_WEIGHTS).optional(),
|
|
1118
|
+
textAlign: z.enum(TEXT_ALIGNS).optional(),
|
|
1119
|
+
verticalAlign: z.enum(VERTICAL_ALIGNS).optional(),
|
|
1120
|
+
wrap: z.boolean().optional()
|
|
1121
|
+
}).strict();
|
|
1122
|
+
var edgeStyleInputSchema = z.object({
|
|
1123
|
+
preset: z.enum(STYLE_PRESETS).optional(),
|
|
1124
|
+
strokeColor: hexColorSchema.optional(),
|
|
1125
|
+
strokeWidth: literalUnion(STROKE_WIDTHS).optional(),
|
|
1126
|
+
dashed: z.boolean().optional(),
|
|
1127
|
+
textColor: hexColorSchema.optional(),
|
|
1128
|
+
fontSize: literalUnion(EDGE_FONT_SIZES).optional(),
|
|
1129
|
+
fontWeight: z.enum(FONT_WEIGHTS).optional(),
|
|
1130
|
+
textAlign: z.enum(TEXT_ALIGNS).optional(),
|
|
1131
|
+
verticalAlign: z.enum(VERTICAL_ALIGNS).optional(),
|
|
1132
|
+
wrap: z.boolean().optional(),
|
|
1133
|
+
arrowStart: z.enum(ARROW_TYPES).optional(),
|
|
1134
|
+
arrowEnd: z.enum(ARROW_TYPES).optional()
|
|
1135
|
+
}).strict();
|
|
1136
|
+
var resolvedNodeStyleSchema = nodeStyleInputSchema.extend({
|
|
1137
|
+
preset: z.enum(STYLE_PRESETS),
|
|
1138
|
+
fillColor: hexColorSchema,
|
|
1139
|
+
strokeColor: hexColorSchema,
|
|
1140
|
+
strokeWidth: literalUnion(STROKE_WIDTHS),
|
|
1141
|
+
dashed: z.boolean(),
|
|
1142
|
+
rounded: z.boolean(),
|
|
1143
|
+
textColor: hexColorSchema,
|
|
1144
|
+
fontSize: literalUnion(FONT_SIZES),
|
|
1145
|
+
fontWeight: z.enum(FONT_WEIGHTS),
|
|
1146
|
+
textAlign: z.enum(TEXT_ALIGNS),
|
|
1147
|
+
verticalAlign: z.enum(VERTICAL_ALIGNS),
|
|
1148
|
+
wrap: z.boolean()
|
|
1149
|
+
});
|
|
1150
|
+
var resolvedEdgeStyleSchema = edgeStyleInputSchema.extend({
|
|
1151
|
+
preset: z.enum(STYLE_PRESETS),
|
|
1152
|
+
strokeColor: hexColorSchema,
|
|
1153
|
+
strokeWidth: literalUnion(STROKE_WIDTHS),
|
|
1154
|
+
dashed: z.boolean(),
|
|
1155
|
+
textColor: hexColorSchema,
|
|
1156
|
+
fontSize: literalUnion(EDGE_FONT_SIZES),
|
|
1157
|
+
fontWeight: z.enum(FONT_WEIGHTS),
|
|
1158
|
+
textAlign: z.enum(TEXT_ALIGNS),
|
|
1159
|
+
verticalAlign: z.enum(VERTICAL_ALIGNS),
|
|
1160
|
+
wrap: z.boolean(),
|
|
1161
|
+
arrowStart: z.enum(ARROW_TYPES),
|
|
1162
|
+
arrowEnd: z.enum(ARROW_TYPES)
|
|
1163
|
+
});
|
|
1164
|
+
var pointSchema = z.object({
|
|
1165
|
+
x: z.number().int(),
|
|
1166
|
+
y: z.number().int()
|
|
1167
|
+
}).strict();
|
|
1168
|
+
var edgeRouteSchema = z.object({
|
|
1169
|
+
points: z.array(pointSchema),
|
|
1170
|
+
sourceAnchor: z.enum(ANCHOR_SIDES),
|
|
1171
|
+
targetAnchor: z.enum(ANCHOR_SIDES),
|
|
1172
|
+
calculatedAtRevision: z.number().int().nullable()
|
|
1173
|
+
}).strict();
|
|
1174
|
+
var nodeInputSchema = z.object({
|
|
1175
|
+
id: z.string().regex(idPattern(NODE_ID_PREFIX)).optional(),
|
|
1176
|
+
type: z.enum(NODE_TYPES),
|
|
1177
|
+
x: z.number().int().min(0).max(1e5),
|
|
1178
|
+
y: z.number().int().min(0).max(1e5),
|
|
1179
|
+
width: z.number().int().min(16).max(4e3),
|
|
1180
|
+
height: z.number().int().min(16).max(4e3),
|
|
1181
|
+
text: z.string().max(4e3).default(""),
|
|
1182
|
+
style: nodeStyleInputSchema.optional(),
|
|
1183
|
+
zIndex: z.number().int().min(0).max(9999).default(0),
|
|
1184
|
+
metadata: metadataSchema.default({})
|
|
1185
|
+
}).strict();
|
|
1186
|
+
var nodeChangesSchema = z.object({
|
|
1187
|
+
type: z.enum(NODE_TYPES).optional(),
|
|
1188
|
+
x: z.number().int().min(0).max(1e5).optional(),
|
|
1189
|
+
y: z.number().int().min(0).max(1e5).optional(),
|
|
1190
|
+
width: z.number().int().min(16).max(4e3).optional(),
|
|
1191
|
+
height: z.number().int().min(16).max(4e3).optional(),
|
|
1192
|
+
text: z.string().max(4e3).optional(),
|
|
1193
|
+
style: nodeStyleInputSchema.optional(),
|
|
1194
|
+
zIndex: z.number().int().min(0).max(9999).optional(),
|
|
1195
|
+
metadata: metadataSchema.optional()
|
|
1196
|
+
}).strict();
|
|
1197
|
+
var edgeInputSchema = z.object({
|
|
1198
|
+
id: z.string().regex(idPattern(EDGE_ID_PREFIX)).optional(),
|
|
1199
|
+
sourceNodeId: z.string().regex(idPattern(NODE_ID_PREFIX)),
|
|
1200
|
+
targetNodeId: z.string().regex(idPattern(NODE_ID_PREFIX)),
|
|
1201
|
+
label: z.string().max(1e3).default(""),
|
|
1202
|
+
pathType: z.enum(EDGE_PATH_TYPES).default("straight"),
|
|
1203
|
+
route: edgeRouteSchema.partial().optional(),
|
|
1204
|
+
style: edgeStyleInputSchema.optional(),
|
|
1205
|
+
metadata: metadataSchema.default({})
|
|
1206
|
+
}).strict();
|
|
1207
|
+
var edgeChangesSchema = z.object({
|
|
1208
|
+
sourceNodeId: z.string().regex(idPattern(NODE_ID_PREFIX)).optional(),
|
|
1209
|
+
targetNodeId: z.string().regex(idPattern(NODE_ID_PREFIX)).optional(),
|
|
1210
|
+
label: z.string().max(1e3).optional(),
|
|
1211
|
+
pathType: z.enum(EDGE_PATH_TYPES).optional(),
|
|
1212
|
+
route: edgeRouteSchema.partial().optional(),
|
|
1213
|
+
style: edgeStyleInputSchema.optional(),
|
|
1214
|
+
metadata: metadataSchema.optional()
|
|
1215
|
+
}).strict();
|
|
1216
|
+
var nodeSchema = nodeInputSchema.extend({
|
|
1217
|
+
id: z.string().regex(idPattern(NODE_ID_PREFIX)),
|
|
1218
|
+
style: resolvedNodeStyleSchema
|
|
1219
|
+
});
|
|
1220
|
+
var edgeSchema = edgeInputSchema.extend({
|
|
1221
|
+
id: z.string().regex(idPattern(EDGE_ID_PREFIX)),
|
|
1222
|
+
route: edgeRouteSchema,
|
|
1223
|
+
style: resolvedEdgeStyleSchema
|
|
1224
|
+
});
|
|
1225
|
+
var documentSchema = z.object({
|
|
1226
|
+
id: z.string().regex(idPattern(DOCUMENT_ID_PREFIX)),
|
|
1227
|
+
title: z.string().min(1).max(120),
|
|
1228
|
+
revision: z.number().int().min(1),
|
|
1229
|
+
metadata: documentMetadataSchema,
|
|
1230
|
+
canvas: canvasSchema,
|
|
1231
|
+
nodes: z.array(nodeSchema),
|
|
1232
|
+
edges: z.array(edgeSchema)
|
|
1233
|
+
}).strict();
|
|
1234
|
+
var createDocumentRequestSchema = z.object({
|
|
1235
|
+
id: z.string().regex(idPattern(DOCUMENT_ID_PREFIX)).optional(),
|
|
1236
|
+
title: z.string().min(1).max(120).optional(),
|
|
1237
|
+
canvas: canvasSchema.partial().optional()
|
|
1238
|
+
}).strict();
|
|
1239
|
+
var baseRevisionSchema = z.object({
|
|
1240
|
+
baseRevision: z.number().int().min(1)
|
|
1241
|
+
});
|
|
1242
|
+
var addNodeRequestSchema = z.object({
|
|
1243
|
+
baseRevision: z.number().int().min(1),
|
|
1244
|
+
node: nodeInputSchema
|
|
1245
|
+
}).strict();
|
|
1246
|
+
var updateNodeRequestSchema = z.object({
|
|
1247
|
+
baseRevision: z.number().int().min(1),
|
|
1248
|
+
changes: nodeChangesSchema
|
|
1249
|
+
}).strict();
|
|
1250
|
+
var addEdgeRequestSchema = z.object({
|
|
1251
|
+
baseRevision: z.number().int().min(1),
|
|
1252
|
+
edge: edgeInputSchema
|
|
1253
|
+
}).strict();
|
|
1254
|
+
var updateEdgeRequestSchema = z.object({
|
|
1255
|
+
baseRevision: z.number().int().min(1),
|
|
1256
|
+
changes: edgeChangesSchema
|
|
1257
|
+
}).strict();
|
|
1258
|
+
var cloneDocumentRequestSchema = z.object({
|
|
1259
|
+
title: z.string().min(1).max(120).optional()
|
|
1260
|
+
}).strict();
|
|
1261
|
+
var patchOperationSchema = z.discriminatedUnion("op", [
|
|
1262
|
+
z.object({ op: z.literal("addNode"), node: nodeInputSchema }).strict(),
|
|
1263
|
+
z.object({ op: z.literal("updateNode"), nodeId: z.string().regex(idPattern(NODE_ID_PREFIX)), changes: nodeChangesSchema }).strict(),
|
|
1264
|
+
z.object({ op: z.literal("deleteNode"), nodeId: z.string().regex(idPattern(NODE_ID_PREFIX)) }).strict(),
|
|
1265
|
+
z.object({ op: z.literal("addEdge"), edge: edgeInputSchema }).strict(),
|
|
1266
|
+
z.object({ op: z.literal("updateEdge"), edgeId: z.string().regex(idPattern(EDGE_ID_PREFIX)), changes: edgeChangesSchema }).strict(),
|
|
1267
|
+
z.object({ op: z.literal("deleteEdge"), edgeId: z.string().regex(idPattern(EDGE_ID_PREFIX)) }).strict(),
|
|
1268
|
+
z.object({ op: z.literal("replaceDocument"), document: documentSchema }).strict(),
|
|
1269
|
+
z.object({
|
|
1270
|
+
op: z.literal("recalculateEdgePaths"),
|
|
1271
|
+
edgeIds: z.array(z.string().regex(idPattern(EDGE_ID_PREFIX))).optional()
|
|
1272
|
+
}).strict()
|
|
1273
|
+
]);
|
|
1274
|
+
var patchRequestSchema = z.object({
|
|
1275
|
+
baseRevision: z.number().int().min(1),
|
|
1276
|
+
atomic: z.boolean().default(true),
|
|
1277
|
+
dryRun: z.boolean().default(false),
|
|
1278
|
+
operations: z.array(patchOperationSchema).min(1)
|
|
1279
|
+
}).strict();
|
|
1280
|
+
var validateRequestSchema = z.object({
|
|
1281
|
+
target: z.enum(["document", "drawio"]).default("document"),
|
|
1282
|
+
drawioMode: z.enum(["mxfile", "mxGraphModel"]).default("mxfile")
|
|
1283
|
+
}).strict();
|
|
1284
|
+
var renderRequestSchema = z.object({
|
|
1285
|
+
includeObjectMetadata: z.boolean().default(true),
|
|
1286
|
+
fitToCanvas: z.boolean().default(true)
|
|
1287
|
+
}).strict();
|
|
1288
|
+
var exportRequestSchema = z.object({
|
|
1289
|
+
format: z.enum(["drawio", "svg", "png"]).default("drawio"),
|
|
1290
|
+
drawioMode: z.enum(["mxfile", "mxGraphModel"]).default("mxfile"),
|
|
1291
|
+
scale: z.union([z.literal(1), z.literal(2), z.literal(3)]).default(1)
|
|
1292
|
+
}).strict();
|
|
1293
|
+
var issuePath = (issue) => issue.path.join(".");
|
|
1294
|
+
var mapIssue = (issue) => {
|
|
1295
|
+
const path = issuePath(issue);
|
|
1296
|
+
const isStylePath = path.includes("style");
|
|
1297
|
+
if (issue.code === "unrecognized_keys") {
|
|
1298
|
+
return issue.keys.map(
|
|
1299
|
+
(key) => error(
|
|
1300
|
+
isStylePath ? "INVALID_STYLE_KEY" : "INVALID_INPUT",
|
|
1301
|
+
`Unknown property: ${key}`,
|
|
1302
|
+
{ fieldPath: path ? `${path}.${key}` : key }
|
|
1303
|
+
)
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
if (issue.code === "invalid_value") {
|
|
1307
|
+
if (path.endsWith("type")) {
|
|
1308
|
+
return [error("UNSUPPORTED_NODE_TYPE", "Unsupported node type", { fieldPath: path })];
|
|
1309
|
+
}
|
|
1310
|
+
if (path.endsWith("pathType")) {
|
|
1311
|
+
return [error("UNSUPPORTED_EDGE_PATH", "Unsupported edge path type", { fieldPath: path })];
|
|
1312
|
+
}
|
|
1313
|
+
return [error(isStylePath ? "INVALID_STYLE" : "INVALID_INPUT", issue.message, { fieldPath: path })];
|
|
1314
|
+
}
|
|
1315
|
+
if (issue.code === "invalid_format" && path.toLowerCase().includes("color")) {
|
|
1316
|
+
return [error("INVALID_COLOR", issue.message, { fieldPath: path })];
|
|
1317
|
+
}
|
|
1318
|
+
if (issue.code === "too_small" || issue.code === "too_big") {
|
|
1319
|
+
const code = path.endsWith("x") || path.endsWith("y") || path.endsWith("width") || path.endsWith("height") || path.endsWith("padding") ? "INVALID_GEOMETRY" : isStylePath ? "INVALID_STYLE" : "INVALID_INPUT";
|
|
1320
|
+
return [error(code, issue.message, { fieldPath: path })];
|
|
1321
|
+
}
|
|
1322
|
+
return [
|
|
1323
|
+
error(isStylePath ? "INVALID_STYLE" : "INVALID_INPUT", issue.message, {
|
|
1324
|
+
...path ? { fieldPath: path } : {}
|
|
1325
|
+
})
|
|
1326
|
+
];
|
|
1327
|
+
};
|
|
1328
|
+
var safeParseWithErrors = (schema, input) => {
|
|
1329
|
+
const result = schema.safeParse(input);
|
|
1330
|
+
if (result.success) {
|
|
1331
|
+
return result;
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
success: false,
|
|
1335
|
+
errors: result.error.issues.flatMap(mapIssue)
|
|
1336
|
+
};
|
|
1337
|
+
};
|
|
1338
|
+
var STYLE_ALLOWLISTS = {
|
|
1339
|
+
node: new Set(NODE_STYLE_KEYS),
|
|
1340
|
+
edge: new Set(EDGE_STYLE_KEYS)
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
var lineHeightMultiplier = 1.2;
|
|
1344
|
+
var nodeOverlapThreshold = 0.25;
|
|
1345
|
+
var minimumMeaningfulOverlapArea = 400;
|
|
1346
|
+
var lineHeight = (fontSize) => Math.round(fontSize * lineHeightMultiplier);
|
|
1347
|
+
var intersectionArea = (a, b) => {
|
|
1348
|
+
const x = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
|
|
1349
|
+
const y = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
|
1350
|
+
return x * y;
|
|
1351
|
+
};
|
|
1352
|
+
var splitLongWord = (word, maxChars) => {
|
|
1353
|
+
const parts = [];
|
|
1354
|
+
for (let index = 0; index < word.length; index += maxChars) {
|
|
1355
|
+
parts.push(word.slice(index, index + maxChars));
|
|
1356
|
+
}
|
|
1357
|
+
return parts;
|
|
1358
|
+
};
|
|
1359
|
+
var wrapText = (text, maxWidth, fontSize, wrap) => {
|
|
1360
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / (fontSize * 0.56)));
|
|
1361
|
+
return text.split("\n").flatMap((paragraph) => {
|
|
1362
|
+
if (!wrap) {
|
|
1363
|
+
return [paragraph];
|
|
1364
|
+
}
|
|
1365
|
+
const words = paragraph.split(/\s+/).filter(Boolean);
|
|
1366
|
+
if (words.length === 0) {
|
|
1367
|
+
return [""];
|
|
1368
|
+
}
|
|
1369
|
+
const lines = [];
|
|
1370
|
+
let current = "";
|
|
1371
|
+
for (const word of words) {
|
|
1372
|
+
const candidates = word.length > maxChars ? splitLongWord(word, maxChars) : [word];
|
|
1373
|
+
for (const candidate of candidates) {
|
|
1374
|
+
const next = current ? `${current} ${candidate}` : candidate;
|
|
1375
|
+
if (next.length <= maxChars) {
|
|
1376
|
+
current = next;
|
|
1377
|
+
} else {
|
|
1378
|
+
if (current) {
|
|
1379
|
+
lines.push(current);
|
|
1380
|
+
}
|
|
1381
|
+
current = candidate;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (current) {
|
|
1386
|
+
lines.push(current);
|
|
1387
|
+
}
|
|
1388
|
+
return lines.length > 0 ? lines : [""];
|
|
1389
|
+
});
|
|
1390
|
+
};
|
|
1391
|
+
var detectTextOverflow = (node) => {
|
|
1392
|
+
if (!node.text.trim()) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
const availableWidth = Math.max(1, node.width - (node.type === "text" ? 0 : 24));
|
|
1396
|
+
const lines = wrapText(node.text, availableWidth, node.style.fontSize, node.style.wrap);
|
|
1397
|
+
const maxLines = Math.max(1, Math.floor((node.height - 12) / lineHeight(node.style.fontSize)));
|
|
1398
|
+
return lines.length > maxLines;
|
|
1399
|
+
};
|
|
1400
|
+
var dedupeWarnings = (warnings) => {
|
|
1401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1402
|
+
return warnings.filter((item) => {
|
|
1403
|
+
const key = `${item.code}:${item.objectId ?? ""}:${item.fieldPath ?? ""}`;
|
|
1404
|
+
if (seen.has(key)) {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
seen.add(key);
|
|
1408
|
+
return true;
|
|
1409
|
+
});
|
|
1410
|
+
};
|
|
1411
|
+
var validateNodeWarnings = (document) => {
|
|
1412
|
+
const warnings = [];
|
|
1413
|
+
for (const node of document.nodes) {
|
|
1414
|
+
if (node.x < 0 || node.y < 0 || node.x + node.width > document.canvas.width || node.y + node.height > document.canvas.height) {
|
|
1415
|
+
warnings.push(
|
|
1416
|
+
warning("OBJECT_OUTSIDE_CANVAS", "Object exceeds canvas bounds", { objectId: node.id })
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
if (detectTextOverflow(node)) {
|
|
1420
|
+
warnings.push(warning("TEXT_OVERFLOW", "Text may overflow the node bounds", { objectId: node.id }));
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (let index = 0; index < document.nodes.length; index += 1) {
|
|
1424
|
+
for (let otherIndex = index + 1; otherIndex < document.nodes.length; otherIndex += 1) {
|
|
1425
|
+
const left = document.nodes[index];
|
|
1426
|
+
const right = document.nodes[otherIndex];
|
|
1427
|
+
if (!left || !right) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const overlapArea = intersectionArea(left, right);
|
|
1431
|
+
if (overlapArea === 0) {
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (overlapArea < minimumMeaningfulOverlapArea) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const smallerArea = Math.min(left.width * left.height, right.width * right.height);
|
|
1438
|
+
if (smallerArea > 0 && overlapArea / smallerArea >= nodeOverlapThreshold) {
|
|
1439
|
+
warnings.push(
|
|
1440
|
+
warning("HIGH_NODE_OVERLAP", "Nodes overlap significantly", {
|
|
1441
|
+
objectId: [left.id, right.id].sort().join(",")
|
|
1442
|
+
})
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return warnings;
|
|
1448
|
+
};
|
|
1449
|
+
var validateEdgeWarnings = (edges, nodeIds) => edges.flatMap(
|
|
1450
|
+
(edge) => edge.pathType === "orthogonal" && edge.route.points.length === 0 && nodeIds.has(edge.sourceNodeId) && nodeIds.has(edge.targetNodeId) ? [warning("ORTHOGONAL_ROUTE_MISSING", "Orthogonal route has not been calculated", { objectId: edge.id })] : []
|
|
1451
|
+
);
|
|
1452
|
+
var validateDocumentSemantics = (document) => {
|
|
1453
|
+
const errors = [];
|
|
1454
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1455
|
+
const nodeIds = new Set(document.nodes.map((node) => node.id));
|
|
1456
|
+
const allIds = [
|
|
1457
|
+
document.id,
|
|
1458
|
+
...document.nodes.map((node) => node.id),
|
|
1459
|
+
...document.edges.map((edge) => edge.id)
|
|
1460
|
+
];
|
|
1461
|
+
for (const id of allIds) {
|
|
1462
|
+
if (seenIds.has(id)) {
|
|
1463
|
+
errors.push(error("DUPLICATE_OBJECT_ID", "Object ID must be unique across the document", { objectId: id }));
|
|
1464
|
+
}
|
|
1465
|
+
seenIds.add(id);
|
|
1466
|
+
}
|
|
1467
|
+
for (const node of document.nodes) {
|
|
1468
|
+
if (node.width <= 0 || node.height <= 0) {
|
|
1469
|
+
errors.push(error("INVALID_GEOMETRY", "Node width and height must be positive", { objectId: node.id }));
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
for (const edge of document.edges) {
|
|
1473
|
+
if (!nodeIds.has(edge.sourceNodeId) || !nodeIds.has(edge.targetNodeId)) {
|
|
1474
|
+
errors.push(
|
|
1475
|
+
error("EDGE_ENDPOINT_MISSING", "Edge endpoints must reference existing nodes", { objectId: edge.id })
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
errors,
|
|
1481
|
+
warnings: dedupeWarnings([...validateNodeWarnings(document), ...validateEdgeWarnings(document.edges, nodeIds)])
|
|
1482
|
+
};
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
var statusForErrors = (errors) => {
|
|
1486
|
+
if (errors.some((item) => item.code === "AUTH_REQUIRED" || item.code === "INVALID_AUTH_TOKEN")) {
|
|
1487
|
+
return 401;
|
|
1488
|
+
}
|
|
1489
|
+
if (errors.some((item) => item.code === "REVISION_CONFLICT" || item.code === "DOCUMENT_ALREADY_EXISTS")) {
|
|
1490
|
+
return 409;
|
|
1491
|
+
}
|
|
1492
|
+
if (errors.some((item) => item.code.endsWith("_NOT_FOUND"))) {
|
|
1493
|
+
return 404;
|
|
1494
|
+
}
|
|
1495
|
+
if (errors.some((item) => item.code === "FORBIDDEN")) {
|
|
1496
|
+
return 403;
|
|
1497
|
+
}
|
|
1498
|
+
return 400;
|
|
1499
|
+
};
|
|
1500
|
+
var sendEnvelope = (reply, envelope) => envelope.success ? reply.code(200).send(envelope) : reply.code(statusForErrors(envelope.errors)).send(envelope);
|
|
1501
|
+
var failureEnvelope = (requestId, errors) => ({
|
|
1502
|
+
requestId,
|
|
1503
|
+
success: false,
|
|
1504
|
+
revision: null,
|
|
1505
|
+
errors,
|
|
1506
|
+
warnings: [],
|
|
1507
|
+
changedObjectIds: []
|
|
1508
|
+
});
|
|
1509
|
+
var parseOrReply = (reply, schema, body, requestId) => {
|
|
1510
|
+
const parsed = safeParseWithErrors(schema, body);
|
|
1511
|
+
if (parsed.success) {
|
|
1512
|
+
return parsed.data;
|
|
1513
|
+
}
|
|
1514
|
+
reply.code(statusForErrors(parsed.errors)).send(failureEnvelope(requestId, parsed.errors));
|
|
1515
|
+
return void 0;
|
|
1516
|
+
};
|
|
1517
|
+
var actorFromRequest = (request) => request.drowaiActor;
|
|
1518
|
+
var registerDocumentRoutes = (app, { documentService, auth }) => {
|
|
1519
|
+
app.addHook("preHandler", async (request, reply) => {
|
|
1520
|
+
const resolved = resolveActorFromAuthorizationHeader(request.headers.authorization, auth);
|
|
1521
|
+
if (!resolved.success) {
|
|
1522
|
+
return reply.code(statusForErrors(resolved.errors)).send(failureEnvelope(request.id, resolved.errors));
|
|
1523
|
+
}
|
|
1524
|
+
request.drowaiActor = resolved.actor;
|
|
1525
|
+
return void 0;
|
|
1526
|
+
});
|
|
1527
|
+
app.post("/documents", async (request, reply) => {
|
|
1528
|
+
const parsed = parseOrReply(reply, createDocumentRequestSchema, request.body, request.id);
|
|
1529
|
+
if (!parsed) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
return sendEnvelope(reply, documentService.createDocument(request.id, actorFromRequest(request), parsed));
|
|
1533
|
+
});
|
|
1534
|
+
app.get("/documents/:id", async (request, reply) => {
|
|
1535
|
+
const params = request.params;
|
|
1536
|
+
return sendEnvelope(reply, documentService.getDocument(request.id, actorFromRequest(request), params.id));
|
|
1537
|
+
});
|
|
1538
|
+
app.post("/documents/:id/clone", async (request, reply) => {
|
|
1539
|
+
const params = request.params;
|
|
1540
|
+
const parsed = parseOrReply(reply, cloneDocumentRequestSchema, request.body ?? {}, request.id);
|
|
1541
|
+
if (!parsed) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
return sendEnvelope(reply, documentService.cloneDocument(request.id, actorFromRequest(request), params.id, parsed.title));
|
|
1545
|
+
});
|
|
1546
|
+
app.post("/documents/:id/nodes", async (request, reply) => {
|
|
1547
|
+
const params = request.params;
|
|
1548
|
+
const parsed = parseOrReply(reply, addNodeRequestSchema, request.body, request.id);
|
|
1549
|
+
if (!parsed) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
return sendEnvelope(
|
|
1553
|
+
reply,
|
|
1554
|
+
documentService.addNode(request.id, actorFromRequest(request), params.id, parsed.baseRevision, parsed.node)
|
|
1555
|
+
);
|
|
1556
|
+
});
|
|
1557
|
+
app.patch("/documents/:id/nodes/:nodeId", async (request, reply) => {
|
|
1558
|
+
const params = request.params;
|
|
1559
|
+
const parsed = parseOrReply(reply, updateNodeRequestSchema, request.body, request.id);
|
|
1560
|
+
if (!parsed) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
return sendEnvelope(
|
|
1564
|
+
reply,
|
|
1565
|
+
documentService.updateNode(
|
|
1566
|
+
request.id,
|
|
1567
|
+
actorFromRequest(request),
|
|
1568
|
+
params.id,
|
|
1569
|
+
parsed.baseRevision,
|
|
1570
|
+
params.nodeId,
|
|
1571
|
+
parsed.changes
|
|
1572
|
+
)
|
|
1573
|
+
);
|
|
1574
|
+
});
|
|
1575
|
+
app.delete("/documents/:id/nodes/:nodeId", async (request, reply) => {
|
|
1576
|
+
const params = request.params;
|
|
1577
|
+
const parsed = parseOrReply(reply, baseRevisionSchema, request.body, request.id);
|
|
1578
|
+
if (!parsed) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
return sendEnvelope(
|
|
1582
|
+
reply,
|
|
1583
|
+
documentService.deleteNode(
|
|
1584
|
+
request.id,
|
|
1585
|
+
actorFromRequest(request),
|
|
1586
|
+
params.id,
|
|
1587
|
+
parsed.baseRevision,
|
|
1588
|
+
params.nodeId
|
|
1589
|
+
)
|
|
1590
|
+
);
|
|
1591
|
+
});
|
|
1592
|
+
app.post("/documents/:id/edges", async (request, reply) => {
|
|
1593
|
+
const params = request.params;
|
|
1594
|
+
const parsed = parseOrReply(reply, addEdgeRequestSchema, request.body, request.id);
|
|
1595
|
+
if (!parsed) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
return sendEnvelope(
|
|
1599
|
+
reply,
|
|
1600
|
+
documentService.addEdge(request.id, actorFromRequest(request), params.id, parsed.baseRevision, parsed.edge)
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
app.patch("/documents/:id/edges/:edgeId", async (request, reply) => {
|
|
1604
|
+
const params = request.params;
|
|
1605
|
+
const parsed = parseOrReply(reply, updateEdgeRequestSchema, request.body, request.id);
|
|
1606
|
+
if (!parsed) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
return sendEnvelope(
|
|
1610
|
+
reply,
|
|
1611
|
+
documentService.updateEdge(
|
|
1612
|
+
request.id,
|
|
1613
|
+
actorFromRequest(request),
|
|
1614
|
+
params.id,
|
|
1615
|
+
parsed.baseRevision,
|
|
1616
|
+
params.edgeId,
|
|
1617
|
+
parsed.changes
|
|
1618
|
+
)
|
|
1619
|
+
);
|
|
1620
|
+
});
|
|
1621
|
+
app.delete("/documents/:id/edges/:edgeId", async (request, reply) => {
|
|
1622
|
+
const params = request.params;
|
|
1623
|
+
const parsed = parseOrReply(reply, baseRevisionSchema, request.body, request.id);
|
|
1624
|
+
if (!parsed) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
return sendEnvelope(
|
|
1628
|
+
reply,
|
|
1629
|
+
documentService.deleteEdge(
|
|
1630
|
+
request.id,
|
|
1631
|
+
actorFromRequest(request),
|
|
1632
|
+
params.id,
|
|
1633
|
+
parsed.baseRevision,
|
|
1634
|
+
params.edgeId
|
|
1635
|
+
)
|
|
1636
|
+
);
|
|
1637
|
+
});
|
|
1638
|
+
app.post("/documents/:id/patch", async (request, reply) => {
|
|
1639
|
+
const params = request.params;
|
|
1640
|
+
const parsed = parseOrReply(reply, patchRequestSchema, request.body, request.id);
|
|
1641
|
+
if (!parsed) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
return sendEnvelope(
|
|
1645
|
+
reply,
|
|
1646
|
+
documentService.patchDocument(
|
|
1647
|
+
request.id,
|
|
1648
|
+
actorFromRequest(request),
|
|
1649
|
+
params.id,
|
|
1650
|
+
parsed.baseRevision,
|
|
1651
|
+
parsed.operations,
|
|
1652
|
+
parsed.atomic,
|
|
1653
|
+
parsed.dryRun
|
|
1654
|
+
)
|
|
1655
|
+
);
|
|
1656
|
+
});
|
|
1657
|
+
app.post("/documents/:id/validate", async (request, reply) => {
|
|
1658
|
+
const params = request.params;
|
|
1659
|
+
const parsed = parseOrReply(reply, validateRequestSchema, request.body ?? {}, request.id);
|
|
1660
|
+
if (!parsed) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
return sendEnvelope(
|
|
1664
|
+
reply,
|
|
1665
|
+
documentService.validateDocument(
|
|
1666
|
+
request.id,
|
|
1667
|
+
actorFromRequest(request),
|
|
1668
|
+
params.id,
|
|
1669
|
+
parsed.target,
|
|
1670
|
+
parsed.drawioMode
|
|
1671
|
+
)
|
|
1672
|
+
);
|
|
1673
|
+
});
|
|
1674
|
+
app.post("/documents/:id/render", async (request, reply) => {
|
|
1675
|
+
const params = request.params;
|
|
1676
|
+
const parsed = parseOrReply(reply, renderRequestSchema, request.body ?? {}, request.id);
|
|
1677
|
+
if (!parsed) {
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
return sendEnvelope(reply, documentService.renderDocument(request.id, actorFromRequest(request), params.id));
|
|
1681
|
+
});
|
|
1682
|
+
app.post("/documents/:id/export", async (request, reply) => {
|
|
1683
|
+
const params = request.params;
|
|
1684
|
+
const parsed = parseOrReply(reply, exportRequestSchema, request.body ?? {}, request.id);
|
|
1685
|
+
if (!parsed) {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
return sendEnvelope(
|
|
1689
|
+
reply,
|
|
1690
|
+
documentService.exportDocument(
|
|
1691
|
+
request.id,
|
|
1692
|
+
actorFromRequest(request),
|
|
1693
|
+
params.id,
|
|
1694
|
+
parsed.format,
|
|
1695
|
+
parsed.drawioMode,
|
|
1696
|
+
parsed.scale
|
|
1697
|
+
)
|
|
1698
|
+
);
|
|
1699
|
+
});
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
import { create } from "xmlbuilder2";
|
|
1703
|
+
var DEFAULT_OPTIONS = {
|
|
1704
|
+
mode: "mxfile",
|
|
1705
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1706
|
+
host: "drowai",
|
|
1707
|
+
agent: "DrowAI"
|
|
1708
|
+
};
|
|
1709
|
+
var serializeStyle = (tokens) => tokens.join(";") + ";";
|
|
1710
|
+
var nodeStyle = (node) => {
|
|
1711
|
+
const shapeTokens = node.type === "rectangle" ? ["shape=rectangle", "rounded=0", "whiteSpace=wrap", "html=1"] : node.type === "roundedRectangle" ? ["shape=rectangle", "rounded=1", "whiteSpace=wrap", "html=1"] : node.type === "ellipse" ? ["ellipse", "whiteSpace=wrap", "html=1"] : node.type === "diamond" ? ["rhombus", "whiteSpace=wrap", "html=1"] : ["text", "whiteSpace=wrap", "html=1", "strokeColor=none", "fillColor=none"];
|
|
1712
|
+
const commonTokens = node.type === "text" ? [
|
|
1713
|
+
`fontColor=${node.style.textColor}`,
|
|
1714
|
+
`fontSize=${node.style.fontSize}`,
|
|
1715
|
+
`fontStyle=${node.style.fontWeight === "bold" ? 1 : 0}`,
|
|
1716
|
+
`align=${node.style.textAlign}`,
|
|
1717
|
+
`verticalAlign=${node.style.verticalAlign}`
|
|
1718
|
+
] : [
|
|
1719
|
+
`fillColor=${node.style.fillColor}`,
|
|
1720
|
+
`strokeColor=${node.style.strokeColor}`,
|
|
1721
|
+
`strokeWidth=${node.style.strokeWidth}`,
|
|
1722
|
+
`dashed=${node.style.dashed ? 1 : 0}`,
|
|
1723
|
+
`fontColor=${node.style.textColor}`,
|
|
1724
|
+
`fontSize=${node.style.fontSize}`,
|
|
1725
|
+
`fontStyle=${node.style.fontWeight === "bold" ? 1 : 0}`,
|
|
1726
|
+
`align=${node.style.textAlign}`,
|
|
1727
|
+
`verticalAlign=${node.style.verticalAlign}`
|
|
1728
|
+
];
|
|
1729
|
+
return serializeStyle([...shapeTokens, ...commonTokens]);
|
|
1730
|
+
};
|
|
1731
|
+
var edgeStyle = (edge) => {
|
|
1732
|
+
const baseTokens = edge.pathType === "orthogonal" ? ["edgeStyle=orthogonalEdgeStyle", "rounded=0", "jettySize=auto", "html=1"] : ["edgeStyle=none", "rounded=0", "html=1"];
|
|
1733
|
+
return serializeStyle([
|
|
1734
|
+
...baseTokens,
|
|
1735
|
+
`strokeColor=${edge.style.strokeColor}`,
|
|
1736
|
+
`strokeWidth=${edge.style.strokeWidth}`,
|
|
1737
|
+
`dashed=${edge.style.dashed ? 1 : 0}`,
|
|
1738
|
+
`fontColor=${edge.style.textColor}`,
|
|
1739
|
+
`fontSize=${edge.style.fontSize}`,
|
|
1740
|
+
`fontStyle=${edge.style.fontWeight === "bold" ? 1 : 0}`,
|
|
1741
|
+
`align=${edge.style.textAlign}`,
|
|
1742
|
+
`verticalAlign=${edge.style.verticalAlign}`,
|
|
1743
|
+
`startArrow=${edge.style.arrowStart}`,
|
|
1744
|
+
`endArrow=${edge.style.arrowEnd}`
|
|
1745
|
+
]);
|
|
1746
|
+
};
|
|
1747
|
+
var appendNodeCell = (root, node) => {
|
|
1748
|
+
root.ele("mxCell", {
|
|
1749
|
+
id: node.id,
|
|
1750
|
+
value: node.text,
|
|
1751
|
+
style: nodeStyle(node),
|
|
1752
|
+
vertex: "1",
|
|
1753
|
+
parent: "1"
|
|
1754
|
+
}).ele("mxGeometry", {
|
|
1755
|
+
x: String(node.x),
|
|
1756
|
+
y: String(node.y),
|
|
1757
|
+
width: String(node.width),
|
|
1758
|
+
height: String(node.height),
|
|
1759
|
+
as: "geometry"
|
|
1760
|
+
}).up().up();
|
|
1761
|
+
};
|
|
1762
|
+
var appendEdgeCell = (root, edge) => {
|
|
1763
|
+
const cell = root.ele("mxCell", {
|
|
1764
|
+
id: edge.id,
|
|
1765
|
+
value: edge.label,
|
|
1766
|
+
style: edgeStyle(edge),
|
|
1767
|
+
edge: "1",
|
|
1768
|
+
parent: "1",
|
|
1769
|
+
source: edge.sourceNodeId,
|
|
1770
|
+
target: edge.targetNodeId
|
|
1771
|
+
});
|
|
1772
|
+
const geometry = cell.ele("mxGeometry", {
|
|
1773
|
+
relative: "1",
|
|
1774
|
+
as: "geometry"
|
|
1775
|
+
});
|
|
1776
|
+
if (edge.route.points.length > 0) {
|
|
1777
|
+
const points = geometry.ele("Array", { as: "points" });
|
|
1778
|
+
for (const point of edge.route.points) {
|
|
1779
|
+
points.ele("mxPoint", {
|
|
1780
|
+
x: String(point.x),
|
|
1781
|
+
y: String(point.y)
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
var buildGraphModel = (document) => {
|
|
1787
|
+
const graphModel = create({ version: "1.0", encoding: "UTF-8" }).ele("mxGraphModel", {
|
|
1788
|
+
dx: String(document.canvas.width),
|
|
1789
|
+
dy: String(document.canvas.height),
|
|
1790
|
+
grid: "1",
|
|
1791
|
+
gridSize: "10",
|
|
1792
|
+
page: "1",
|
|
1793
|
+
pageWidth: String(document.canvas.width),
|
|
1794
|
+
pageHeight: String(document.canvas.height)
|
|
1795
|
+
});
|
|
1796
|
+
const root = graphModel.ele("root");
|
|
1797
|
+
root.ele("mxCell", { id: "0" });
|
|
1798
|
+
root.ele("mxCell", { id: "1", parent: "0" });
|
|
1799
|
+
for (const node of document.nodes) {
|
|
1800
|
+
appendNodeCell(root, node);
|
|
1801
|
+
}
|
|
1802
|
+
for (const edge of document.edges) {
|
|
1803
|
+
appendEdgeCell(root, edge);
|
|
1804
|
+
}
|
|
1805
|
+
return graphModel;
|
|
1806
|
+
};
|
|
1807
|
+
var exportDrawioDocument = (document, options = {}) => {
|
|
1808
|
+
const resolved = {
|
|
1809
|
+
...DEFAULT_OPTIONS,
|
|
1810
|
+
...options,
|
|
1811
|
+
modifiedAt: options.modifiedAt ?? document.metadata.updatedAt
|
|
1812
|
+
};
|
|
1813
|
+
const graphModel = buildGraphModel(document);
|
|
1814
|
+
if (resolved.mode === "mxGraphModel") {
|
|
1815
|
+
return graphModel.end({ prettyPrint: false });
|
|
1816
|
+
}
|
|
1817
|
+
const mxfile = create({ version: "1.0", encoding: "UTF-8" }).ele("mxfile", {
|
|
1818
|
+
host: resolved.host,
|
|
1819
|
+
modified: resolved.modifiedAt,
|
|
1820
|
+
agent: resolved.agent
|
|
1821
|
+
});
|
|
1822
|
+
mxfile.ele("diagram", {
|
|
1823
|
+
id: document.id,
|
|
1824
|
+
name: "Page-1"
|
|
1825
|
+
}).import(graphModel);
|
|
1826
|
+
return mxfile.end({ prettyPrint: false });
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
import { createHash } from "node:crypto";
|
|
1830
|
+
|
|
1831
|
+
var xmlEscape = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1832
|
+
var lineHeight2 = (fontSize) => Math.round(fontSize * 1.2);
|
|
1833
|
+
var splitLongWord2 = (word, maxChars) => {
|
|
1834
|
+
const parts = [];
|
|
1835
|
+
for (let index = 0; index < word.length; index += maxChars) {
|
|
1836
|
+
parts.push(word.slice(index, index + maxChars));
|
|
1837
|
+
}
|
|
1838
|
+
return parts;
|
|
1839
|
+
};
|
|
1840
|
+
var wrapText2 = (text, maxWidth, fontSize, wrap) => {
|
|
1841
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / (fontSize * 0.56)));
|
|
1842
|
+
return text.split("\n").flatMap((paragraph) => {
|
|
1843
|
+
if (!wrap) {
|
|
1844
|
+
return [paragraph];
|
|
1845
|
+
}
|
|
1846
|
+
const words = paragraph.split(/\s+/).filter(Boolean);
|
|
1847
|
+
if (words.length === 0) {
|
|
1848
|
+
return [""];
|
|
1849
|
+
}
|
|
1850
|
+
const lines = [];
|
|
1851
|
+
let current = "";
|
|
1852
|
+
for (const word of words) {
|
|
1853
|
+
const candidates = word.length > maxChars ? splitLongWord2(word, maxChars) : [word];
|
|
1854
|
+
for (const candidate of candidates) {
|
|
1855
|
+
const next = current ? `${current} ${candidate}` : candidate;
|
|
1856
|
+
if (next.length <= maxChars) {
|
|
1857
|
+
current = next;
|
|
1858
|
+
} else {
|
|
1859
|
+
if (current) {
|
|
1860
|
+
lines.push(current);
|
|
1861
|
+
}
|
|
1862
|
+
current = candidate;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (current) {
|
|
1867
|
+
lines.push(current);
|
|
1868
|
+
}
|
|
1869
|
+
return lines.length > 0 ? lines : [""];
|
|
1870
|
+
});
|
|
1871
|
+
};
|
|
1872
|
+
var alignText = (x, width, align) => {
|
|
1873
|
+
if (align === "left") {
|
|
1874
|
+
return { x: x + 12, anchor: "start" };
|
|
1875
|
+
}
|
|
1876
|
+
if (align === "right") {
|
|
1877
|
+
return { x: x + width - 12, anchor: "end" };
|
|
1878
|
+
}
|
|
1879
|
+
return { x: x + Math.round(width / 2), anchor: "middle" };
|
|
1880
|
+
};
|
|
1881
|
+
var computeTextStartY = (y, height, lineCount, fontSize, verticalAlign) => {
|
|
1882
|
+
const totalHeight = lineCount * lineHeight2(fontSize);
|
|
1883
|
+
if (verticalAlign === "top") {
|
|
1884
|
+
return y + fontSize + 8;
|
|
1885
|
+
}
|
|
1886
|
+
if (verticalAlign === "bottom") {
|
|
1887
|
+
return y + height - totalHeight + fontSize - 8;
|
|
1888
|
+
}
|
|
1889
|
+
return y + Math.round((height - totalHeight) / 2) + fontSize;
|
|
1890
|
+
};
|
|
1891
|
+
var renderTextBlock = (node, warnings, isTextOnly = false) => {
|
|
1892
|
+
const lines = wrapText2(node.text, node.width - (isTextOnly ? 0 : 24), node.style.fontSize, node.style.wrap);
|
|
1893
|
+
const maxLines = Math.max(1, Math.floor((node.height - 12) / lineHeight2(node.style.fontSize)));
|
|
1894
|
+
const renderedLines = lines.slice(0, maxLines);
|
|
1895
|
+
if (lines.length > maxLines) {
|
|
1896
|
+
warnings.push(
|
|
1897
|
+
warning("TEXT_OVERFLOW", "Text rendering truncated to fit node bounds", { objectId: node.id })
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
const { x, anchor } = alignText(node.x, node.width, node.style.textAlign);
|
|
1901
|
+
const startY = computeTextStartY(
|
|
1902
|
+
node.y,
|
|
1903
|
+
node.height,
|
|
1904
|
+
renderedLines.length,
|
|
1905
|
+
node.style.fontSize,
|
|
1906
|
+
node.style.verticalAlign
|
|
1907
|
+
);
|
|
1908
|
+
const tspans = renderedLines.map(
|
|
1909
|
+
(line, index) => `<tspan x="${x}" dy="${index === 0 ? 0 : lineHeight2(node.style.fontSize)}">${xmlEscape(line)}</tspan>`
|
|
1910
|
+
).join("");
|
|
1911
|
+
return `<text fill="${node.style.textColor}" font-size="${node.style.fontSize}" font-weight="${node.style.fontWeight}" text-anchor="${anchor}" dominant-baseline="alphabetic">${tspans}</text>`;
|
|
1912
|
+
};
|
|
1913
|
+
var renderNodeShape = (node) => {
|
|
1914
|
+
const common = `fill="${node.type === "text" ? "none" : node.style.fillColor}" stroke="${node.type === "text" ? "none" : node.style.strokeColor}" stroke-width="${node.type === "text" ? 0 : node.style.strokeWidth}"${node.style.dashed ? ` stroke-dasharray="${node.style.strokeWidth * 4} ${node.style.strokeWidth * 2}"` : ""}`;
|
|
1915
|
+
switch (node.type) {
|
|
1916
|
+
case "rectangle":
|
|
1917
|
+
return `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="0" ry="0" ${common}/>`;
|
|
1918
|
+
case "roundedRectangle":
|
|
1919
|
+
return `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="12" ry="12" ${common}/>`;
|
|
1920
|
+
case "ellipse":
|
|
1921
|
+
return `<ellipse cx="${node.x + Math.round(node.width / 2)}" cy="${node.y + Math.round(node.height / 2)}" rx="${Math.round(node.width / 2)}" ry="${Math.round(node.height / 2)}" ${common}/>`;
|
|
1922
|
+
case "diamond": {
|
|
1923
|
+
const points = [
|
|
1924
|
+
`${node.x + Math.round(node.width / 2)},${node.y}`,
|
|
1925
|
+
`${node.x + node.width},${node.y + Math.round(node.height / 2)}`,
|
|
1926
|
+
`${node.x + Math.round(node.width / 2)},${node.y + node.height}`,
|
|
1927
|
+
`${node.x},${node.y + Math.round(node.height / 2)}`
|
|
1928
|
+
].join(" ");
|
|
1929
|
+
return `<polygon points="${points}" ${common}/>`;
|
|
1930
|
+
}
|
|
1931
|
+
case "text":
|
|
1932
|
+
return "";
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
var polylineMidpoint = (points) => {
|
|
1936
|
+
if (points.length === 0) {
|
|
1937
|
+
return { x: 0, y: 0 };
|
|
1938
|
+
}
|
|
1939
|
+
const lengths = [];
|
|
1940
|
+
let total = 0;
|
|
1941
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
1942
|
+
const current = points[index];
|
|
1943
|
+
const previous = points[index - 1];
|
|
1944
|
+
const length = Math.hypot(current.x - previous.x, current.y - previous.y);
|
|
1945
|
+
lengths.push(length);
|
|
1946
|
+
total += length;
|
|
1947
|
+
}
|
|
1948
|
+
const target = total / 2;
|
|
1949
|
+
let walked = 0;
|
|
1950
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
1951
|
+
const segmentLength = lengths[index - 1] ?? 0;
|
|
1952
|
+
const current = points[index];
|
|
1953
|
+
const previous = points[index - 1];
|
|
1954
|
+
if (walked + segmentLength >= target) {
|
|
1955
|
+
const ratio = segmentLength === 0 ? 0 : (target - walked) / segmentLength;
|
|
1956
|
+
return {
|
|
1957
|
+
x: Math.round(previous.x + (current.x - previous.x) * ratio),
|
|
1958
|
+
y: Math.round(previous.y + (current.y - previous.y) * ratio)
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
walked += segmentLength;
|
|
1962
|
+
}
|
|
1963
|
+
return points.at(-1) ?? points[0];
|
|
1964
|
+
};
|
|
1965
|
+
var renderEdge = (edge, document, warnings) => {
|
|
1966
|
+
const sourceNode = document.nodes.find((node) => node.id === edge.sourceNodeId);
|
|
1967
|
+
const targetNode = document.nodes.find((node) => node.id === edge.targetNodeId);
|
|
1968
|
+
if (!sourceNode || !targetNode) {
|
|
1969
|
+
return "";
|
|
1970
|
+
}
|
|
1971
|
+
const source = getAnchorPoint(sourceNode, edge.route.sourceAnchor);
|
|
1972
|
+
const target = getAnchorPoint(targetNode, edge.route.targetAnchor);
|
|
1973
|
+
const points = [source, ...edge.route.points, target];
|
|
1974
|
+
const stroke = `stroke="${edge.style.strokeColor}" stroke-width="${edge.style.strokeWidth}" fill="none"${edge.style.dashed ? ` stroke-dasharray="${edge.style.strokeWidth * 4} ${edge.style.strokeWidth * 2}"` : ""}`;
|
|
1975
|
+
const markerStart = edge.style.arrowStart !== "none" ? ` marker-start="url(#arrow-${edge.style.arrowStart})"` : "";
|
|
1976
|
+
const markerEnd = edge.style.arrowEnd !== "none" ? ` marker-end="url(#arrow-${edge.style.arrowEnd})"` : "";
|
|
1977
|
+
if (edge.pathType === "orthogonal" && edge.route.points.length === 0) {
|
|
1978
|
+
warnings.push(
|
|
1979
|
+
warning("ORTHOGONAL_ROUTE_MISSING", "Orthogonal edge route has not been calculated", { objectId: edge.id })
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
const shape = edge.pathType === "straight" ? `<line x1="${source.x}" y1="${source.y}" x2="${target.x}" y2="${target.y}" ${stroke}${markerStart}${markerEnd}/>` : `<polyline points="${points.map((point) => `${point.x},${point.y}`).join(" ")}" ${stroke}${markerStart}${markerEnd}/>`;
|
|
1983
|
+
const midpoint = polylineMidpoint(points);
|
|
1984
|
+
const label = edge.label ? `<text x="${midpoint.x}" y="${midpoint.y - 6}" fill="${edge.style.textColor}" font-size="${edge.style.fontSize}" font-weight="${edge.style.fontWeight}" text-anchor="middle">${xmlEscape(edge.label)}</text>` : "";
|
|
1985
|
+
return `<g data-object-id="${edge.id}" data-object-type="edge" data-z-index="0">${shape}${label}</g>`;
|
|
1986
|
+
};
|
|
1987
|
+
var renderNode = (node, warnings) => {
|
|
1988
|
+
const shape = renderNodeShape(node);
|
|
1989
|
+
const text = node.text ? renderTextBlock(node, warnings, node.type === "text") : "";
|
|
1990
|
+
return `<g data-object-id="${node.id}" data-object-type="${node.type}" data-z-index="${node.zIndex}">${shape}${text}</g>`;
|
|
1991
|
+
};
|
|
1992
|
+
var markers = () => `
|
|
1993
|
+
<defs>
|
|
1994
|
+
<marker id="arrow-classic" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
1995
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="context-stroke"/>
|
|
1996
|
+
</marker>
|
|
1997
|
+
<marker id="arrow-block" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
1998
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="context-stroke"/>
|
|
1999
|
+
</marker>
|
|
2000
|
+
<marker id="arrow-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
2001
|
+
<path d="M 1 1 L 9 5 L 1 9" fill="none" stroke="context-stroke" stroke-width="1.5"/>
|
|
2002
|
+
</marker>
|
|
2003
|
+
</defs>
|
|
2004
|
+
`;
|
|
2005
|
+
var renderDocumentAsSvg = (document) => {
|
|
2006
|
+
const warnings = [];
|
|
2007
|
+
const viewBox = `0 0 ${document.canvas.width} ${document.canvas.height}`;
|
|
2008
|
+
const nodeMarkup = document.nodes.map((node) => renderNode(node, warnings)).join("");
|
|
2009
|
+
const edgeMarkup = document.edges.map((edge) => renderEdge(edge, document, warnings)).join("");
|
|
2010
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="${document.canvas.width}" height="${document.canvas.height}">${markers()}<rect x="0" y="0" width="${document.canvas.width}" height="${document.canvas.height}" fill="${document.canvas.backgroundColor}"/>${edgeMarkup}${nodeMarkup}</svg>`;
|
|
2011
|
+
return {
|
|
2012
|
+
svg,
|
|
2013
|
+
viewBox,
|
|
2014
|
+
warnings
|
|
2015
|
+
};
|
|
2016
|
+
};
|
|
2017
|
+
var renderSvgDocument = renderDocumentAsSvg;
|
|
2018
|
+
|
|
2019
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
2020
|
+
var PNG_EXPORT_SCALES = [1, 2, 3];
|
|
2021
|
+
var exportDocumentToPng = (document, scale = 1) => {
|
|
2022
|
+
if (!PNG_EXPORT_SCALES.includes(scale)) {
|
|
2023
|
+
throw new RangeError(`PNG export scale must be one of ${PNG_EXPORT_SCALES.join(", ")}`);
|
|
2024
|
+
}
|
|
2025
|
+
const rendered = renderSvgDocument(document);
|
|
2026
|
+
const resvg = new Resvg(rendered.svg, {
|
|
2027
|
+
background: document.canvas.backgroundColor,
|
|
2028
|
+
fitTo: {
|
|
2029
|
+
mode: "zoom",
|
|
2030
|
+
value: scale
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
const png = resvg.render();
|
|
2034
|
+
const content = png.asPng();
|
|
2035
|
+
return {
|
|
2036
|
+
content,
|
|
2037
|
+
width: document.canvas.width * scale,
|
|
2038
|
+
height: document.canvas.height * scale,
|
|
2039
|
+
scale,
|
|
2040
|
+
svg: rendered.svg,
|
|
2041
|
+
warnings: rendered.warnings,
|
|
2042
|
+
sha256: createHash("sha256").update(content).digest("hex")
|
|
2043
|
+
};
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
var dedupeIssues = (items) => {
|
|
2047
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2048
|
+
return items.filter((item) => {
|
|
2049
|
+
const key = JSON.stringify(item);
|
|
2050
|
+
if (seen.has(key)) {
|
|
2051
|
+
return false;
|
|
2052
|
+
}
|
|
2053
|
+
seen.add(key);
|
|
2054
|
+
return true;
|
|
2055
|
+
});
|
|
2056
|
+
};
|
|
2057
|
+
var mergeWarnings = (...groups) => {
|
|
2058
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2059
|
+
return groups.flat().filter((item) => {
|
|
2060
|
+
const key = `${item.code}:${item.objectId ?? ""}:${item.fieldPath ?? ""}:${item.severity}`;
|
|
2061
|
+
if (seen.has(key)) {
|
|
2062
|
+
return false;
|
|
2063
|
+
}
|
|
2064
|
+
seen.add(key);
|
|
2065
|
+
return true;
|
|
2066
|
+
});
|
|
2067
|
+
};
|
|
2068
|
+
var validateNormalizedDocument = (document) => {
|
|
2069
|
+
const parsed = safeParseWithErrors(documentSchema, document);
|
|
2070
|
+
const semantic = validateDocumentSemantics(document);
|
|
2071
|
+
return {
|
|
2072
|
+
errors: dedupeIssues(parsed.success ? semantic.errors : [...parsed.errors, ...semantic.errors]),
|
|
2073
|
+
warnings: dedupeIssues(semantic.warnings)
|
|
2074
|
+
};
|
|
2075
|
+
};
|
|
2076
|
+
var applyActorAccess = (document, actor) => {
|
|
2077
|
+
if (document.metadata.tenantId !== actor.tenantId || document.metadata.ownerId !== actor.ownerId) {
|
|
2078
|
+
return [error("FORBIDDEN", "Document belongs to a different owner or tenant")];
|
|
2079
|
+
}
|
|
2080
|
+
return [];
|
|
2081
|
+
};
|
|
2082
|
+
var cloneDocumentWithFreshIds = (document, actor, title) => {
|
|
2083
|
+
const nodeIdMap = new Map(document.nodes.map((node) => [node.id, generateNodeId()]));
|
|
2084
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2085
|
+
return normalizeDocument({
|
|
2086
|
+
...document,
|
|
2087
|
+
id: generateDocumentId(),
|
|
2088
|
+
title: title ?? `${document.title} Copy`,
|
|
2089
|
+
revision: 1,
|
|
2090
|
+
metadata: {
|
|
2091
|
+
...document.metadata,
|
|
2092
|
+
createdBy: actor.actorId,
|
|
2093
|
+
createdAt: timestamp,
|
|
2094
|
+
updatedAt: timestamp,
|
|
2095
|
+
ownerId: actor.ownerId,
|
|
2096
|
+
tenantId: actor.tenantId
|
|
2097
|
+
},
|
|
2098
|
+
nodes: document.nodes.map((node) => ({
|
|
2099
|
+
...node,
|
|
2100
|
+
id: nodeIdMap.get(node.id) ?? generateNodeId()
|
|
2101
|
+
})),
|
|
2102
|
+
edges: document.edges.map((edge) => ({
|
|
2103
|
+
...edge,
|
|
2104
|
+
id: generateEdgeId(),
|
|
2105
|
+
sourceNodeId: nodeIdMap.get(edge.sourceNodeId) ?? edge.sourceNodeId,
|
|
2106
|
+
targetNodeId: nodeIdMap.get(edge.targetNodeId) ?? edge.targetNodeId
|
|
2107
|
+
}))
|
|
2108
|
+
});
|
|
2109
|
+
};
|
|
2110
|
+
var DocumentService = class {
|
|
2111
|
+
constructor(repository) {
|
|
2112
|
+
this.repository = repository;
|
|
2113
|
+
}
|
|
2114
|
+
repository;
|
|
2115
|
+
logOperation(params) {
|
|
2116
|
+
this.repository.logOperation({
|
|
2117
|
+
requestId: params.requestId,
|
|
2118
|
+
documentId: params.documentId,
|
|
2119
|
+
tenantId: params.actor.tenantId,
|
|
2120
|
+
actorId: params.actor.actorId,
|
|
2121
|
+
operationType: params.operationType,
|
|
2122
|
+
success: params.success,
|
|
2123
|
+
revision: params.revision,
|
|
2124
|
+
changedObjectIds: params.changedObjectIds,
|
|
2125
|
+
errors: params.errors,
|
|
2126
|
+
warnings: params.warnings
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
success(requestId, document, changedObjectIds, warnings = [], data) {
|
|
2130
|
+
return {
|
|
2131
|
+
requestId,
|
|
2132
|
+
success: true,
|
|
2133
|
+
revision: document.revision,
|
|
2134
|
+
errors: [],
|
|
2135
|
+
warnings,
|
|
2136
|
+
changedObjectIds,
|
|
2137
|
+
normalizedDocument: document,
|
|
2138
|
+
...data !== void 0 ? { data } : {}
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
failure(requestId, errors, warnings = [], document) {
|
|
2142
|
+
return {
|
|
2143
|
+
requestId,
|
|
2144
|
+
success: false,
|
|
2145
|
+
revision: document?.revision ?? null,
|
|
2146
|
+
errors,
|
|
2147
|
+
warnings,
|
|
2148
|
+
changedObjectIds: [],
|
|
2149
|
+
...document ? { normalizedDocument: document } : {}
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
loadDocument(documentId, actor) {
|
|
2153
|
+
const document = this.repository.getDocument(documentId);
|
|
2154
|
+
if (!document) {
|
|
2155
|
+
return { errors: [error("DOCUMENT_NOT_FOUND", "Document not found", { objectId: documentId })] };
|
|
2156
|
+
}
|
|
2157
|
+
const accessErrors = applyActorAccess(document, actor);
|
|
2158
|
+
if (accessErrors.length > 0) {
|
|
2159
|
+
return { errors: accessErrors };
|
|
2160
|
+
}
|
|
2161
|
+
return { document, errors: [] };
|
|
2162
|
+
}
|
|
2163
|
+
ensureRevision(document, baseRevision) {
|
|
2164
|
+
return document.revision === baseRevision ? [] : [
|
|
2165
|
+
error("REVISION_CONFLICT", "Document revision does not match baseRevision", {
|
|
2166
|
+
objectId: document.id
|
|
2167
|
+
})
|
|
2168
|
+
];
|
|
2169
|
+
}
|
|
2170
|
+
persistDocument(requestId, actor, operationType, document, changedObjectIds, warnings, isCreate = false) {
|
|
2171
|
+
this.repository.transaction(() => {
|
|
2172
|
+
if (isCreate) {
|
|
2173
|
+
this.repository.insertDocument(document);
|
|
2174
|
+
} else {
|
|
2175
|
+
this.repository.updateDocument(document);
|
|
2176
|
+
}
|
|
2177
|
+
this.logOperation({
|
|
2178
|
+
requestId,
|
|
2179
|
+
actor,
|
|
2180
|
+
operationType,
|
|
2181
|
+
documentId: document.id,
|
|
2182
|
+
success: true,
|
|
2183
|
+
revision: document.revision,
|
|
2184
|
+
changedObjectIds,
|
|
2185
|
+
errors: [],
|
|
2186
|
+
warnings
|
|
2187
|
+
});
|
|
2188
|
+
});
|
|
2189
|
+
return this.success(requestId, document, changedObjectIds, warnings);
|
|
2190
|
+
}
|
|
2191
|
+
createDocument(requestId, actor, input) {
|
|
2192
|
+
const document = createDocument(input, actor);
|
|
2193
|
+
const validation = validateNormalizedDocument(document);
|
|
2194
|
+
if (validation.errors.length > 0) {
|
|
2195
|
+
return this.failure(requestId, validation.errors, validation.warnings, document);
|
|
2196
|
+
}
|
|
2197
|
+
try {
|
|
2198
|
+
return this.persistDocument(requestId, actor, "createDocument", document, [document.id], validation.warnings, true);
|
|
2199
|
+
} catch (cause) {
|
|
2200
|
+
if (cause instanceof DocumentAlreadyExistsError) {
|
|
2201
|
+
const errors = [error("DOCUMENT_ALREADY_EXISTS", "Document already exists", { objectId: document.id })];
|
|
2202
|
+
this.logOperation({
|
|
2203
|
+
requestId,
|
|
2204
|
+
actor,
|
|
2205
|
+
operationType: "createDocument",
|
|
2206
|
+
documentId: document.id,
|
|
2207
|
+
success: false,
|
|
2208
|
+
revision: null,
|
|
2209
|
+
changedObjectIds: [],
|
|
2210
|
+
errors,
|
|
2211
|
+
warnings: []
|
|
2212
|
+
});
|
|
2213
|
+
return this.failure(requestId, errors);
|
|
2214
|
+
}
|
|
2215
|
+
throw cause;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
getDocument(requestId, actor, documentId) {
|
|
2219
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2220
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2221
|
+
return this.failure(requestId, loaded.errors);
|
|
2222
|
+
}
|
|
2223
|
+
return this.success(requestId, loaded.document, []);
|
|
2224
|
+
}
|
|
2225
|
+
cloneDocument(requestId, actor, documentId, title) {
|
|
2226
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2227
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2228
|
+
return this.failure(requestId, loaded.errors);
|
|
2229
|
+
}
|
|
2230
|
+
const cloned = cloneDocumentWithFreshIds(loaded.document, actor, title);
|
|
2231
|
+
return this.persistDocument(requestId, actor, "cloneDocument", cloned, [cloned.id], [], true);
|
|
2232
|
+
}
|
|
2233
|
+
updateWithMutation(requestId, actor, documentId, baseRevision, operationType, mutate) {
|
|
2234
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2235
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2236
|
+
return this.failure(requestId, loaded.errors);
|
|
2237
|
+
}
|
|
2238
|
+
const revisionErrors = this.ensureRevision(loaded.document, baseRevision);
|
|
2239
|
+
if (revisionErrors.length > 0) {
|
|
2240
|
+
return this.failure(requestId, revisionErrors, [], loaded.document);
|
|
2241
|
+
}
|
|
2242
|
+
const result = mutate(loaded.document);
|
|
2243
|
+
if (result.errors && result.errors.length > 0) {
|
|
2244
|
+
return this.failure(requestId, result.errors, [], loaded.document);
|
|
2245
|
+
}
|
|
2246
|
+
const nextDocument = withRevision(result.document, loaded.document.revision + 1);
|
|
2247
|
+
const validation = validateNormalizedDocument(nextDocument);
|
|
2248
|
+
if (validation.errors.length > 0) {
|
|
2249
|
+
this.logOperation({
|
|
2250
|
+
requestId,
|
|
2251
|
+
actor,
|
|
2252
|
+
operationType,
|
|
2253
|
+
documentId,
|
|
2254
|
+
success: false,
|
|
2255
|
+
revision: loaded.document.revision,
|
|
2256
|
+
changedObjectIds: result.changedObjectIds,
|
|
2257
|
+
errors: validation.errors,
|
|
2258
|
+
warnings: validation.warnings
|
|
2259
|
+
});
|
|
2260
|
+
return this.failure(requestId, validation.errors, validation.warnings, loaded.document);
|
|
2261
|
+
}
|
|
2262
|
+
return this.persistDocument(
|
|
2263
|
+
requestId,
|
|
2264
|
+
actor,
|
|
2265
|
+
operationType,
|
|
2266
|
+
nextDocument,
|
|
2267
|
+
result.changedObjectIds,
|
|
2268
|
+
validation.warnings
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
addNode(requestId, actor, documentId, baseRevision, node) {
|
|
2272
|
+
return this.updateWithMutation(
|
|
2273
|
+
requestId,
|
|
2274
|
+
actor,
|
|
2275
|
+
documentId,
|
|
2276
|
+
baseRevision,
|
|
2277
|
+
"addNode",
|
|
2278
|
+
(document) => addNode(document, node)
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
updateNode(requestId, actor, documentId, baseRevision, nodeId, changes) {
|
|
2282
|
+
return this.updateWithMutation(requestId, actor, documentId, baseRevision, "updateNode", (document) => {
|
|
2283
|
+
if (!document.nodes.some((node) => node.id === nodeId)) {
|
|
2284
|
+
return {
|
|
2285
|
+
document,
|
|
2286
|
+
changedObjectIds: [],
|
|
2287
|
+
errors: [error("NODE_NOT_FOUND", "Node not found", { objectId: nodeId })]
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
return updateNode(document, nodeId, changes);
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
deleteNode(requestId, actor, documentId, baseRevision, nodeId) {
|
|
2294
|
+
return this.updateWithMutation(requestId, actor, documentId, baseRevision, "deleteNode", (document) => {
|
|
2295
|
+
if (!document.nodes.some((node) => node.id === nodeId)) {
|
|
2296
|
+
return {
|
|
2297
|
+
document,
|
|
2298
|
+
changedObjectIds: [],
|
|
2299
|
+
errors: [error("NODE_NOT_FOUND", "Node not found", { objectId: nodeId })]
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
return deleteNode(document, nodeId);
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
addEdge(requestId, actor, documentId, baseRevision, edge) {
|
|
2306
|
+
return this.updateWithMutation(
|
|
2307
|
+
requestId,
|
|
2308
|
+
actor,
|
|
2309
|
+
documentId,
|
|
2310
|
+
baseRevision,
|
|
2311
|
+
"addEdge",
|
|
2312
|
+
(document) => addEdge(document, edge)
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
updateEdge(requestId, actor, documentId, baseRevision, edgeId, changes) {
|
|
2316
|
+
return this.updateWithMutation(requestId, actor, documentId, baseRevision, "updateEdge", (document) => {
|
|
2317
|
+
if (!document.edges.some((edge) => edge.id === edgeId)) {
|
|
2318
|
+
return {
|
|
2319
|
+
document,
|
|
2320
|
+
changedObjectIds: [],
|
|
2321
|
+
errors: [error("EDGE_NOT_FOUND", "Edge not found", { objectId: edgeId })]
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
return updateEdge(document, edgeId, changes);
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
deleteEdge(requestId, actor, documentId, baseRevision, edgeId) {
|
|
2328
|
+
return this.updateWithMutation(requestId, actor, documentId, baseRevision, "deleteEdge", (document) => {
|
|
2329
|
+
if (!document.edges.some((edge) => edge.id === edgeId)) {
|
|
2330
|
+
return {
|
|
2331
|
+
document,
|
|
2332
|
+
changedObjectIds: [],
|
|
2333
|
+
errors: [error("EDGE_NOT_FOUND", "Edge not found", { objectId: edgeId })]
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
return deleteEdge(document, edgeId);
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
patchDocument(requestId, actor, documentId, baseRevision, operations, atomic = true, dryRun = false) {
|
|
2340
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2341
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2342
|
+
return this.failure(requestId, loaded.errors);
|
|
2343
|
+
}
|
|
2344
|
+
const revisionErrors = this.ensureRevision(loaded.document, baseRevision);
|
|
2345
|
+
if (revisionErrors.length > 0) {
|
|
2346
|
+
return this.failure(requestId, revisionErrors, [], loaded.document);
|
|
2347
|
+
}
|
|
2348
|
+
const preview = applyPatchOperations(loaded.document, operations, loaded.document.revision + 1);
|
|
2349
|
+
const previewDocument = withRevision(preview.document, loaded.document.revision + 1);
|
|
2350
|
+
const previewValidation = validateNormalizedDocument(previewDocument);
|
|
2351
|
+
if (atomic && previewValidation.errors.length > 0) {
|
|
2352
|
+
this.logOperation({
|
|
2353
|
+
requestId,
|
|
2354
|
+
actor,
|
|
2355
|
+
operationType: "patchDocument",
|
|
2356
|
+
documentId,
|
|
2357
|
+
success: false,
|
|
2358
|
+
revision: loaded.document.revision,
|
|
2359
|
+
changedObjectIds: preview.changedObjectIds,
|
|
2360
|
+
errors: previewValidation.errors,
|
|
2361
|
+
warnings: previewValidation.warnings
|
|
2362
|
+
});
|
|
2363
|
+
return this.failure(requestId, previewValidation.errors, previewValidation.warnings, loaded.document);
|
|
2364
|
+
}
|
|
2365
|
+
if (!atomic) {
|
|
2366
|
+
let current = loaded.document;
|
|
2367
|
+
const changedObjectIds = /* @__PURE__ */ new Set();
|
|
2368
|
+
const warnings = [];
|
|
2369
|
+
const errors = [];
|
|
2370
|
+
for (const operation of operations) {
|
|
2371
|
+
const candidate = applyPatchOperations(current, [operation], loaded.document.revision + 1);
|
|
2372
|
+
const candidateDocument = withRevision(candidate.document, loaded.document.revision + 1);
|
|
2373
|
+
const candidateValidation = validateNormalizedDocument(candidateDocument);
|
|
2374
|
+
if (candidateValidation.errors.length > 0) {
|
|
2375
|
+
errors.push(...candidateValidation.errors);
|
|
2376
|
+
warnings.push(...candidateValidation.warnings);
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
current = candidateDocument;
|
|
2380
|
+
for (const objectId of candidate.changedObjectIds) {
|
|
2381
|
+
changedObjectIds.add(objectId);
|
|
2382
|
+
}
|
|
2383
|
+
warnings.push(...candidateValidation.warnings);
|
|
2384
|
+
}
|
|
2385
|
+
if (errors.length > 0 && changedObjectIds.size === 0) {
|
|
2386
|
+
return this.failure(requestId, dedupeIssues(errors), dedupeIssues(warnings), loaded.document);
|
|
2387
|
+
}
|
|
2388
|
+
if (dryRun) {
|
|
2389
|
+
return this.success(requestId, current, [...changedObjectIds], dedupeIssues(warnings));
|
|
2390
|
+
}
|
|
2391
|
+
return this.persistDocument(
|
|
2392
|
+
requestId,
|
|
2393
|
+
actor,
|
|
2394
|
+
"patchDocument",
|
|
2395
|
+
current,
|
|
2396
|
+
[...changedObjectIds],
|
|
2397
|
+
dedupeIssues(warnings)
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
if (dryRun) {
|
|
2401
|
+
return this.success(
|
|
2402
|
+
requestId,
|
|
2403
|
+
previewDocument,
|
|
2404
|
+
preview.changedObjectIds,
|
|
2405
|
+
previewValidation.warnings
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
return this.persistDocument(
|
|
2409
|
+
requestId,
|
|
2410
|
+
actor,
|
|
2411
|
+
"patchDocument",
|
|
2412
|
+
previewDocument,
|
|
2413
|
+
preview.changedObjectIds,
|
|
2414
|
+
previewValidation.warnings
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
validateDocument(requestId, actor, documentId, target, drawioMode) {
|
|
2418
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2419
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2420
|
+
return this.failure(requestId, loaded.errors);
|
|
2421
|
+
}
|
|
2422
|
+
if (target === "document") {
|
|
2423
|
+
const validation2 = validateNormalizedDocument(loaded.document);
|
|
2424
|
+
return {
|
|
2425
|
+
...this.success(requestId, loaded.document, [], validation2.warnings, {
|
|
2426
|
+
target,
|
|
2427
|
+
valid: validation2.errors.length === 0
|
|
2428
|
+
}),
|
|
2429
|
+
success: validation2.errors.length === 0,
|
|
2430
|
+
errors: validation2.errors
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
const xml = exportDrawioDocument(loaded.document, { mode: drawioMode });
|
|
2434
|
+
const validation = validateDrawioXml(xml);
|
|
2435
|
+
return {
|
|
2436
|
+
...this.success(requestId, loaded.document, [], validation.warnings, {
|
|
2437
|
+
target,
|
|
2438
|
+
valid: validation.errors.length === 0
|
|
2439
|
+
}),
|
|
2440
|
+
success: validation.errors.length === 0,
|
|
2441
|
+
errors: validation.errors
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
renderDocument(requestId, actor, documentId) {
|
|
2445
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2446
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2447
|
+
return this.failure(requestId, loaded.errors);
|
|
2448
|
+
}
|
|
2449
|
+
const rendered = renderSvgDocument(loaded.document);
|
|
2450
|
+
const validation = validateNormalizedDocument(loaded.document);
|
|
2451
|
+
return this.success(requestId, loaded.document, [], mergeWarnings(validation.warnings, rendered.warnings), {
|
|
2452
|
+
svg: rendered.svg,
|
|
2453
|
+
viewBox: rendered.viewBox
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
exportDocument(requestId, actor, documentId, format, drawioMode, scale = 1) {
|
|
2457
|
+
const loaded = this.loadDocument(documentId, actor);
|
|
2458
|
+
if (loaded.errors.length > 0 || !loaded.document) {
|
|
2459
|
+
return this.failure(requestId, loaded.errors);
|
|
2460
|
+
}
|
|
2461
|
+
if (format === "svg") {
|
|
2462
|
+
const rendered = renderSvgDocument(loaded.document);
|
|
2463
|
+
const validation2 = validateNormalizedDocument(loaded.document);
|
|
2464
|
+
return this.success(requestId, loaded.document, [], mergeWarnings(validation2.warnings, rendered.warnings), {
|
|
2465
|
+
format,
|
|
2466
|
+
contentType: "image/svg+xml",
|
|
2467
|
+
content: rendered.svg
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
if (format === "png") {
|
|
2471
|
+
const rendered = exportDocumentToPng(loaded.document, scale);
|
|
2472
|
+
const validation2 = validateNormalizedDocument(loaded.document);
|
|
2473
|
+
return this.success(requestId, loaded.document, [], mergeWarnings(validation2.warnings, rendered.warnings), {
|
|
2474
|
+
format,
|
|
2475
|
+
contentType: "image/png",
|
|
2476
|
+
content: Buffer.from(rendered.content).toString("base64"),
|
|
2477
|
+
encoding: "base64"
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
const xml = exportDrawioDocument(loaded.document, { mode: drawioMode });
|
|
2481
|
+
const validation = validateDrawioXml(xml);
|
|
2482
|
+
if (validation.errors.length > 0) {
|
|
2483
|
+
return this.failure(requestId, validation.errors, validation.warnings, loaded.document);
|
|
2484
|
+
}
|
|
2485
|
+
return this.success(requestId, loaded.document, [], validation.warnings, {
|
|
2486
|
+
format,
|
|
2487
|
+
contentType: "application/xml",
|
|
2488
|
+
content: xml
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
|
|
2493
|
+
var createApp = async (options = {}) => {
|
|
2494
|
+
const app = Fastify({ logger: false });
|
|
2495
|
+
const repository = new SqliteDocumentRepository(options.dbPath ?? process.env.DROWAI_DB_PATH ?? ":memory:");
|
|
2496
|
+
const documentService = new DocumentService(repository);
|
|
2497
|
+
const auth = resolveAuthOptions(options.auth);
|
|
2498
|
+
registerDocumentRoutes(app, { documentService, auth });
|
|
2499
|
+
app.addHook("onClose", async () => {
|
|
2500
|
+
repository.close();
|
|
2501
|
+
});
|
|
2502
|
+
return app;
|
|
2503
|
+
};
|
|
2504
|
+
|
|
2505
|
+
import { z as z2 } from "zod";
|
|
2506
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
2507
|
+
"2025-11-25",
|
|
2508
|
+
"2025-06-18",
|
|
2509
|
+
"2025-03-26",
|
|
2510
|
+
"2024-11-05"
|
|
2511
|
+
];
|
|
2512
|
+
var DROWAI_MCP_SERVER_NAME = "drowai-orchestrator";
|
|
2513
|
+
var DROWAI_MCP_SERVER_TITLE = "DrowAI Orchestrator";
|
|
2514
|
+
var DROWAI_MCP_SERVER_VERSION = "0.1.0";
|
|
2515
|
+
var SERVER_INSTRUCTIONS = "Use these tools as a thin adapter over DrowAI's canonical HTTP JSON API. Mutations require baseRevision. The normalizedDocument in each result remains the source of truth.";
|
|
2516
|
+
var MCP_LOCAL_ACTOR = {
|
|
2517
|
+
actorId: "mcp-local",
|
|
2518
|
+
ownerId: "mcp-local",
|
|
2519
|
+
tenantId: "local"
|
|
2520
|
+
};
|
|
2521
|
+
var documentIdToolSchema = z2.object({
|
|
2522
|
+
documentId: z2.string().min(1).describe("Target document id, for example doc_examplecase")
|
|
2523
|
+
});
|
|
2524
|
+
var nodeIdToolSchema = z2.object({
|
|
2525
|
+
nodeId: z2.string().min(1).describe("Target node id")
|
|
2526
|
+
});
|
|
2527
|
+
var edgeIdToolSchema = z2.object({
|
|
2528
|
+
edgeId: z2.string().min(1).describe("Target edge id")
|
|
2529
|
+
});
|
|
2530
|
+
var getDocumentToolSchema = documentIdToolSchema.strict();
|
|
2531
|
+
var addNodeToolSchema = addNodeRequestSchema.extend(documentIdToolSchema.shape).strict();
|
|
2532
|
+
var updateNodeToolSchema = updateNodeRequestSchema.extend({
|
|
2533
|
+
...documentIdToolSchema.shape,
|
|
2534
|
+
...nodeIdToolSchema.shape
|
|
2535
|
+
}).strict();
|
|
2536
|
+
var deleteNodeToolSchema = baseRevisionSchema.extend({
|
|
2537
|
+
...documentIdToolSchema.shape,
|
|
2538
|
+
...nodeIdToolSchema.shape
|
|
2539
|
+
}).strict();
|
|
2540
|
+
var addEdgeToolSchema = addEdgeRequestSchema.extend(documentIdToolSchema.shape).strict();
|
|
2541
|
+
var updateEdgeToolSchema = updateEdgeRequestSchema.extend({
|
|
2542
|
+
...documentIdToolSchema.shape,
|
|
2543
|
+
...edgeIdToolSchema.shape
|
|
2544
|
+
}).strict();
|
|
2545
|
+
var deleteEdgeToolSchema = baseRevisionSchema.extend({
|
|
2546
|
+
...documentIdToolSchema.shape,
|
|
2547
|
+
...edgeIdToolSchema.shape
|
|
2548
|
+
}).strict();
|
|
2549
|
+
var patchDocumentToolSchema = patchRequestSchema.extend(documentIdToolSchema.shape).strict();
|
|
2550
|
+
var validateDocumentToolSchema = validateRequestSchema.extend(documentIdToolSchema.shape).strict();
|
|
2551
|
+
var renderDocumentToolSchema = renderRequestSchema.extend(documentIdToolSchema.shape).strict();
|
|
2552
|
+
var exportDocumentToolSchema = exportRequestSchema.extend(documentIdToolSchema.shape).strict();
|
|
2553
|
+
var ApiBridge = class {
|
|
2554
|
+
constructor(request) {
|
|
2555
|
+
this.request = request;
|
|
2556
|
+
}
|
|
2557
|
+
request;
|
|
2558
|
+
encode(segment) {
|
|
2559
|
+
return encodeURIComponent(segment);
|
|
2560
|
+
}
|
|
2561
|
+
createDocument(payload) {
|
|
2562
|
+
return this.request("POST", "/documents", payload);
|
|
2563
|
+
}
|
|
2564
|
+
getDocument(documentId) {
|
|
2565
|
+
return this.request("GET", `/documents/${this.encode(documentId)}`);
|
|
2566
|
+
}
|
|
2567
|
+
addNode(documentId, payload) {
|
|
2568
|
+
return this.request("POST", `/documents/${this.encode(documentId)}/nodes`, payload);
|
|
2569
|
+
}
|
|
2570
|
+
updateNode(documentId, nodeId, payload) {
|
|
2571
|
+
return this.request("PATCH", `/documents/${this.encode(documentId)}/nodes/${this.encode(nodeId)}`, payload);
|
|
2572
|
+
}
|
|
2573
|
+
deleteNode(documentId, nodeId, payload) {
|
|
2574
|
+
return this.request("DELETE", `/documents/${this.encode(documentId)}/nodes/${this.encode(nodeId)}`, payload);
|
|
2575
|
+
}
|
|
2576
|
+
addEdge(documentId, payload) {
|
|
2577
|
+
return this.request("POST", `/documents/${this.encode(documentId)}/edges`, payload);
|
|
2578
|
+
}
|
|
2579
|
+
updateEdge(documentId, edgeId, payload) {
|
|
2580
|
+
return this.request("PATCH", `/documents/${this.encode(documentId)}/edges/${this.encode(edgeId)}`, payload);
|
|
2581
|
+
}
|
|
2582
|
+
deleteEdge(documentId, edgeId, payload) {
|
|
2583
|
+
return this.request("DELETE", `/documents/${this.encode(documentId)}/edges/${this.encode(edgeId)}`, payload);
|
|
2584
|
+
}
|
|
2585
|
+
patchDocument(documentId, payload) {
|
|
2586
|
+
return this.request("POST", `/documents/${this.encode(documentId)}/patch`, payload);
|
|
2587
|
+
}
|
|
2588
|
+
validateDocument(documentId, payload) {
|
|
2589
|
+
return this.request("POST", `/documents/${this.encode(documentId)}/validate`, payload);
|
|
2590
|
+
}
|
|
2591
|
+
renderDocument(documentId, payload) {
|
|
2592
|
+
return this.request("POST", `/documents/${this.encode(documentId)}/render`, payload);
|
|
2593
|
+
}
|
|
2594
|
+
exportDocument(documentId, payload) {
|
|
2595
|
+
return this.request("POST", `/documents/${this.encode(documentId)}/export`, payload);
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
var createToolContracts = () => [
|
|
2599
|
+
{
|
|
2600
|
+
name: "create_document",
|
|
2601
|
+
title: "Create document",
|
|
2602
|
+
description: "Create a new DrowAI document and return the normalized JSON envelope.",
|
|
2603
|
+
schema: createDocumentRequestSchema,
|
|
2604
|
+
execute: (args, api) => api.createDocument(args)
|
|
2605
|
+
},
|
|
2606
|
+
{
|
|
2607
|
+
name: "get_document",
|
|
2608
|
+
title: "Get document",
|
|
2609
|
+
description: "Fetch a normalized DrowAI document by id.",
|
|
2610
|
+
schema: getDocumentToolSchema,
|
|
2611
|
+
execute: (args, api) => api.getDocument(args.documentId)
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
name: "add_node",
|
|
2615
|
+
title: "Add node",
|
|
2616
|
+
description: "Append one node to an existing document.",
|
|
2617
|
+
schema: addNodeToolSchema,
|
|
2618
|
+
execute: (args, api) => {
|
|
2619
|
+
const { documentId, ...payload } = args;
|
|
2620
|
+
return api.addNode(documentId, payload);
|
|
2621
|
+
}
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
name: "update_node",
|
|
2625
|
+
title: "Update node",
|
|
2626
|
+
description: "Apply a partial update to one node.",
|
|
2627
|
+
schema: updateNodeToolSchema,
|
|
2628
|
+
execute: (args, api) => {
|
|
2629
|
+
const { documentId, nodeId, ...payload } = args;
|
|
2630
|
+
return api.updateNode(documentId, nodeId, payload);
|
|
2631
|
+
}
|
|
2632
|
+
},
|
|
2633
|
+
{
|
|
2634
|
+
name: "delete_node",
|
|
2635
|
+
title: "Delete node",
|
|
2636
|
+
description: "Delete one node and any connected edges.",
|
|
2637
|
+
schema: deleteNodeToolSchema,
|
|
2638
|
+
execute: (args, api) => {
|
|
2639
|
+
const { documentId, nodeId, ...payload } = args;
|
|
2640
|
+
return api.deleteNode(documentId, nodeId, payload);
|
|
2641
|
+
}
|
|
2642
|
+
},
|
|
2643
|
+
{
|
|
2644
|
+
name: "add_edge",
|
|
2645
|
+
title: "Add edge",
|
|
2646
|
+
description: "Append one edge to an existing document.",
|
|
2647
|
+
schema: addEdgeToolSchema,
|
|
2648
|
+
execute: (args, api) => {
|
|
2649
|
+
const { documentId, ...payload } = args;
|
|
2650
|
+
return api.addEdge(documentId, payload);
|
|
2651
|
+
}
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
name: "update_edge",
|
|
2655
|
+
title: "Update edge",
|
|
2656
|
+
description: "Apply a partial update to one edge.",
|
|
2657
|
+
schema: updateEdgeToolSchema,
|
|
2658
|
+
execute: (args, api) => {
|
|
2659
|
+
const { documentId, edgeId, ...payload } = args;
|
|
2660
|
+
return api.updateEdge(documentId, edgeId, payload);
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
name: "delete_edge",
|
|
2665
|
+
title: "Delete edge",
|
|
2666
|
+
description: "Delete one edge from a document.",
|
|
2667
|
+
schema: deleteEdgeToolSchema,
|
|
2668
|
+
execute: (args, api) => {
|
|
2669
|
+
const { documentId, edgeId, ...payload } = args;
|
|
2670
|
+
return api.deleteEdge(documentId, edgeId, payload);
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
name: "patch_document",
|
|
2675
|
+
title: "Patch document",
|
|
2676
|
+
description: "Run ordered patch operations, including orthogonal route recalculation.",
|
|
2677
|
+
schema: patchDocumentToolSchema,
|
|
2678
|
+
execute: (args, api) => {
|
|
2679
|
+
const { documentId, ...payload } = args;
|
|
2680
|
+
return api.patchDocument(documentId, payload);
|
|
2681
|
+
}
|
|
2682
|
+
},
|
|
2683
|
+
{
|
|
2684
|
+
name: "validate_document",
|
|
2685
|
+
title: "Validate document",
|
|
2686
|
+
description: "Validate a normalized document or its draw.io export.",
|
|
2687
|
+
schema: validateDocumentToolSchema,
|
|
2688
|
+
execute: (args, api) => {
|
|
2689
|
+
const { documentId, ...payload } = args;
|
|
2690
|
+
return api.validateDocument(documentId, payload);
|
|
2691
|
+
}
|
|
2692
|
+
},
|
|
2693
|
+
{
|
|
2694
|
+
name: "render_document",
|
|
2695
|
+
title: "Render document",
|
|
2696
|
+
description: "Render the canonical document JSON into SVG.",
|
|
2697
|
+
schema: renderDocumentToolSchema,
|
|
2698
|
+
execute: (args, api) => {
|
|
2699
|
+
const { documentId, ...payload } = args;
|
|
2700
|
+
return api.renderDocument(documentId, payload);
|
|
2701
|
+
}
|
|
2702
|
+
},
|
|
2703
|
+
{
|
|
2704
|
+
name: "export_document",
|
|
2705
|
+
title: "Export document",
|
|
2706
|
+
description: "Export the canonical document JSON to draw.io XML, SVG, or PNG.",
|
|
2707
|
+
schema: exportDocumentToolSchema,
|
|
2708
|
+
execute: (args, api) => {
|
|
2709
|
+
const { documentId, ...payload } = args;
|
|
2710
|
+
return api.exportDocument(documentId, payload);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
];
|
|
2714
|
+
var jsonRpcSuccess = (id, result) => ({
|
|
2715
|
+
jsonrpc: "2.0",
|
|
2716
|
+
id,
|
|
2717
|
+
result
|
|
2718
|
+
});
|
|
2719
|
+
var jsonRpcError = (id, code, message, data) => ({
|
|
2720
|
+
jsonrpc: "2.0",
|
|
2721
|
+
id,
|
|
2722
|
+
error: {
|
|
2723
|
+
code,
|
|
2724
|
+
message,
|
|
2725
|
+
...data !== void 0 ? { data } : {}
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
var toolInputSchema = (schema) => z2.toJSONSchema(schema);
|
|
2729
|
+
var toolResult = (payload, isError = false) => ({
|
|
2730
|
+
content: [
|
|
2731
|
+
{
|
|
2732
|
+
type: "text",
|
|
2733
|
+
text: JSON.stringify(payload, null, 2)
|
|
2734
|
+
}
|
|
2735
|
+
],
|
|
2736
|
+
structuredContent: payload,
|
|
2737
|
+
isError
|
|
2738
|
+
});
|
|
2739
|
+
var toolArgumentError = (errors) => toolResult(
|
|
2740
|
+
{
|
|
2741
|
+
success: false,
|
|
2742
|
+
revision: null,
|
|
2743
|
+
errors,
|
|
2744
|
+
warnings: [],
|
|
2745
|
+
changedObjectIds: []
|
|
2746
|
+
},
|
|
2747
|
+
true
|
|
2748
|
+
);
|
|
2749
|
+
var selectProtocolVersion = (requested) => requested && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
2750
|
+
var DrowaiMcpServer = class _DrowaiMcpServer {
|
|
2751
|
+
constructor(app, authToken) {
|
|
2752
|
+
this.app = app;
|
|
2753
|
+
this.bridge = new ApiBridge(async (method, path, payload) => {
|
|
2754
|
+
const injectOptions = {
|
|
2755
|
+
method,
|
|
2756
|
+
url: path,
|
|
2757
|
+
headers: {
|
|
2758
|
+
authorization: `Bearer ${authToken}`
|
|
2759
|
+
},
|
|
2760
|
+
...payload !== void 0 ? { payload } : {}
|
|
2761
|
+
};
|
|
2762
|
+
const response = await this.app.inject(injectOptions);
|
|
2763
|
+
return JSON.parse(String(response.body));
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
app;
|
|
2767
|
+
tools = createToolContracts();
|
|
2768
|
+
toolMap = new Map(this.tools.map((tool) => [tool.name, tool]));
|
|
2769
|
+
bridge;
|
|
2770
|
+
negotiatedProtocolVersion = null;
|
|
2771
|
+
static async create(options = {}) {
|
|
2772
|
+
const authToken = options.authToken ?? randomUUID();
|
|
2773
|
+
const app = await createApp({
|
|
2774
|
+
...options.dbPath ? { dbPath: options.dbPath } : {},
|
|
2775
|
+
auth: {
|
|
2776
|
+
tokenRegistry: {
|
|
2777
|
+
[authToken]: MCP_LOCAL_ACTOR
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
return new _DrowaiMcpServer(app, authToken);
|
|
2782
|
+
}
|
|
2783
|
+
async close() {
|
|
2784
|
+
await this.app.close();
|
|
2785
|
+
}
|
|
2786
|
+
async handleMessage(message) {
|
|
2787
|
+
if (Array.isArray(message)) {
|
|
2788
|
+
const responses = [];
|
|
2789
|
+
for (const item of message) {
|
|
2790
|
+
const response = await this.handleRequest(item);
|
|
2791
|
+
if (response) {
|
|
2792
|
+
responses.push(response);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return responses.length > 0 ? responses : void 0;
|
|
2796
|
+
}
|
|
2797
|
+
return this.handleRequest(message);
|
|
2798
|
+
}
|
|
2799
|
+
async handleRequest(message) {
|
|
2800
|
+
const id = "id" in message ? message.id ?? null : null;
|
|
2801
|
+
if (message.jsonrpc !== "2.0") {
|
|
2802
|
+
return jsonRpcError(id, -32600, "Invalid JSON-RPC version");
|
|
2803
|
+
}
|
|
2804
|
+
if (typeof message.method !== "string") {
|
|
2805
|
+
return "id" in message ? jsonRpcError(id, -32600, "Invalid JSON-RPC request") : void 0;
|
|
2806
|
+
}
|
|
2807
|
+
switch (message.method) {
|
|
2808
|
+
case "initialize":
|
|
2809
|
+
return this.initialize(id, message.params);
|
|
2810
|
+
case "notifications/initialized":
|
|
2811
|
+
return void 0;
|
|
2812
|
+
case "ping":
|
|
2813
|
+
return jsonRpcSuccess(id, {});
|
|
2814
|
+
case "tools/list":
|
|
2815
|
+
return this.listTools(id);
|
|
2816
|
+
case "tools/call":
|
|
2817
|
+
return this.callTool(id, message.params);
|
|
2818
|
+
default:
|
|
2819
|
+
return "id" in message ? jsonRpcError(id, -32601, `Method not found: ${message.method}`) : void 0;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
initialize(id, params) {
|
|
2823
|
+
const parsed = safeParseWithErrors(
|
|
2824
|
+
z2.object({
|
|
2825
|
+
protocolVersion: z2.string().min(1),
|
|
2826
|
+
capabilities: z2.record(z2.string(), z2.unknown()).default({}),
|
|
2827
|
+
clientInfo: z2.object({
|
|
2828
|
+
name: z2.string().min(1),
|
|
2829
|
+
version: z2.string().min(1),
|
|
2830
|
+
title: z2.string().optional()
|
|
2831
|
+
}).strict()
|
|
2832
|
+
}).strict(),
|
|
2833
|
+
params
|
|
2834
|
+
);
|
|
2835
|
+
if (!parsed.success) {
|
|
2836
|
+
return jsonRpcError(id, -32602, "Invalid initialize params", { errors: parsed.errors });
|
|
2837
|
+
}
|
|
2838
|
+
const selectedProtocolVersion = selectProtocolVersion(parsed.data.protocolVersion);
|
|
2839
|
+
this.negotiatedProtocolVersion = selectedProtocolVersion;
|
|
2840
|
+
return jsonRpcSuccess(id, {
|
|
2841
|
+
protocolVersion: selectedProtocolVersion,
|
|
2842
|
+
capabilities: {
|
|
2843
|
+
tools: {
|
|
2844
|
+
listChanged: false
|
|
2845
|
+
}
|
|
2846
|
+
},
|
|
2847
|
+
serverInfo: {
|
|
2848
|
+
name: DROWAI_MCP_SERVER_NAME,
|
|
2849
|
+
title: DROWAI_MCP_SERVER_TITLE,
|
|
2850
|
+
version: DROWAI_MCP_SERVER_VERSION
|
|
2851
|
+
},
|
|
2852
|
+
instructions: SERVER_INSTRUCTIONS
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
listTools(id) {
|
|
2856
|
+
if (!this.negotiatedProtocolVersion) {
|
|
2857
|
+
return jsonRpcError(id, -32002, "Server not initialized");
|
|
2858
|
+
}
|
|
2859
|
+
return jsonRpcSuccess(id, {
|
|
2860
|
+
tools: this.tools.map((tool) => ({
|
|
2861
|
+
name: tool.name,
|
|
2862
|
+
title: tool.title,
|
|
2863
|
+
description: tool.description,
|
|
2864
|
+
inputSchema: toolInputSchema(tool.schema)
|
|
2865
|
+
}))
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
async callTool(id, params) {
|
|
2869
|
+
if (!this.negotiatedProtocolVersion) {
|
|
2870
|
+
return jsonRpcError(id, -32002, "Server not initialized");
|
|
2871
|
+
}
|
|
2872
|
+
const parsed = safeParseWithErrors(
|
|
2873
|
+
z2.object({
|
|
2874
|
+
name: z2.string().min(1),
|
|
2875
|
+
arguments: z2.record(z2.string(), z2.unknown()).optional()
|
|
2876
|
+
}).strict(),
|
|
2877
|
+
params
|
|
2878
|
+
);
|
|
2879
|
+
if (!parsed.success) {
|
|
2880
|
+
return jsonRpcSuccess(id, toolArgumentError(parsed.errors));
|
|
2881
|
+
}
|
|
2882
|
+
const tool = this.toolMap.get(parsed.data.name);
|
|
2883
|
+
if (!tool) {
|
|
2884
|
+
return jsonRpcSuccess(
|
|
2885
|
+
id,
|
|
2886
|
+
toolResult(
|
|
2887
|
+
{
|
|
2888
|
+
success: false,
|
|
2889
|
+
revision: null,
|
|
2890
|
+
errors: [
|
|
2891
|
+
{
|
|
2892
|
+
code: "UNKNOWN_TOOL",
|
|
2893
|
+
message: `Unknown MCP tool: ${parsed.data.name}`
|
|
2894
|
+
}
|
|
2895
|
+
],
|
|
2896
|
+
warnings: [],
|
|
2897
|
+
changedObjectIds: []
|
|
2898
|
+
},
|
|
2899
|
+
true
|
|
2900
|
+
)
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
const validatedArguments = safeParseWithErrors(tool.schema, parsed.data.arguments ?? {});
|
|
2904
|
+
if (!validatedArguments.success) {
|
|
2905
|
+
return jsonRpcSuccess(id, toolArgumentError(validatedArguments.errors));
|
|
2906
|
+
}
|
|
2907
|
+
try {
|
|
2908
|
+
const payload = await tool.execute(validatedArguments.data, this.bridge);
|
|
2909
|
+
const resultPayload = typeof payload === "object" && payload !== null && "success" in payload ? payload : { success: true, data: payload };
|
|
2910
|
+
const isError = typeof resultPayload === "object" && resultPayload !== null && "success" in resultPayload && resultPayload.success === false;
|
|
2911
|
+
return jsonRpcSuccess(id, toolResult(resultPayload, isError));
|
|
2912
|
+
} catch (cause) {
|
|
2913
|
+
return jsonRpcSuccess(
|
|
2914
|
+
id,
|
|
2915
|
+
toolResult(
|
|
2916
|
+
{
|
|
2917
|
+
success: false,
|
|
2918
|
+
revision: null,
|
|
2919
|
+
errors: [
|
|
2920
|
+
{
|
|
2921
|
+
code: "MCP_TOOL_EXECUTION_FAILED",
|
|
2922
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
2923
|
+
}
|
|
2924
|
+
],
|
|
2925
|
+
warnings: [],
|
|
2926
|
+
changedObjectIds: []
|
|
2927
|
+
},
|
|
2928
|
+
true
|
|
2929
|
+
)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
var createDrowaiMcpServer = (options = {}) => DrowaiMcpServer.create(options);
|
|
2935
|
+
|
|
2936
|
+
import { createInterface } from "node:readline";
|
|
2937
|
+
var defaultRuntime = () => ({
|
|
2938
|
+
stdin: process.stdin,
|
|
2939
|
+
stdout: process.stdout,
|
|
2940
|
+
signalProcess: process,
|
|
2941
|
+
exit: (code) => {
|
|
2942
|
+
process.exit(code);
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
var send = (output, message) => {
|
|
2946
|
+
output.write(`${JSON.stringify(message)}
|
|
2947
|
+
`);
|
|
2948
|
+
};
|
|
2949
|
+
var runStdioMcpServer = async (options, runtime = defaultRuntime()) => {
|
|
2950
|
+
const server = await createDrowaiMcpServer(options);
|
|
2951
|
+
const reader = createInterface({
|
|
2952
|
+
input: runtime.stdin,
|
|
2953
|
+
crlfDelay: Infinity
|
|
2954
|
+
});
|
|
2955
|
+
let closed = false;
|
|
2956
|
+
const closeServer = async () => {
|
|
2957
|
+
if (closed) {
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
closed = true;
|
|
2961
|
+
reader.close();
|
|
2962
|
+
await server.close();
|
|
2963
|
+
};
|
|
2964
|
+
const handleSignal = async () => {
|
|
2965
|
+
await closeServer();
|
|
2966
|
+
runtime.exit?.(0);
|
|
2967
|
+
};
|
|
2968
|
+
const signalProcess = runtime.signalProcess;
|
|
2969
|
+
signalProcess?.on("SIGINT", handleSignal);
|
|
2970
|
+
signalProcess?.on("SIGTERM", handleSignal);
|
|
2971
|
+
try {
|
|
2972
|
+
for await (const rawLine of reader) {
|
|
2973
|
+
const line = rawLine.trim();
|
|
2974
|
+
if (!line) {
|
|
2975
|
+
continue;
|
|
2976
|
+
}
|
|
2977
|
+
try {
|
|
2978
|
+
const message = JSON.parse(line);
|
|
2979
|
+
const response = await server.handleMessage(message);
|
|
2980
|
+
if (response) {
|
|
2981
|
+
send(runtime.stdout, response);
|
|
2982
|
+
}
|
|
2983
|
+
} catch (cause) {
|
|
2984
|
+
send(runtime.stdout, {
|
|
2985
|
+
jsonrpc: "2.0",
|
|
2986
|
+
id: null,
|
|
2987
|
+
error: {
|
|
2988
|
+
code: -32700,
|
|
2989
|
+
message: "Parse error",
|
|
2990
|
+
data: cause instanceof Error ? cause.message : String(cause)
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
} finally {
|
|
2996
|
+
signalProcess?.off("SIGINT", handleSignal);
|
|
2997
|
+
signalProcess?.off("SIGTERM", handleSignal);
|
|
2998
|
+
await closeServer();
|
|
2999
|
+
}
|
|
3000
|
+
};
|
|
3001
|
+
|
|
3002
|
+
var defaultCliRuntime = () => ({
|
|
3003
|
+
stdin: process.stdin,
|
|
3004
|
+
stdout: process.stdout,
|
|
3005
|
+
stderr: process.stderr,
|
|
3006
|
+
signalProcess: process,
|
|
3007
|
+
exit: (code) => {
|
|
3008
|
+
process.exit(code);
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
var usageText = () => [
|
|
3012
|
+
"Usage: drowai-mcp [--db-path <path>] [--auth-token <token>] [--help] [--version]",
|
|
3013
|
+
"",
|
|
3014
|
+
"MCP-only CLI for the DrowAI diagram platform.",
|
|
3015
|
+
"The package does not expose API or viewer subcommands.",
|
|
3016
|
+
"",
|
|
3017
|
+
"Options:",
|
|
3018
|
+
" --db-path <path> Persist SQLite data to the given path",
|
|
3019
|
+
" --auth-token <token> Override the embedded API auth secret",
|
|
3020
|
+
" --help Print this help text to stderr",
|
|
3021
|
+
" --version Print the CLI version to stderr",
|
|
3022
|
+
"",
|
|
3023
|
+
"Environment:",
|
|
3024
|
+
" DROWAI_DB_PATH Fallback database path when --db-path is omitted",
|
|
3025
|
+
" DROWAI_MCP_TOKEN Fallback auth token when --auth-token is omitted",
|
|
3026
|
+
"",
|
|
3027
|
+
"Examples:",
|
|
3028
|
+
" npx drowai-mcp",
|
|
3029
|
+
" DROWAI_DB_PATH=./state/drowai.sqlite npx drowai-mcp",
|
|
3030
|
+
" npx drowai-mcp --db-path ./state/drowai.sqlite"
|
|
3031
|
+
].join("\n");
|
|
3032
|
+
var readInlineOrNext = (argv, index, name) => {
|
|
3033
|
+
const current = argv[index] ?? "";
|
|
3034
|
+
const inlinePrefix = `${name}=`;
|
|
3035
|
+
if (current.startsWith(inlinePrefix)) {
|
|
3036
|
+
const value = current.slice(inlinePrefix.length);
|
|
3037
|
+
return value ? { value, nextIndex: index } : { nextIndex: index, error: `Missing value for ${name}` };
|
|
3038
|
+
}
|
|
3039
|
+
const next = argv[index + 1];
|
|
3040
|
+
if (!next || next.startsWith("-")) {
|
|
3041
|
+
return { nextIndex: index, error: `Missing value for ${name}` };
|
|
3042
|
+
}
|
|
3043
|
+
return { value: next, nextIndex: index + 1 };
|
|
3044
|
+
};
|
|
3045
|
+
var parseCliArgs = (argv, env = process.env) => {
|
|
3046
|
+
let dbPath = env.DROWAI_DB_PATH;
|
|
3047
|
+
let authToken = env.DROWAI_MCP_TOKEN;
|
|
3048
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
3049
|
+
const arg = argv[index] ?? "";
|
|
3050
|
+
if (arg === "--help" || arg === "-h") {
|
|
3051
|
+
return { kind: "help" };
|
|
3052
|
+
}
|
|
3053
|
+
if (arg === "--version" || arg === "-v") {
|
|
3054
|
+
return { kind: "version" };
|
|
3055
|
+
}
|
|
3056
|
+
if (arg === "--db-path" || arg.startsWith("--db-path=")) {
|
|
3057
|
+
const parsed = readInlineOrNext(argv, index, "--db-path");
|
|
3058
|
+
if (parsed.error) {
|
|
3059
|
+
return { kind: "error", message: parsed.error };
|
|
3060
|
+
}
|
|
3061
|
+
dbPath = parsed.value;
|
|
3062
|
+
index = parsed.nextIndex;
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
if (arg === "--auth-token" || arg.startsWith("--auth-token=")) {
|
|
3066
|
+
const parsed = readInlineOrNext(argv, index, "--auth-token");
|
|
3067
|
+
if (parsed.error) {
|
|
3068
|
+
return { kind: "error", message: parsed.error };
|
|
3069
|
+
}
|
|
3070
|
+
authToken = parsed.value;
|
|
3071
|
+
index = parsed.nextIndex;
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
if (arg.startsWith("-")) {
|
|
3075
|
+
return { kind: "error", message: `Unknown argument: ${arg}` };
|
|
3076
|
+
}
|
|
3077
|
+
return { kind: "error", message: `Unexpected positional argument: ${arg}` };
|
|
3078
|
+
}
|
|
3079
|
+
return {
|
|
3080
|
+
kind: "run",
|
|
3081
|
+
serverOptions: {
|
|
3082
|
+
...dbPath ? { dbPath } : {},
|
|
3083
|
+
...authToken ? { authToken } : {}
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
};
|
|
3087
|
+
var runCli = async (argv = process.argv.slice(2), runtime = defaultCliRuntime(), env = process.env) => {
|
|
3088
|
+
const command = parseCliArgs(argv, env);
|
|
3089
|
+
switch (command.kind) {
|
|
3090
|
+
case "help":
|
|
3091
|
+
runtime.stderr.write(`${usageText()}
|
|
3092
|
+
`);
|
|
3093
|
+
return 0;
|
|
3094
|
+
case "version":
|
|
3095
|
+
runtime.stderr.write(`${DROWAI_MCP_SERVER_VERSION}
|
|
3096
|
+
`);
|
|
3097
|
+
return 0;
|
|
3098
|
+
case "error":
|
|
3099
|
+
runtime.stderr.write(`${command.message}
|
|
3100
|
+
|
|
3101
|
+
${usageText()}
|
|
3102
|
+
`);
|
|
3103
|
+
return 1;
|
|
3104
|
+
case "run":
|
|
3105
|
+
await runStdioMcpServer(command.serverOptions, runtime);
|
|
3106
|
+
return 0;
|
|
3107
|
+
}
|
|
3108
|
+
};
|
|
3109
|
+
var isCliEntrypoint = (entryUrl, argvPath) => {
|
|
3110
|
+
if (!argvPath) {
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
try {
|
|
3114
|
+
return realpathSync(fileURLToPath(entryUrl)) === realpathSync(argvPath);
|
|
3115
|
+
} catch {
|
|
3116
|
+
return entryUrl === new URL(argvPath, "file://").href;
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
if (isCliEntrypoint(import.meta.url, process.argv[1])) {
|
|
3120
|
+
const exitCode = await runCli();
|
|
3121
|
+
process.exitCode = exitCode;
|
|
3122
|
+
}
|
|
3123
|
+
export {
|
|
3124
|
+
parseCliArgs,
|
|
3125
|
+
runCli
|
|
3126
|
+
};
|