@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.
- package/README.md +396 -0
- package/package.json +44 -0
- package/src/index.d.ts +18 -0
- package/src/index.js +19 -0
- package/src/lib/clients/firebase-auth-client.d.ts +24 -0
- package/src/lib/clients/firebase-auth-client.js +76 -0
- package/src/lib/clients/firebase-firestore-client.d.ts +22 -0
- package/src/lib/clients/firebase-firestore-client.js +79 -0
- package/src/lib/config/api-config.d.ts +20 -0
- package/src/lib/config/api-config.js +0 -0
- package/src/lib/constants/element-defaults.d.ts +15 -0
- package/src/lib/constants/element-defaults.js +19 -0
- package/src/lib/errors/api-errors.d.ts +33 -0
- package/src/lib/errors/api-errors.js +51 -0
- package/src/lib/graph-knowledge-api.d.ts +106 -0
- package/src/lib/graph-knowledge-api.js +154 -0
- package/src/lib/interfaces/auth-client.interface.d.ts +35 -0
- package/src/lib/interfaces/auth-client.interface.js +0 -0
- package/src/lib/interfaces/batch-operations.interface.d.ts +46 -0
- package/src/lib/interfaces/batch-operations.interface.js +0 -0
- package/src/lib/interfaces/document-operations.interface.d.ts +51 -0
- package/src/lib/interfaces/document-operations.interface.js +0 -0
- package/src/lib/interfaces/element-operations.interface.d.ts +57 -0
- package/src/lib/interfaces/element-operations.interface.js +0 -0
- package/src/lib/interfaces/element-validator.interface.d.ts +42 -0
- package/src/lib/interfaces/element-validator.interface.js +0 -0
- package/src/lib/interfaces/firestore-client.interface.d.ts +62 -0
- package/src/lib/interfaces/firestore-client.interface.js +0 -0
- package/src/lib/interfaces/node-operations.interface.d.ts +62 -0
- package/src/lib/interfaces/node-operations.interface.js +0 -0
- package/src/lib/models/document.model.d.ts +73 -0
- package/src/lib/models/document.model.js +13 -0
- package/src/lib/models/index.d.ts +6 -0
- package/src/lib/models/index.js +5 -0
- package/src/lib/operations/batch-operations.d.ts +30 -0
- package/src/lib/operations/batch-operations.js +181 -0
- package/src/lib/operations/document-operations.d.ts +20 -0
- package/src/lib/operations/document-operations.js +108 -0
- package/src/lib/operations/element-operations.d.ts +33 -0
- package/src/lib/operations/element-operations.js +175 -0
- package/src/lib/operations/node-operations.d.ts +22 -0
- package/src/lib/operations/node-operations.js +89 -0
- package/src/lib/testing/index.d.ts +2 -0
- package/src/lib/testing/index.js +3 -0
- package/src/lib/testing/mock-auth-client.d.ts +26 -0
- package/src/lib/testing/mock-auth-client.js +49 -0
- package/src/lib/testing/mock-firestore-client.d.ts +32 -0
- package/src/lib/testing/mock-firestore-client.js +126 -0
- package/src/lib/types/api-types.d.ts +61 -0
- package/src/lib/types/api-types.js +0 -0
- package/src/lib/types/element-input-types.d.ts +237 -0
- package/src/lib/types/element-input-types.js +3 -0
- package/src/lib/utils/link-level-manager.d.ts +66 -0
- package/src/lib/utils/link-level-manager.js +200 -0
- package/src/lib/utils/uuid.d.ts +5 -0
- package/src/lib/utils/uuid.js +16 -0
- package/src/lib/validators/document-validator.d.ts +22 -0
- package/src/lib/validators/document-validator.js +68 -0
- package/src/lib/validators/element-type-validators/base-element-validator.d.ts +35 -0
- package/src/lib/validators/element-type-validators/base-element-validator.js +96 -0
- package/src/lib/validators/element-type-validators/connector-validator.d.ts +13 -0
- package/src/lib/validators/element-type-validators/connector-validator.js +66 -0
- package/src/lib/validators/element-type-validators/custom-shape-validator.d.ts +22 -0
- package/src/lib/validators/element-type-validators/custom-shape-validator.js +44 -0
- package/src/lib/validators/element-type-validators/rectangle-validator.d.ts +11 -0
- package/src/lib/validators/element-type-validators/rectangle-validator.js +31 -0
- package/src/lib/validators/element-type-validators/text-validator.d.ts +14 -0
- package/src/lib/validators/element-type-validators/text-validator.js +66 -0
- package/src/lib/validators/element-type-validators/uml-validators.d.ts +58 -0
- package/src/lib/validators/element-type-validators/uml-validators.js +123 -0
- package/src/lib/validators/element-validator-registry.d.ts +18 -0
- package/src/lib/validators/element-validator-registry.js +58 -0
- package/src/lib/validators/node-validator.d.ts +26 -0
- 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,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,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
|
+
}
|