@graph-knowledge/api 0.3.2 → 0.9.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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/index.d.ts +3 -2
  3. package/src/lib/clients/firebase-auth-client.d.ts +7 -0
  4. package/src/lib/clients/firebase-auth-client.js +20 -2
  5. package/src/lib/clients/firebase-firestore-client.js +83 -29
  6. package/src/lib/constants/element-defaults.js +1 -0
  7. package/src/lib/graph-knowledge-api.d.ts +21 -2
  8. package/src/lib/graph-knowledge-api.js +35 -5
  9. package/src/lib/interfaces/auth-client.interface.d.ts +10 -0
  10. package/src/lib/interfaces/custom-shape-operations.interface.d.ts +82 -0
  11. package/src/lib/interfaces/custom-shape-operations.interface.js +2 -0
  12. package/src/lib/interfaces/node-operations.interface.d.ts +29 -1
  13. package/src/lib/interfaces/template-operations.interface.d.ts +22 -1
  14. package/src/lib/operations/batch-operations.d.ts +3 -15
  15. package/src/lib/operations/batch-operations.js +8 -80
  16. package/src/lib/operations/custom-shape-operations.d.ts +27 -0
  17. package/src/lib/operations/custom-shape-operations.js +175 -0
  18. package/src/lib/operations/document-operations.js +9 -2
  19. package/src/lib/operations/element-operations.d.ts +3 -22
  20. package/src/lib/operations/element-operations.js +6 -119
  21. package/src/lib/operations/node-operations.d.ts +9 -3
  22. package/src/lib/operations/node-operations.js +18 -5
  23. package/src/lib/operations/template-operations.d.ts +3 -1
  24. package/src/lib/operations/template-operations.js +50 -7
  25. package/src/lib/testing/mock-auth-client.d.ts +2 -0
  26. package/src/lib/testing/mock-auth-client.js +7 -0
  27. package/src/lib/types/api-types.d.ts +113 -2
  28. package/src/lib/types/element-input-types.d.ts +67 -11
  29. package/src/lib/utils/element-builder.d.ts +63 -0
  30. package/src/lib/utils/element-builder.js +258 -0
  31. package/src/lib/utils/rotation.d.ts +4 -0
  32. package/src/lib/utils/rotation.js +13 -0
  33. package/src/lib/validators/custom-shape-definition-validator.d.ts +17 -0
  34. package/src/lib/validators/custom-shape-definition-validator.js +135 -0
  35. package/src/lib/validators/element-type-validators/base-element-validator.d.ts +4 -0
  36. package/src/lib/validators/element-type-validators/base-element-validator.js +30 -0
  37. package/src/lib/validators/element-type-validators/basic-shape-validators.js +2 -0
  38. package/src/lib/validators/element-type-validators/bezier-curve-validator.d.ts +12 -0
  39. package/src/lib/validators/element-type-validators/bezier-curve-validator.js +47 -0
  40. package/src/lib/validators/element-type-validators/block-arrow-validator.js +2 -0
  41. package/src/lib/validators/element-type-validators/line-validator.d.ts +1 -0
  42. package/src/lib/validators/element-type-validators/line-validator.js +12 -5
  43. package/src/lib/validators/element-type-validators/rectangle-validator.js +2 -0
  44. package/src/lib/validators/element-validator-registry.js +2 -0
  45. package/src/lib/validators/template-validator.d.ts +7 -1
  46. package/src/lib/validators/template-validator.js +21 -0
@@ -1,62 +1,33 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BatchOperations = void 0;
4
- const uuid_1 = require("../utils/uuid");
5
4
  const api_errors_1 = require("../errors/api-errors");
6
- const element_defaults_1 = require("../constants/element-defaults");
5
+ const rotation_1 = require("../utils/rotation");
7
6
  /**
8
7
  * Implementation of batch element operations.
9
8
  */
