@graph-knowledge/api 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.
Files changed (74) hide show
  1. package/README.md +396 -0
  2. package/package.json +44 -0
  3. package/src/index.d.ts +18 -0
  4. package/src/index.js +19 -0
  5. package/src/lib/clients/firebase-auth-client.d.ts +24 -0
  6. package/src/lib/clients/firebase-auth-client.js +76 -0
  7. package/src/lib/clients/firebase-firestore-client.d.ts +22 -0
  8. package/src/lib/clients/firebase-firestore-client.js +79 -0
  9. package/src/lib/config/api-config.d.ts +20 -0
  10. package/src/lib/config/api-config.js +0 -0
  11. package/src/lib/constants/element-defaults.d.ts +15 -0
  12. package/src/lib/constants/element-defaults.js +19 -0
  13. package/src/lib/errors/api-errors.d.ts +33 -0
  14. package/src/lib/errors/api-errors.js +51 -0
  15. package/src/lib/graph-knowledge-api.d.ts +106 -0
  16. package/src/lib/graph-knowledge-api.js +154 -0
  17. package/src/lib/interfaces/auth-client.interface.d.ts +35 -0
  18. package/src/lib/interfaces/auth-client.interface.js +0 -0
  19. package/src/lib/interfaces/batch-operations.interface.d.ts +46 -0
  20. package/src/lib/interfaces/batch-operations.interface.js +0 -0
  21. package/src/lib/interfaces/document-operations.interface.d.ts +51 -0
  22. package/src/lib/interfaces/document-operations.interface.js +0 -0
  23. package/src/lib/interfaces/element-operations.interface.d.ts +57 -0
  24. package/src/lib/interfaces/element-operations.interface.js +0 -0
  25. package/src/lib/interfaces/element-validator.interface.d.ts +42 -0
  26. package/src/lib/interfaces/element-validator.interface.js +0 -0
  27. package/src/lib/interfaces/firestore-client.interface.d.ts +62 -0
  28. package/src/lib/interfaces/firestore-client.interface.js +0 -0
  29. package/src/lib/interfaces/node-operations.interface.d.ts +62 -0
  30. package/src/lib/interfaces/node-operations.interface.js +0 -0
  31. package/src/lib/models/document.model.d.ts +73 -0
  32. package/src/lib/models/document.model.js +13 -0
  33. package/src/lib/models/index.d.ts +6 -0
  34. package/src/lib/models/index.js +5 -0
  35. package/src/lib/operations/batch-operations.d.ts +30 -0
  36. package/src/lib/operations/batch-operations.js +181 -0
  37. package/src/lib/operations/document-operations.d.ts +20 -0
  38. package/src/lib/operations/document-operations.js +108 -0
  39. package/src/lib/operations/element-operations.d.ts +33 -0
  40. package/src/lib/operations/element-operations.js +175 -0
  41. package/src/lib/operations/node-operations.d.ts +22 -0
  42. package/src/lib/operations/node-operations.js +89 -0
  43. package/src/lib/testing/index.d.ts +2 -0
  44. package/src/lib/testing/index.js +3 -0
  45. package/src/lib/testing/mock-auth-client.d.ts +26 -0
  46. package/src/lib/testing/mock-auth-client.js +49 -0
  47. package/src/lib/testing/mock-firestore-client.d.ts +32 -0
  48. package/src/lib/testing/mock-firestore-client.js +126 -0
  49. package/src/lib/types/api-types.d.ts +61 -0
  50. package/src/lib/types/api-types.js +0 -0
  51. package/src/lib/types/element-input-types.d.ts +237 -0
  52. package/src/lib/types/element-input-types.js +3 -0
  53. package/src/lib/utils/link-level-manager.d.ts +66 -0
  54. package/src/lib/utils/link-level-manager.js +200 -0
  55. package/src/lib/utils/uuid.d.ts +5 -0
  56. package/src/lib/utils/uuid.js +16 -0
  57. package/src/lib/validators/document-validator.d.ts +22 -0
  58. package/src/lib/validators/document-validator.js +68 -0
  59. package/src/lib/validators/element-type-validators/base-element-validator.d.ts +35 -0
  60. package/src/lib/validators/element-type-validators/base-element-validator.js +96 -0
  61. package/src/lib/validators/element-type-validators/connector-validator.d.ts +13 -0
  62. package/src/lib/validators/element-type-validators/connector-validator.js +66 -0
  63. package/src/lib/validators/element-type-validators/custom-shape-validator.d.ts +22 -0
  64. package/src/lib/validators/element-type-validators/custom-shape-validator.js +44 -0
  65. package/src/lib/validators/element-type-validators/rectangle-validator.d.ts +11 -0
  66. package/src/lib/validators/element-type-validators/rectangle-validator.js +31 -0
  67. package/src/lib/validators/element-type-validators/text-validator.d.ts +14 -0
  68. package/src/lib/validators/element-type-validators/text-validator.js +66 -0
  69. package/src/lib/validators/element-type-validators/uml-validators.d.ts +58 -0
  70. package/src/lib/validators/element-type-validators/uml-validators.js +123 -0
  71. package/src/lib/validators/element-validator-registry.d.ts +18 -0
  72. package/src/lib/validators/element-validator-registry.js +58 -0
  73. package/src/lib/validators/node-validator.d.ts +26 -0
  74. package/src/lib/validators/node-validator.js +90 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Common properties for all element inputs.
3
+ */
4
+ export interface BaseElementInput {
5
+ /** X position on canvas */
6
+ x: number;
7
+ /** Y position on canvas */
8
+ y: number;
9
+ /** Element width (uses default if not specified) */
10
+ width?: number;
11
+ /** Element height (uses default if not specified) */
12
+ height?: number;
13
+ /** Rotation in degrees [0, 360) */
14
+ rotation?: number;
15
+ /** Whether this element links to another node */
16
+ isLink?: boolean;
17
+ /** Target node ID when isLink is true */
18
+ linkTarget?: string;
19
+ }
20
+ /**
21
+ * Input for creating a rectangle element.
22
+ */
23
+ export interface RectangleInput extends BaseElementInput {
24
+ type: "rectangle";
25
+ /** Fill color (hex, e.g., "#FF5733") */
26
+ fillColor?: string;
27
+ /** Stroke color (hex) */
28
+ strokeColor?: string;
29
+ /** Stroke width in pixels */
30
+ strokeWidth?: number;
31
+ /** Corner radius for rounded rectangles */
32
+ cornerRadius?: number;
33
+ }
34
+ /**
35
+ * Input for creating a text element.
36
+ */
37
+ export interface TextInput extends BaseElementInput {
38
+ type: "text";
39
+ /** Text content (required for text elements) */
40
+ text: string;
41
+ /** Font family */
42
+ fontFamily?: string;
43
+ /** Font size in pixels */
44
+ fontSize?: number;
45
+ /** Font weight (100-900) */
46
+ fontWeight?: number;
47
+ /** Text color (hex) */
48
+ fillColor?: string;
49
+ /** Text alignment */
50
+ textAlign?: "left" | "center" | "right";
51
+ /** Line height multiplier */
52
+ lineHeight?: number;
53
+ }
54
+ /**
55
+ * Anchor points for connectors.
56
+ * These match the snap geometry labels used by the graph editor.
57
+ */
58
+ export type ConnectorAnchor = "edge-top" | "edge-right" | "edge-bottom" | "edge-left" | "center";
59
+ /**
60
+ * Line styles for connectors.
61
+ */
62
+ export type LineStyle = "solid" | "dashed" | "dotted";
63
+ /**
64
+ * Marker types for connector ends.
65
+ */
66
+ export type MarkerType = "none" | "arrow" | "diamond" | "circle";
67
+ /**
68
+ * Input for creating a connector element.
69
+ * Note: Connectors don't use x/y/width/height - their position is determined by connected elements.
70
+ */
71
+ export interface ConnectorInput {
72
+ type: "connector";
73
+ /** X position (ignored for connectors, defaults to 0) */
74
+ x?: number;
75
+ /** Y position (ignored for connectors, defaults to 0) */
76
+ y?: number;
77
+ /** Width (ignored for connectors, defaults to 0) */
78
+ width?: number;
79
+ /** Height (ignored for connectors, defaults to 0) */
80
+ height?: number;
81
+ /** Rotation (ignored for connectors) */
82
+ rotation?: number;
83
+ /** Source element ID (required) */
84
+ startElementId: string;
85
+ /** Anchor point on source element */
86
+ startAnchor?: ConnectorAnchor;
87
+ /** Target element ID (required) */
88
+ endElementId: string;
89
+ /** Anchor point on target element */
90
+ endAnchor?: ConnectorAnchor;
91
+ /** Line color (hex) */
92
+ strokeColor?: string;
93
+ /** Line width in pixels */
94
+ strokeWidth?: number;
95
+ /** Line style */
96
+ lineStyle?: LineStyle;
97
+ /** Marker at start of line */
98
+ startMarker?: MarkerType;
99
+ /** Marker at end of line */
100
+ endMarker?: MarkerType;
101
+ /** Optional label on connector */
102
+ label?: string;
103
+ /** Whether this element links to another node */
104
+ isLink?: boolean;
105
+ /** Target node ID when isLink is true */
106
+ linkTarget?: string;
107
+ }
108
+ /**
109
+ * Input for creating a UML class element.
110
+ */
111
+ export interface UmlClassInput extends BaseElementInput {
112
+ type: "uml-class";
113
+ /** Class name */
114
+ name?: string;
115
+ /** Attributes (one per line, e.g., "+ attribute: Type") */
116
+ attributes?: string;
117
+ /** Methods (one per line, e.g., "+ method(): void") */
118
+ methods?: string;
119
+ /** Stereotype (e.g., "entity", "service") */
120
+ stereotype?: string;
121
+ /** Fill color (hex) */
122
+ fillColor?: string;
123
+ /** Stroke color (hex) */
124
+ strokeColor?: string;
125
+ /** Stroke width in pixels */
126
+ strokeWidth?: number;
127
+ }
128
+ /**
129
+ * Input for creating a UML interface element.
130
+ */
131
+ export interface UmlInterfaceInput extends BaseElementInput {
132
+ type: "uml-interface";
133
+ /** Interface name */
134
+ name?: string;
135
+ /** Methods (one per line) */
136
+ methods?: string;
137
+ /** Fill color (hex) */
138
+ fillColor?: string;
139
+ /** Stroke color (hex) */
140
+ strokeColor?: string;
141
+ /** Stroke width in pixels */
142
+ strokeWidth?: number;
143
+ }
144
+ /**
145
+ * Input for creating a UML component element.
146
+ */
147
+ export interface UmlComponentInput extends BaseElementInput {
148
+ type: "uml-component";
149
+ /** Component name */
150
+ name?: string;
151
+ /** Stereotype (e.g., "service", "middleware") */
152
+ stereotype?: string;
153
+ /** Fill color (hex) */
154
+ fillColor?: string;
155
+ /** Stroke color (hex) */
156
+ strokeColor?: string;
157
+ /** Stroke width in pixels */
158
+ strokeWidth?: number;
159
+ }
160
+ /**
161
+ * Input for creating a UML package element.
162
+ */
163
+ export interface UmlPackageInput extends BaseElementInput {
164
+ type: "uml-package";
165
+ /** Package name */
166
+ name?: string;
167
+ /** Fill color (hex) */
168
+ fillColor?: string;
169
+ /** Stroke color (hex) */
170
+ strokeColor?: string;
171
+ /** Stroke width in pixels */
172
+ strokeWidth?: number;
173
+ }
174
+ /**
175
+ * Input for creating a UML artifact element.
176
+ */
177
+ export interface UmlArtifactInput extends BaseElementInput {
178
+ type: "uml-artifact";
179
+ /** Artifact name */
180
+ name?: string;
181
+ /** Stereotype (e.g., "document", "source") */
182
+ stereotype?: string;
183
+ /** Fill color (hex) */
184
+ fillColor?: string;
185
+ /** Stroke color (hex) */
186
+ strokeColor?: string;
187
+ /** Stroke width in pixels */
188
+ strokeWidth?: number;
189
+ }
190
+ /**
191
+ * Input for creating a UML note element.
192
+ */
193
+ export interface UmlNoteInput extends BaseElementInput {
194
+ type: "uml-note";
195
+ /** Note text */
196
+ text?: string;
197
+ /** Fill color (hex) */
198
+ fillColor?: string;
199
+ /** Stroke color (hex) */
200
+ strokeColor?: string;
201
+ /** Stroke width in pixels */
202
+ strokeWidth?: number;
203
+ }
204
+ /**
205
+ * Input for creating a custom shape element.
206
+ */
207
+ export interface CustomShapeInput extends BaseElementInput {
208
+ type: `custom:${string}`;
209
+ /** Custom shape definition ID (required) */
210
+ customShapeId: string;
211
+ /** Fill color (hex) */
212
+ fillColor?: string;
213
+ /** Stroke color (hex) */
214
+ strokeColor?: string;
215
+ /** Stroke width in pixels */
216
+ strokeWidth?: number;
217
+ /** Additional custom properties */
218
+ [key: string]: unknown;
219
+ }
220
+ /**
221
+ * Union of all element input types.
222
+ */
223
+ export type AnyElementInput = RectangleInput | TextInput | ConnectorInput | UmlClassInput | UmlInterfaceInput | UmlComponentInput | UmlPackageInput | UmlArtifactInput | UmlNoteInput | CustomShapeInput;
224
+ /**
225
+ * Input for updating an existing element.
226
+ */
227
+ export interface UpdateElementInput {
228
+ x?: number;
229
+ y?: number;
230
+ width?: number;
231
+ height?: number;
232
+ rotation?: number;
233
+ /** Shape-specific properties to update */
234
+ properties?: Record<string, unknown>;
235
+ isLink?: boolean;
236
+ linkTarget?: string;
237
+ }
@@ -0,0 +1,3 @@
1
+ // ============================================================================
2
+ // Base Element Input
3
+ // ============================================================================
@@ -0,0 +1,66 @@
1
+ import { GraphNode } from "@graph-knowledge/domain";
2
+ /**
3
+ * Represents a change to a node's level.
4
+ */
5
+ export interface LevelChange {
6
+ nodeId: string;
7
+ level: number | undefined;
8
+ }
9
+ /**
10
+ * Link state for calculating level changes.
11
+ */
12
+ export interface LinkState {
13
+ isLink?: boolean;
14
+ linkTarget?: string;
15
+ }
16
+ /**
17
+ * Pure utility for managing node level calculations.
18
+ * Levels represent distance from root node in the graph.
19
+ * - Root node: level = 0
20
+ * - Linked nodes: level = source.level + 1 (minimum if multiple incoming links)
21
+ * - Orphan nodes: level = undefined
22
+ *
23
+ * Extracted from web app's LinkManagerService for use in headless API.
24
+ */
25
+ export declare class LinkLevelManager {
26
+ /**
27
+ * Called when a link is created (element.isLink set to true with linkTarget).
28
+ * @param sourceNodeId The node containing the linking element
29
+ * @param targetNodeId The node being linked to
30
+ * @param nodes Current state of all nodes
31
+ * @returns Array of level changes to apply
32
+ */
33
+ onLinkCreated(sourceNodeId: string, targetNodeId: string, nodes: GraphNode[]): LevelChange[];
34
+ /**
35
+ * Called when a link is removed (element.isLink set to false or linkTarget cleared).
36
+ * @param targetNodeId The node that was being linked to
37
+ * @param nodes Current state of all nodes (should already have the link removed)
38
+ * @returns Array of level changes to apply
39
+ */
40
+ onLinkRemoved(targetNodeId: string, nodes: GraphNode[]): LevelChange[];
41
+ /**
42
+ * Calculates level changes based on before/after link state.
43
+ * This is a convenience method that determines the appropriate action.
44
+ * @param sourceNodeId The node containing the element
45
+ * @param elementId The element being modified
46
+ * @param before Previous link state
47
+ * @param after New link state
48
+ * @param nodes Current state of all nodes (with the change already applied)
49
+ * @returns Array of level changes to apply
50
+ */
51
+ calculateLevelChanges(sourceNodeId: string, elementId: string, before: LinkState, after: LinkState, nodes: GraphNode[]): LevelChange[];
52
+ /**
53
+ * Find all elements across all nodes that link to the target node.
54
+ */
55
+ private findIncomingLinks;
56
+ /**
57
+ * Calculate the minimum level from a set of incoming links.
58
+ * @param incomingLinks The incoming links with their source levels
59
+ * @param pendingChanges Pending level changes to consider
60
+ */
61
+ private calculateMinLevel;
62
+ /**
63
+ * Recursively update levels for nodes that the changed node links to.
64
+ */
65
+ private cascadeUpdates;
66
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Pure utility for managing node level calculations.
3
+ * Levels represent distance from root node in the graph.
4
+ * - Root node: level = 0
5
+ * - Linked nodes: level = source.level + 1 (minimum if multiple incoming links)
6
+ * - Orphan nodes: level = undefined
7
+ *
8
+ * Extracted from web app's LinkManagerService for use in headless API.
9
+ */
10
+ export class LinkLevelManager {
11
+ /**
12
+ * Called when a link is created (element.isLink set to true with linkTarget).
13
+ * @param sourceNodeId The node containing the linking element
14
+ * @param targetNodeId The node being linked to
15
+ * @param nodes Current state of all nodes
16
+ * @returns Array of level changes to apply
17
+ */
18
+ onLinkCreated(sourceNodeId, targetNodeId, nodes) {
19
+ const changes = [];
20
+ const sourceNode = nodes.find((n) => n.id === sourceNodeId);
21
+ const targetNode = nodes.find((n) => n.id === targetNodeId);
22
+ if (!sourceNode || !targetNode) {
23
+ return changes;
24
+ }
25
+ // Source must have a defined level to propagate
26
+ if (sourceNode.level === undefined) {
27
+ return changes;
28
+ }
29
+ const proposedLevel = sourceNode.level + 1;
30
+ // Only update if target has no level or proposed is better (lower)
31
+ if (targetNode.level === undefined ||
32
+ proposedLevel < targetNode.level) {
33
+ changes.push({ nodeId: targetNodeId, level: proposedLevel });
34
+ // Cascade to nodes that targetNode links to
35
+ this.cascadeUpdates(targetNodeId, proposedLevel, nodes, changes);
36
+ }
37
+ return changes;
38
+ }
39
+ /**
40
+ * Called when a link is removed (element.isLink set to false or linkTarget cleared).
41
+ * @param targetNodeId The node that was being linked to
42
+ * @param nodes Current state of all nodes (should already have the link removed)
43
+ * @returns Array of level changes to apply
44
+ */
45
+ onLinkRemoved(targetNodeId, nodes) {
46
+ const changes = [];
47
+ const targetNode = nodes.find((n) => n.id === targetNodeId);
48
+ if (!targetNode) {
49
+ return changes;
50
+ }
51
+ // Don't modify root node level
52
+ if (targetNode.level === 0) {
53
+ return changes;
54
+ }
55
+ // If already undefined, nothing to do
56
+ if (targetNode.level === undefined) {
57
+ return changes;
58
+ }
59
+ // Find all remaining incoming links to this node
60
+ const incomingLinks = this.findIncomingLinks(targetNodeId, nodes);
61
+ if (incomingLinks.length === 0) {
62
+ // No incoming links - node becomes orphan
63
+ changes.push({ nodeId: targetNodeId, level: undefined });
64
+ // Cascade: downstream nodes may also become orphans
65
+ this.cascadeUpdates(targetNodeId, undefined, nodes, changes);
66
+ }
67
+ else {
68
+ // Recalculate from remaining incoming links
69
+ const newLevel = this.calculateMinLevel(incomingLinks);
70
+ if (newLevel !== targetNode.level) {
71
+ changes.push({ nodeId: targetNodeId, level: newLevel });
72
+ // Cascade the change
73
+ this.cascadeUpdates(targetNodeId, newLevel, nodes, changes);
74
+ }
75
+ }
76
+ return changes;
77
+ }
78
+ /**
79
+ * Calculates level changes based on before/after link state.
80
+ * This is a convenience method that determines the appropriate action.
81
+ * @param sourceNodeId The node containing the element
82
+ * @param elementId The element being modified
83
+ * @param before Previous link state
84
+ * @param after New link state
85
+ * @param nodes Current state of all nodes (with the change already applied)
86
+ * @returns Array of level changes to apply
87
+ */
88
+ calculateLevelChanges(sourceNodeId, elementId, before, after, nodes) {
89
+ const wasLink = before.isLink && before.linkTarget;
90
+ const isLink = after.isLink && after.linkTarget;
91
+ // Case 1: Link created
92
+ if (!wasLink && isLink) {
93
+ return this.onLinkCreated(sourceNodeId, after.linkTarget, nodes);
94
+ }
95
+ // Case 2: Link removed
96
+ if (wasLink && !isLink) {
97
+ return this.onLinkRemoved(before.linkTarget, nodes);
98
+ }
99
+ // Case 3: Link target changed
100
+ if (wasLink && isLink && before.linkTarget !== after.linkTarget) {
101
+ const removedChanges = this.onLinkRemoved(before.linkTarget, nodes);
102
+ const createdChanges = this.onLinkCreated(sourceNodeId, after.linkTarget, nodes);
103
+ // Merge changes, avoiding duplicates
104
+ const merged = new Map();
105
+ for (const change of [...removedChanges, ...createdChanges]) {
106
+ merged.set(change.nodeId, change);
107
+ }
108
+ return Array.from(merged.values());
109
+ }
110
+ // No link change
111
+ return [];
112
+ }
113
+ /**
114
+ * Find all elements across all nodes that link to the target node.
115
+ */
116
+ findIncomingLinks(targetNodeId, nodes) {
117
+ const incoming = [];
118
+ for (const node of nodes) {
119
+ if (!node.elements)
120
+ continue;
121
+ for (const element of node.elements) {
122
+ if (element.isLink && element.linkTarget === targetNodeId) {
123
+ incoming.push({
124
+ sourceNodeId: node.id,
125
+ sourceLevel: node.level
126
+ });
127
+ }
128
+ }
129
+ }
130
+ return incoming;
131
+ }
132
+ /**
133
+ * Calculate the minimum level from a set of incoming links.
134
+ * @param incomingLinks The incoming links with their source levels
135
+ * @param pendingChanges Pending level changes to consider
136
+ */
137
+ calculateMinLevel(incomingLinks, pendingChanges = []) {
138
+ let minLevel = undefined;
139
+ // Create a map of pending changes for quick lookup
140
+ const changeMap = new Map(pendingChanges.map((c) => [c.nodeId, c.level]));
141
+ for (const link of incomingLinks) {
142
+ // Use pending level if there's a change for this source
143
+ const effectiveLevel = changeMap.has(link.sourceNodeId)
144
+ ? changeMap.get(link.sourceNodeId)
145
+ : link.sourceLevel;
146
+ if (effectiveLevel !== undefined) {
147
+ const proposedLevel = effectiveLevel + 1;
148
+ if (minLevel === undefined || proposedLevel < minLevel) {
149
+ minLevel = proposedLevel;
150
+ }
151
+ }
152
+ }
153
+ return minLevel;
154
+ }
155
+ /**
156
+ * Recursively update levels for nodes that the changed node links to.
157
+ */
158
+ cascadeUpdates(nodeId, newLevel, nodes, changes) {
159
+ const node = nodes.find((n) => n.id === nodeId);
160
+ if (!node?.elements)
161
+ return;
162
+ // Find all outgoing links from this node
163
+ const outgoingTargetIds = node.elements
164
+ .filter((el) => el.isLink && el.linkTarget)
165
+ .map((el) => el.linkTarget);
166
+ for (const targetId of outgoingTargetIds) {
167
+ // Skip if we've already queued a change for this node
168
+ if (changes.some((c) => c.nodeId === targetId))
169
+ continue;
170
+ const targetNode = nodes.find((n) => n.id === targetId);
171
+ if (!targetNode)
172
+ continue;
173
+ // Don't modify root node
174
+ if (targetNode.level === 0)
175
+ continue;
176
+ if (newLevel === undefined) {
177
+ // Source became orphan - recalculate target from all its incoming links
178
+ // Pass pending changes so we use updated levels, not stale ones
179
+ const targetIncoming = this.findIncomingLinks(targetId, nodes);
180
+ const recalculatedLevel = this.calculateMinLevel(targetIncoming, changes);
181
+ if (recalculatedLevel !== targetNode.level) {
182
+ changes.push({
183
+ nodeId: targetId,
184
+ level: recalculatedLevel
185
+ });
186
+ this.cascadeUpdates(targetId, recalculatedLevel, nodes, changes);
187
+ }
188
+ }
189
+ else {
190
+ // Source has a level - propose level + 1
191
+ const proposedLevel = newLevel + 1;
192
+ if (targetNode.level === undefined ||
193
+ proposedLevel < targetNode.level) {
194
+ changes.push({ nodeId: targetId, level: proposedLevel });
195
+ this.cascadeUpdates(targetId, proposedLevel, nodes, changes);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generates a UUID v4.
3
+ * Uses crypto.randomUUID() if available, otherwise falls back to a polyfill.
4
+ */
5
+ export declare function generateUuid(): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generates a UUID v4.
3
+ * Uses crypto.randomUUID() if available, otherwise falls back to a polyfill.
4
+ */
5
+ export function generateUuid() {
6
+ if (typeof crypto !== "undefined" &&
7
+ typeof crypto.randomUUID === "function") {
8
+ return crypto.randomUUID();
9
+ }
10
+ // Fallback for environments without crypto.randomUUID
11
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
12
+ const r = (Math.random() * 16) | 0;
13
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
14
+ return v.toString(16);
15
+ });
16
+ }
@@ -0,0 +1,22 @@
1
+ import { CreateDocumentInput, UpdateDocumentInput } from "../types/api-types";
2
+ /**
3
+ * Validator for document operations.
4
+ */
5
+ export declare class DocumentValidator {
6
+ /**
7
+ * Validates input for document creation.
8
+ * @param input The creation input to validate
9
+ * @throws ValidationError if validation fails
10
+ */
11
+ validateCreate(input: CreateDocumentInput): void;
12
+ /**
13
+ * Validates input for document update.
14
+ * @param input The update input to validate
15
+ * @throws ValidationError if validation fails
16
+ */
17
+ validateUpdate(input: UpdateDocumentInput): void;
18
+ /**
19
+ * Validates canvas dimensions.
20
+ */
21
+ private validateCanvasDimensions;
22
+ }
@@ -0,0 +1,68 @@
1
+ import { ValidationError } from "../errors/api-errors";
2
+ /**
3
+ * Validator for document operations.
4
+ */
5
+ export class DocumentValidator {
6
+ /**
7
+ * Validates input for document creation.
8
+ * @param input The creation input to validate
9
+ * @throws ValidationError if validation fails
10
+ */
11
+ validateCreate(input) {
12
+ if (!input.title || typeof input.title !== "string") {
13
+ throw new ValidationError("title is required", "title");
14
+ }
15
+ if (input.title.trim().length === 0) {
16
+ throw new ValidationError("title cannot be empty", "title");
17
+ }
18
+ if (input.title.length > 200) {
19
+ throw new ValidationError("title must be 200 characters or less", "title");
20
+ }
21
+ if (input.content !== undefined && typeof input.content !== "string") {
22
+ throw new ValidationError("content must be a string", "content");
23
+ }
24
+ this.validateCanvasDimensions(input.canvasWidth, input.canvasHeight);
25
+ }
26
+ /**
27
+ * Validates input for document update.
28
+ * @param input The update input to validate
29
+ * @throws ValidationError if validation fails
30
+ */
31
+ validateUpdate(input) {
32
+ if (input.title !== undefined) {
33
+ if (typeof input.title !== "string") {
34
+ throw new ValidationError("title must be a string", "title");
35
+ }
36
+ if (input.title.trim().length === 0) {
37
+ throw new ValidationError("title cannot be empty", "title");
38
+ }
39
+ if (input.title.length > 200) {
40
+ throw new ValidationError("title must be 200 characters or less", "title");
41
+ }
42
+ }
43
+ if (input.content !== undefined && typeof input.content !== "string") {
44
+ throw new ValidationError("content must be a string", "content");
45
+ }
46
+ }
47
+ /**
48
+ * Validates canvas dimensions.
49
+ */
50
+ validateCanvasDimensions(width, height) {
51
+ if (width !== undefined) {
52
+ if (typeof width !== "number" || isNaN(width)) {
53
+ throw new ValidationError("canvasWidth must be a valid number", "canvasWidth");
54
+ }
55
+ if (width < 100 || width > 10000) {
56
+ throw new ValidationError("canvasWidth must be between 100 and 10000", "canvasWidth");
57
+ }
58
+ }
59
+ if (height !== undefined) {
60
+ if (typeof height !== "number" || isNaN(height)) {
61
+ throw new ValidationError("canvasHeight must be a valid number", "canvasHeight");
62
+ }
63
+ if (height < 100 || height > 10000) {
64
+ throw new ValidationError("canvasHeight must be between 100 and 10000", "canvasHeight");
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseElementInput } from "../../types/element-input-types";
2
+ /**
3
+ * Base class for element type validators.
4
+ * Provides common validation logic for position, dimensions, and colors.
5
+ */
6
+ export declare abstract class BaseElementValidator {
7
+ /**
8
+ * Validates common element properties (position, dimensions, rotation).
9
+ * @param input The element input to validate
10
+ * @throws ValidationError if validation fails
11
+ */
12
+ protected validateCommon(input: BaseElementInput): void;
13
+ /**
14
+ * Validates position properties (x, y).
15
+ */
16
+ protected validatePosition(input: BaseElementInput): void;
17
+ /**
18
+ * Validates dimension properties (width, height).
19
+ */
20
+ protected validateDimensions(input: BaseElementInput): void;
21
+ /**
22
+ * Validates rotation property.
23
+ */
24
+ protected validateRotation(input: BaseElementInput): void;
25
+ /**
26
+ * Validates a hex color string.
27
+ * @param value The color value to validate
28
+ * @param field The field name for error messages
29
+ */
30
+ protected validateColor(value: unknown, field: string): void;
31
+ /**
32
+ * Validates stroke width.
33
+ */
34
+ protected validateStrokeWidth(value: unknown, field: string): void;
35
+ }