10
9
  class BatchOperations {
11
10
  firestore;
12
11
  auth;
13
- validatorRegistry;
14
- constructor(firestore, auth, validatorRegistry) {
12
+ elementBuilder;
13
+ constructor(firestore, auth, elementBuilder) {
15
14
  this.firestore = firestore;
16
15
  this.auth = auth;
17
- this.validatorRegistry = validatorRegistry;
16
+ this.elementBuilder = elementBuilder;
18
17
  }
19
18
  async createElements(documentId, nodeId, inputs) {
20
19
  this.auth.requireAuth();
21
20
  if (inputs.length === 0) {
22
21
  return [];
23
22
  }
24
- // Validate ALL inputs first (before creating any)
25
- for (const input of inputs) {
26
- // Custom shapes require premium
27
- if (input.type.startsWith("custom:")) {
28
- await this.auth.requirePremium();
29
- }
30
- this.validatorRegistry.validate(input);
31
- }
23
+ // Build all elements first (validates all before writing any)
24
+ // Uses buildMany for deduped custom shape fetching
25
+ const newElements = await this.elementBuilder.buildMany(inputs);
32
26
  const nodePath = `documents/${documentId}/nodes/${nodeId}`;
33
27
  const node = await this.firestore.getDocument(nodePath);
34
28
  if (!node) {
35
29
  throw new api_errors_1.NotFoundError(`Node ${nodeId} not found`);
36
30
  }
37
- // Build all elements
38
- const newElements = inputs.map((input) => {
39
- const defaults = this.getDefaultDimensions(input.type);
40
- const x = input.x ?? 0;
41
- const y = input.y ?? 0;
42
- const element = {
43
- id: (0, uuid_1.generateUuid)(),
44
- elementType: input.type,
45
- x,
46
- y,
47
- width: input.width ?? defaults.width,
48
- height: input.height ?? defaults.height,
49
- rotation: this.normalizeRotation(input.rotation ?? 0),
50
- properties: this.buildProperties(input)
51
- };
52
- if (input.isLink !== undefined) {
53
- element.isLink = input.isLink;
54
- }
55
- if (input.linkTarget !== undefined) {
56
- element.linkTarget = input.linkTarget;
57
- }
58
- return element;
59
- });
60
31
  // Update node with all new elements atomically
61
32
  const updatedElements = [...(node.elements || []), ...newElements];
62
33
  await this.firestore.updateDocument(nodePath, {
@@ -96,7 +67,7 @@ class BatchOperations {
96
67
  width: update.width ?? element.width,
97
68
  height: update.height ?? element.height,
98
69
  rotation: update.rotation !== undefined
99
- ? this.normalizeRotation(update.rotation)
70
+ ? (0, rotation_1.normalizeRotation)(update.rotation)
100
71
  : element.rotation,
101
72
  properties: {
102
73
  ...element.properties,
@@ -138,48 +109,5 @@ class BatchOperations {
138
109
  elements: updatedElements
139
110
  });
140
111
  }
141
- /**
142
- * Gets default dimensions for an element type.
143
- */
144
- getDefaultDimensions(type) {
145
- if (type.startsWith("custom:")) {
146
- return element_defaults_1.DEFAULT_CUSTOM_SHAPE_DIMENSIONS;
147
- }
148
- return (element_defaults_1.DEFAULT_ELEMENT_DIMENSIONS[type] ?? element_defaults_1.DEFAULT_CUSTOM_SHAPE_DIMENSIONS);
149
- }
150
- /**
151
- * Normalizes rotation to integer in range [0, 360).
152
- */
153
- normalizeRotation(rotation) {
154
- let normalized = rotation % 360;
155
- if (normalized < 0) {
156
- normalized += 360;
157
- }
158
- return Math.round(normalized);
159
- }
160
- /**
161
- * Builds properties object from element input.
162
- */
163
- buildProperties(input) {
164
- const props = {
165
- __schemaVersion: 1
166
- };
167
- const commonFields = new Set([
168
- "type",
169
- "x",
170
- "y",
171
- "width",
172
- "height",
173
- "rotation",
174
- "isLink",
175
- "linkTarget"
176
- ]);
177
- for (const [key, value] of Object.entries(input)) {
178
- if (!commonFields.has(key) && value !== undefined) {
179
- props[key] = value;
180
- }
181
- }
182
- return props;
183
- }
184
112
  }
185
113
  exports.BatchOperations = BatchOperations;
@@ -0,0 +1,27 @@
1
+ import { ICustomShapeOperations } from "../interfaces/custom-shape-operations.interface";
2
+ import { IFirestoreClient } from "../interfaces/firestore-client.interface";
3
+ import { IAuthClient } from "../interfaces/auth-client.interface";
4
+ import { CustomShapeDefinitionValidator } from "../validators/custom-shape-definition-validator";
5
+ import { CreateCustomShapeInput, UpdateCustomShapeInput, CustomShapeResult } from "../types/api-types";
6
+ /**
7
+ * Implementation of custom shape operations.
8
+ *
9
+ * Custom shapes are stored in:
10
+ * - `/users/{uid}/customShapes/{shapeId}` (user drafts)
11
+ * - `/shapeStore/{shapeId}` (published shapes)
12
+ */
13
+ export declare class CustomShapeOperations implements ICustomShapeOperations {
14
+ private readonly firestore;
15
+ private readonly auth;
16
+ private readonly validator;
17
+ constructor(firestore: IFirestoreClient, auth: IAuthClient, validator: CustomShapeDefinitionValidator);
18
+ create(input: CreateCustomShapeInput): Promise<CustomShapeResult>;
19
+ list(): Promise<CustomShapeResult[]>;
20
+ get(shapeId: string): Promise<CustomShapeResult>;
21
+ update(shapeId: string, input: UpdateCustomShapeInput): Promise<void>;
22
+ delete(shapeId: string): Promise<void>;
23
+ publish(shapeId: string): Promise<void>;
24
+ unpublish(shapeId: string): Promise<void>;
25
+ listPublished(): Promise<CustomShapeResult[]>;
26
+ getPublished(shapeId: string): Promise<CustomShapeResult>;
27
+ }
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CustomShapeOperations = void 0;
4
+ const api_errors_1 = require("../errors/api-errors");
5
+ const uuid_1 = require("../utils/uuid");
6
+ // Default values
7
+ const DEFAULT_ICON = "category";
8
+ const DEFAULT_CATEGORY = "Custom";
9
+ const DEFAULT_WIDTH = 120;
10
+ const DEFAULT_HEIGHT = 120;
11
+ const CURRENT_SHAPE_SCHEMA_VERSION = 1;
12
+ /**
13
+ * Implementation of custom shape operations.
14
+ *
15
+ * Custom shapes are stored in:
16
+ * - `/users/{uid}/customShapes/{shapeId}` (user drafts)
17
+ * - `/shapeStore/{shapeId}` (published shapes)
18
+ */
19
+ class CustomShapeOperations {
20
+ firestore;
21
+ auth;
22
+ validator;
23
+ constructor(firestore, auth, validator) {
24
+ this.firestore = firestore;
25
+ this.auth = auth;
26
+ this.validator = validator;
27
+ }
28
+ async create(input) {
29
+ const userId = this.auth.requireAuth();
30
+ await this.auth.requirePremium();
31
+ this.validator.validateCreate(input);
32
+ const shapeId = (0, uuid_1.generateUuid)();
33
+ const shapeData = {
34
+ name: input.name,
35
+ icon: input.icon ?? DEFAULT_ICON,
36
+ category: input.category ?? DEFAULT_CATEGORY,
37
+ authorId: userId,
38
+ status: "draft",
39
+ createdAt: new Date(),
40
+ updatedAt: new Date(),
41
+ svgPathData: input.svgPathData,
42
+ viewBox: input.viewBox,
43
+ defaultWidth: input.defaultWidth ?? DEFAULT_WIDTH,
44
+ defaultHeight: input.defaultHeight ?? DEFAULT_HEIGHT,
45
+ defaultProperties: {
46
+ fillColor: input.defaultProperties?.fillColor ?? "#4A90D9",
47
+ strokeColor: input.defaultProperties?.strokeColor ?? "#2C3E50",
48
+ strokeWidth: input.defaultProperties?.strokeWidth ?? 2
49
+ },
50
+ schemaVersion: CURRENT_SHAPE_SCHEMA_VERSION
51
+ };
52
+ if (input.description !== undefined) {
53
+ shapeData.description = input.description;
54
+ }
55
+ if (input.aspectRatioLocked !== undefined) {
56
+ shapeData.aspectRatioLocked = input.aspectRatioLocked;
57
+ }
58
+ await this.firestore.setDocument(`users/${userId}/customShapes/${shapeId}`, shapeData);
59
+ return { id: shapeId, ...shapeData };
60
+ }
61
+ async list() {
62
+ const userId = this.auth.requireAuth();
63
+ const shapes = await this.firestore.getCollection(`users/${userId}/customShapes`);
64
+ return shapes;
65
+ }
66
+ async get(shapeId) {
67
+ const userId = this.auth.requireAuth();
68
+ const shape = await this.firestore.getDocument(`users/${userId}/customShapes/${shapeId}`);
69
+ if (!shape) {
70
+ throw new api_errors_1.NotFoundError(`Custom shape ${shapeId} not found`);
71
+ }
72
+ return shape;
73
+ }
74
+ async update(shapeId, input) {
75
+ const userId = this.auth.requireAuth();
76
+ this.validator.validateUpdate(input);
77
+ const shape = await this.firestore.getDocument(`users/${userId}/customShapes/${shapeId}`);
78
+ if (!shape) {
79
+ throw new api_errors_1.NotFoundError(`Custom shape ${shapeId} not found`);
80
+ }
81
+ const updates = {};
82
+ if (input.name !== undefined)
83
+ updates["name"] = input.name;
84
+ if (input.description !== undefined)
85
+ updates["description"] = input.description;
86
+ if (input.icon !== undefined)
87
+ updates["icon"] = input.icon;
88
+ if (input.category !== undefined)
89
+ updates["category"] = input.category;
90
+ if (input.svgPathData !== undefined)
91
+ updates["svgPathData"] = input.svgPathData;
92
+ if (input.viewBox !== undefined)
93
+ updates["viewBox"] = input.viewBox;
94
+ if (input.defaultWidth !== undefined)
95
+ updates["defaultWidth"] = input.defaultWidth;
96
+ if (input.defaultHeight !== undefined)
97
+ updates["defaultHeight"] = input.defaultHeight;
98
+ if (input.aspectRatioLocked !== undefined)
99
+ updates["aspectRatioLocked"] = input.aspectRatioLocked;
100
+ if (input.defaultProperties !== undefined) {
101
+ updates["defaultProperties"] = {
102
+ ...shape.defaultProperties,
103
+ ...input.defaultProperties
104
+ };
105
+ }
106
+ updates["updatedAt"] = new Date();
107
+ if (Object.keys(updates).length > 1) {
108
+ await this.firestore.updateDocument(`users/${userId}/customShapes/${shapeId}`, updates);
109
+ }
110
+ }
111
+ async delete(shapeId) {
112
+ const userId = this.auth.requireAuth();
113
+ const shape = await this.firestore.getDocument(`users/${userId}/customShapes/${shapeId}`);
114
+ if (!shape) {
115
+ throw new api_errors_1.NotFoundError(`Custom shape ${shapeId} not found`);
116
+ }
117
+ await this.firestore.deleteDocument(`users/${userId}/customShapes/${shapeId}`);
118
+ }
119
+ async publish(shapeId) {
120
+ const userId = this.auth.requireAuth();
121
+ await this.auth.requirePremium();
122
+ const shape = await this.firestore.getDocument(`users/${userId}/customShapes/${shapeId}`);
123
+ if (!shape) {
124
+ throw new api_errors_1.NotFoundError(`Custom shape ${shapeId} not found`);
125
+ }
126
+ const now = new Date();
127
+ // Copy to public store
128
+ const publishedData = {
129
+ ...shape,
130
+ status: "published",
131
+ publishedAt: now,
132
+ updatedAt: now
133
+ };
134
+ delete publishedData.id;
135
+ // Atomic: write to store + update draft status
136
+ const batch = this.firestore.batch();
137
+ batch.set(`shapeStore/${shapeId}`, publishedData);
138
+ batch.update(`users/${userId}/customShapes/${shapeId}`, {
139
+ status: "published",
140
+ publishedAt: now,
141
+ updatedAt: now
142
+ });
143
+ await batch.commit();
144
+ }
145
+ async unpublish(shapeId) {
146
+ const userId = this.auth.requireAuth();
147
+ const shape = await this.firestore.getDocument(`users/${userId}/customShapes/${shapeId}`);
148
+ if (!shape) {
149
+ throw new api_errors_1.NotFoundError(`Custom shape ${shapeId} not found`);
150
+ }
151
+ // Atomic: delete from store + revert draft status
152
+ const batch = this.firestore.batch();
153
+ batch.delete(`shapeStore/${shapeId}`);
154
+ batch.update(`users/${userId}/customShapes/${shapeId}`, {
155
+ status: "draft",
156
+ publishedAt: null,
157
+ updatedAt: new Date()
158
+ });
159
+ await batch.commit();
160
+ }
161
+ async listPublished() {
162
+ this.auth.requireAuth();
163
+ const shapes = await this.firestore.queryCollection("shapeStore", "status", "published");
164
+ return shapes;
165
+ }
166
+ async getPublished(shapeId) {
167
+ this.auth.requireAuth();
168
+ const shape = await this.firestore.getDocument(`shapeStore/${shapeId}`);
169
+ if (!shape) {
170
+ throw new api_errors_1.NotFoundError(`Published shape ${shapeId} not found in store`);
171
+ }
172
+ return shape;
173
+ }
174
+ }
175
+ exports.CustomShapeOperations = CustomShapeOperations;
@@ -62,18 +62,25 @@ class DocumentOperations {
62
62
  throw new api_errors_1.NotFoundError(`Document ${documentId} not found`);
63
63
  }
64
64
  const nodes = await this.firestore.getCollection(`documents/${documentId}/nodes`);
65
+ // Ensure rootNodeId is present (legacy documents may lack it)
66
+ const rootNodeId = docData.rootNodeId ??
67
+ nodes.find((n) => n.level === 0)?.id ??
68
+ "";
65
69
  return {
66
70
  ...docData,
67
- nodes
71
+ nodes,
72
+ rootNodeId
68
73
  };
69
74
  }
70
75
  async list() {
71
76
  const userId = this.auth.requireAuth();
72
77
  const documents = await this.firestore.queryCollection("documents", "owner", userId);
73
78
  // Return shallow documents without nodes
79
+ // Ensure rootNodeId is present (legacy documents may lack it)
74
80
  return documents.map((doc) => ({
75
81
  ...doc,
76
- nodes: []
82
+ nodes: [],
83
+ rootNodeId: doc.rootNodeId ?? ""
77
84
  }));
78
85
  }
79
86
  async update(documentId, input) {
@@ -2,38 +2,19 @@ import { GraphElement } from "../models";
2
2
  import { IElementOperations } from "../interfaces/element-operations.interface";
3
3
  import { IFirestoreClient } from "../interfaces/firestore-client.interface";
4
4
  import { IAuthClient } from "../interfaces/auth-client.interface";
5
- import { IElementValidatorRegistry } from "../interfaces/element-validator.interface";
6
5
  import { AnyElementInput, UpdateElementInput } from "../types/element-input-types";
6
+ import { ElementBuilder } from "../utils/element-builder";
7
7
  /**
8
8
  * Implementation of element CRUD operations.
9
9
  */
10
10
  export declare class ElementOperations implements IElementOperations {
11
11
  private readonly firestore;
12
12
  private readonly auth;
13
- private readonly validatorRegistry;
14
- constructor(firestore: IFirestoreClient, auth: IAuthClient, validatorRegistry: IElementValidatorRegistry);
13
+ private readonly elementBuilder;
14
+ constructor(firestore: IFirestoreClient, auth: IAuthClient, elementBuilder: ElementBuilder);
15
15
  get(documentId: string, nodeId: string, elementId: string): Promise<GraphElement>;
16
16
  list(documentId: string, nodeId: string): Promise<GraphElement[]>;
17
17
  create(documentId: string, nodeId: string, input: AnyElementInput): Promise<GraphElement>;
18
18
  update(documentId: string, nodeId: string, elementId: string, input: UpdateElementInput): Promise<void>;
19
19
  delete(documentId: string, nodeId: string, elementId: string): Promise<void>;
20
- /**
21
- * Gets default dimensions for an element type.
22
- */
23
- private getDefaultDimensions;
24
- /**
25
- * Normalizes rotation to integer in range [0, 360).
26
- */
27
- private normalizeRotation;
28
- /**
29
- * Auto-measures text width for text elements created without explicit width/maxWidth.
30
- * Uses a lazily-initialized TextMeasurementService singleton.
31
- */
32
- private measureTextWidth;
33
- private static _textMeasurementService;
34
- /**
35
- * Builds properties object from element input.
36
- * Extracts type-specific properties, excluding common fields.
37
- */
38
- private buildProperties;
39
20
  }
@@ -1,21 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ElementOperations = void 0;
4
- const uuid_1 = require("../utils/uuid");
5
4
  const api_errors_1 = require("../errors/api-errors");
6
- const element_defaults_1 = require("../constants/element-defaults");
7
- const text_measurement_service_1 = require("../utils/text-measurement/text-measurement-service");
5
+ const rotation_1 = require("../utils/rotation");
8
6
  /**
9
7
  * Implementation of element CRUD operations.
10
8
  */
11
9
  class ElementOperations {
12
10
  firestore;
13
11
  auth;
14
- validatorRegistry;
15
- constructor(firestore, auth, validatorRegistry) {
12
+ elementBuilder;
13
+ constructor(firestore, auth, elementBuilder) {
16
14
  this.firestore = firestore;
17
15
  this.auth = auth;
18
- this.validatorRegistry = validatorRegistry;
16
+ this.elementBuilder = elementBuilder;
19
17
  }
20
18
  async get(documentId, nodeId, elementId) {
21
19
  this.auth.requireAuth();
@@ -41,50 +39,12 @@ class ElementOperations {
41
39
  }
42
40
  async create(documentId, nodeId, input) {
43
41
  this.auth.requireAuth();
44
- // Custom shapes require premium
45
- if (input.type.startsWith("custom:")) {
46
- await this.auth.requirePremium();
47
- }
48
- this.validatorRegistry.validate(input);
49
42
  const nodePath = `documents/${documentId}/nodes/${nodeId}`;
50
43
  const node = await this.firestore.getDocument(nodePath);
51
44
  if (!node) {
52
45
  throw new api_errors_1.NotFoundError(`Node ${nodeId} not found`);
53
46
  }
54
- const defaults = this.getDefaultDimensions(input.type);
55
- // Connectors have optional x/y since their position is derived from connected elements
56
- const x = input.x ?? 0;
57
- const y = input.y ?? 0;
58
- // For text elements, maxWidth is a convenience alias for width (the wrap boundary)
59
- let resolvedWidth = input.width;
60
- if (input.type === "text" && resolvedWidth === undefined) {
61
- const textInput = input;
62
- if (textInput.maxWidth !== undefined) {
63
- resolvedWidth = textInput.maxWidth;
64
- }
65
- else {
66
- // Auto-measure text width to avoid the 100px default which causes
67
- // unexpected wrapping in the editor.
68
- resolvedWidth = this.measureTextWidth(textInput);
69
- }
70
- }
71
- const element = {
72
- id: (0, uuid_1.generateUuid)(),
73
- elementType: input.type,
74
- x,
75
- y,
76
- width: resolvedWidth ?? defaults.width,
77
- height: input.height ?? defaults.height,
78
- rotation: this.normalizeRotation(input.rotation ?? 0),
79
- properties: this.buildProperties(input)
80
- };
81
- // Only add link properties if they have values (Firestore rejects undefined)
82
- if (input.isLink !== undefined) {
83
- element.isLink = input.isLink;
84
- }
85
- if (input.linkTarget !== undefined) {
86
- element.linkTarget = input.linkTarget;
87
- }
47
+ const element = await this.elementBuilder.build(input);
88
48
  const updatedElements = [...(node.elements || []), element];
89
49
  await this.firestore.updateDocument(nodePath, {
90
50
  elements: updatedElements
@@ -110,7 +70,7 @@ class ElementOperations {
110
70
  width: input.width ?? existingElement.width,
111
71
  height: input.height ?? existingElement.height,
112
72
  rotation: input.rotation !== undefined
113
- ? this.normalizeRotation(input.rotation)
73
+ ? (0, rotation_1.normalizeRotation)(input.rotation)
114
74
  : existingElement.rotation,
115
75
  properties: {
116
76
  ...existingElement.properties,
@@ -144,78 +104,5 @@ class ElementOperations {
144
104
  elements: updatedElements
145
105
  });
146
106
  }
147
- /**
148
- * Gets default dimensions for an element type.
149
- */
150
- getDefaultDimensions(type) {
151
- if (type.startsWith("custom:")) {
152
- return element_defaults_1.DEFAULT_CUSTOM_SHAPE_DIMENSIONS;
153
- }
154
- return (element_defaults_1.DEFAULT_ELEMENT_DIMENSIONS[type] ?? element_defaults_1.DEFAULT_CUSTOM_SHAPE_DIMENSIONS);
155
- }
156
- /**
157
- * Normalizes rotation to integer in range [0, 360).
158
- */
159
- normalizeRotation(rotation) {
160
- let normalized = rotation % 360;
161
- if (normalized < 0) {
162
- normalized += 360;
163
- }
164
- return Math.round(normalized);
165
- }
166
- /**
167
- * Auto-measures text width for text elements created without explicit width/maxWidth.
168
- * Uses a lazily-initialized TextMeasurementService singleton.
169
- */
170
- measureTextWidth(input) {
171
- if (!ElementOperations._textMeasurementService) {
172
- ElementOperations._textMeasurementService = new text_measurement_service_1.TextMeasurementService();
173
- }
174
- const metrics = ElementOperations._textMeasurementService.measureText(input.text, {
175
- fontSize: input.fontSize,
176
- fontFamily: input.fontFamily,
177
- fontWeight: input.fontWeight,
178
- textAlign: input.textAlign,
179
- lineHeight: input.lineHeight
180
- });
181
- return Math.ceil(metrics.width);
182
- }
183
- static _textMeasurementService = null;
184
- /**
185
- * Builds properties object from element input.
186
- * Extracts type-specific properties, excluding common fields.
187
- */
188
- buildProperties(input) {
189
- const props = {
190
- __schemaVersion: 1
191
- };
192
- // Exclude common fields and convenience aliases, copy the rest as properties
193
- const commonFields = new Set([
194
- "type",
195
- "x",
196
- "y",
197
- "width",
198
- "height",
199
- "rotation",
200
- "isLink",
201
- "linkTarget",
202
- "maxWidth"
203
- ]);
204
- for (const [key, value] of Object.entries(input)) {
205
- if (!commonFields.has(key) && value !== undefined) {
206
- props[key] = value;
207
- }
208
- }
209
- // For text elements, set __usesTopLeft: true so the app's normalization
210
- // knows that x/y is the bounding box top-left (not the text anchor point).
211
- // Without this, the app would incorrectly adjust x/y based on textAlign,
212
- // causing centered text to be positioned in negative coordinates.
213
- // Since we now always resolve a proper width for text (via explicit width,
214
- // maxWidth, or auto-measurement), we always set this flag.
215
- if (input.type === "text") {
216
- props["__usesTopLeft"] = true;
217
- }
218
- return props;
219
- }
220
107
  }
221
108
  exports.ElementOperations = ElementOperations;
@@ -3,7 +3,8 @@ import { INodeOperations } from "../interfaces/node-operations.interface";
3
3
  import { IFirestoreClient } from "../interfaces/firestore-client.interface";
4
4
  import { IAuthClient } from "../interfaces/auth-client.interface";
5
5
  import { NodeValidator } from "../validators/node-validator";
6
- import { CreateNodeInput, UpdateNodeInput } from "../types/api-types";
6
+ import { CreateNodeInput, UpdateNodeInput, CreateNodeWithElementsInput, CreateNodeWithElementsResult } from "../types/api-types";
7
+ import { ElementBuilder } from "../utils/element-builder";
7
8
  /**
8
9
  * Implementation of node CRUD operations.
9
10
  */
@@ -11,10 +12,15 @@ export declare class NodeOperations implements INodeOperations {
11
12
  private readonly firestore;
12
13
  private readonly auth;
13
14
  private readonly validator;
14
- constructor(firestore: IFirestoreClient, auth: IAuthClient, validator: NodeValidator);
15
+ private readonly elementBuilder;
16
+ constructor(firestore: IFirestoreClient, auth: IAuthClient, validator: NodeValidator, elementBuilder: ElementBuilder);
15
17
  create(documentId: string, input: CreateNodeInput): Promise<GraphNode>;
16
18
  createWithId(documentId: string, nodeId: string, input: CreateNodeInput): Promise<GraphNode>;
17
- private createNodeWithId;
19
+ createWithElements(documentId: string, input: CreateNodeWithElementsInput): Promise<CreateNodeWithElementsResult>;
20
+ /**
21
+ * Shared node building and persistence logic.
22
+ */
23
+ private _buildAndWrite;
18
24
  get(documentId: string, nodeId: string): Promise<GraphNode>;
19
25
  list(documentId: string): Promise<GraphNode[]>;
20
26
  update(documentId: string, nodeId: string, input: UpdateNodeInput): Promise<void>;
@@ -13,19 +13,32 @@ class NodeOperations {
13
13
  firestore;
14
14
  auth;
15
15
  validator;
16
- constructor(firestore, auth, validator) {
16
+ elementBuilder;
17
+ constructor(firestore, auth, validator, elementBuilder) {
17
18
  this.firestore = firestore;
18
19
  this.auth = auth;
19
20
  this.validator = validator;
21
+ this.elementBuilder = elementBuilder;
20
22
  }
21
23
  async create(documentId, input) {
22
24
  const nodeId = (0, uuid_1.generateUuid)();
23
- return this.createNodeWithId(documentId, nodeId, input);
25
+ return this._buildAndWrite(documentId, nodeId, input, []);
24
26
  }
25
27
  async createWithId(documentId, nodeId, input) {
26
- return this.createNodeWithId(documentId, nodeId, input);
28
+ return this._buildAndWrite(documentId, nodeId, input, []);
27
29
  }
28
- async createNodeWithId(documentId, nodeId, input) {
30
+ async createWithElements(documentId, input) {
31
+ // Build all elements first (validates all before creating anything)
32
+ // Uses buildMany for deduped custom shape fetching
33
+ const elements = await this.elementBuilder.buildMany(input.elements);
34
+ const nodeId = (0, uuid_1.generateUuid)();
35
+ const node = await this._buildAndWrite(documentId, nodeId, input, elements);
36
+ return { node, elements };
37
+ }
38
+ /**
39
+ * Shared node building and persistence logic.
40
+ */
41
+ async _buildAndWrite(documentId, nodeId, input, elements) {
29
42
  const userId = this.auth.requireAuth();
30
43
  this.validator.validateCreate(input);
31
44
  // Determine level from parent if provided
@@ -40,7 +53,7 @@ class NodeOperations {
40
53
  id: nodeId,
41
54
  title: input.title,
42
55
  content: input.content ?? "",
43
- elements: [],
56
+ elements,
44
57
  canvasWidth: input.canvasWidth ?? DEFAULT_CANVAS_WIDTH,
45
58
  canvasHeight: input.canvasHeight ?? DEFAULT_CANVAS_HEIGHT,
46
59
  level,
@@ -1,7 +1,7 @@
1
1
  import { ITemplateOperations } from "../interfaces/template-operations.interface";
2
2
  import { IFirestoreClient } from "../interfaces/firestore-client.interface";
3
3
  import { IAuthClient } from "../interfaces/auth-client.interface";
4
- import { CreateTemplateInput, DocumentResult } from "../types/api-types";
4
+ import { CreateTemplateInput, UpdateTemplateInput, DocumentResult } from "../types/api-types";
5
5
  import { TemplateValidator } from "../validators/template-validator";
6
6
  /**
7
7
  * Implementation of template operations.
@@ -17,6 +17,8 @@ export declare class TemplateOperations implements ITemplateOperations {
17
17
  create(input: CreateTemplateInput): Promise<DocumentResult>;
18
18
  list(): Promise<DocumentResult[]>;
19
19
  get(templateId: string): Promise<DocumentResult>;
20
+ update(templateId: string, input: UpdateTemplateInput): Promise<void>;
21
+ delete(templateId: string): Promise<void>;
20
22
  clone(templateId: string, title?: string): Promise<DocumentResult>;
21
23
  publish(templateId: string): Promise<void>;
22
24
  unpublish(templateId: string): Promise<void>;