@graph-knowledge/api 0.1.3 → 0.1.5

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 CHANGED
@@ -101,6 +101,7 @@ new GraphKnowledgeAPI(config: ApiConfig)
101
101
  | `nodes` | `INodeOperations` | Node CRUD operations |
102
102
  | `elements` | `IElementOperations` | Element CRUD operations |
103
103
  | `batch` | `IBatchOperations` | Batch element operations |
104
+ | `templates` | `ITemplateOperations` | Template operations (list, get, clone) |
104
105
  | `authClient` | `IAuthClient` | Authentication client |
105
106
 
106
107
  ### Document Operations
@@ -194,6 +195,17 @@ await api.elements.create(documentId, nodeId, {
194
195
  textAlign: "center"
195
196
  });
196
197
 
198
+ // Create multi-line text (use \n for line breaks)
199
+ await api.elements.create(documentId, nodeId, {
200
+ type: "text",
201
+ x: 100,
202
+ y: 200,
203
+ text: "Line 1\nLine 2\nLine 3",
204
+ fontSize: 18,
205
+ lineHeight: 1.4 // Optional: adjust line spacing (default: 1.2)
206
+ });
207
+ // Note: height is auto-calculated based on the number of lines and lineHeight
208
+
197
209
  // Create a connector
198
210
  await api.elements.create(documentId, nodeId, {
199
211
  type: "connector",
@@ -232,6 +244,35 @@ await api.elements.update(documentId, nodeId, elementId, {
232
244
  await api.elements.delete(documentId, nodeId, elementId);
233
245
  ```
234
246
 
247
+ ### Template Operations
248
+
249
+ Templates are pre-made documents that can be cloned by premium users:
250
+
251
+ ```typescript
252
+ // List all available templates
253
+ const templates = await api.templates.list();
254
+ console.log(templates);
255
+ // [{ id: "template-1", title: "UML Class Diagram", isTemplate: true, ... }]
256
+
257
+ // Get a specific template with all its nodes and elements
258
+ const template = await api.templates.get("template-1");
259
+
260
+ // Clone a template (creates a new document) - requires premium
261
+ const myDoc = await api.templates.clone("template-1");
262
+ // Creates "Copy of UML Class Diagram" document
263
+
264
+ // Clone with custom title
265
+ const myDoc2 = await api.templates.clone("template-1", "My Project Diagram");
266
+ // Creates "My Project Diagram" document
267
+
268
+ // The cloned document:
269
+ // - Has a new unique ID
270
+ // - Is owned by the current user
271
+ // - Has isTemplate: false
272
+ // - Has clonedFromTemplateId pointing to the source template
273
+ // - Contains copies of all nodes and elements with new IDs
274
+ ```
275
+
235
276
  ### Batch Operations
236
277
 
237
278
  For efficient bulk operations (ideal for AI agents and automation):
@@ -263,7 +304,7 @@ await api.batch.deleteElements(documentId, nodeId, [
263
304
  | Type | Description |
264
305
  |------|-------------|
265
306
  | `rectangle` | Rectangle shape with fill, stroke, corner radius |
266
- | `text` | Text element with font customization |
307
+ | `text` | Text element with font customization (supports multi-line with `\n`) |
267
308
  | `connector` | Line connecting two elements |
268
309
  | `uml-class` | UML class diagram element |
269
310
  | `uml-interface` | UML interface element |
@@ -368,6 +409,7 @@ GraphKnowledgeAPI (Composition Root)
368
409
  ├── NodeOperations : INodeOperations
369
410
  ├── ElementOperations : IElementOperations
370
411
  ├── BatchOperations : IBatchOperations
412
+ ├── TemplateOperations : ITemplateOperations
371
413
  └── ElementValidatorRegistry : IElementValidatorRegistry
372
414
  ├── RectangleValidator
373
415
  ├── TextValidator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graph-knowledge/api",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Headless Document API for Graph Knowledge - programmatic access to documents, nodes, and elements",
5
5
  "license": "MIT",
6
6
  "author": "Graph Knowledge Team",
package/src/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export type { IDocumentOperations } from "./lib/interfaces/document-operations.i
6
6
  export type { INodeOperations } from "./lib/interfaces/node-operations.interface";
7
7
  export type { IElementOperations } from "./lib/interfaces/element-operations.interface";
8
8
  export type { IBatchOperations, BatchUpdateElementInput } from "./lib/interfaces/batch-operations.interface";
9
+ export type { ITemplateOperations } from "./lib/interfaces/template-operations.interface";
9
10
  export type { IElementTypeValidator, IElementValidatorRegistry } from "./lib/interfaces/element-validator.interface";
10
11
  export type { CreateDocumentInput, UpdateDocumentInput, DocumentResult, CreateNodeInput, UpdateNodeInput, NodeResult } from "./lib/types/api-types";
11
12
  export type { BaseElementInput, RectangleInput, TextInput, ConnectorInput, ConnectorAnchor, LineStyle, MarkerType, UmlClassInput, UmlInterfaceInput, UmlComponentInput, UmlPackageInput, UmlArtifactInput, UmlNoteInput, CustomShapeInput, AnyElementInput, UpdateElementInput } from "./lib/types/element-input-types";
@@ -21,4 +21,6 @@ export declare class FirebaseAuthClient implements IAuthClient {
21
21
  requireAuth(): string;
22
22
  isPremium(): Promise<boolean>;
23
23
  requirePremium(): Promise<void>;
24
+ isAdmin(): Promise<boolean>;
25
+ requireAdmin(): Promise<void>;
24
26
  }
@@ -76,5 +76,25 @@ class FirebaseAuthClient {
76
76
  throw new api_errors_1.PermissionError("Premium subscription required");
77
77
  }
78
78
  }
79
+ async isAdmin() {
80
+ if (!this._currentUser) {
81
+ return false;
82
+ }
83
+ // Read admin status from user profile in Firestore
84
+ const userDoc = (0, firestore_1.doc)(this.firestore, `users/${this._currentUser.uid}`);
85
+ const snapshot = await (0, firestore_1.getDoc)(userDoc);
86
+ if (!snapshot.exists()) {
87
+ return false;
88
+ }
89
+ const profile = snapshot.data();
90
+ return profile?.["isAdmin"] === true;
91
+ }
92
+ async requireAdmin() {
93
+ this.requireAuth();
94
+ const admin = await this.isAdmin();
95
+ if (!admin) {
96
+ throw new api_errors_1.PermissionError("Admin access required");
97
+ }
98
+ }
79
99
  }
80
100
  exports.FirebaseAuthClient = FirebaseAuthClient;
@@ -15,7 +15,7 @@ export declare class FirebaseFirestoreClient implements IFirestoreClient {
15
15
  getCollection<T>(path: string): Promise<(T & {
16
16
  id: string;
17
17
  })[]>;
18
- queryCollection<T>(path: string, field: string, value: string): Promise<(T & {
18
+ queryCollection<T>(path: string, field: string, value: string | number | boolean | null): Promise<(T & {
19
19
  id: string;
20
20
  })[]>;
21
21
  batch(): IBatchWriter;
@@ -4,6 +4,7 @@ import { IDocumentOperations } from "./interfaces/document-operations.interface"
4
4
  import { INodeOperations } from "./interfaces/node-operations.interface";
5
5
  import { IElementOperations } from "./interfaces/element-operations.interface";
6
6
  import { IBatchOperations } from "./interfaces/batch-operations.interface";
7
+ import { ITemplateOperations } from "./interfaces/template-operations.interface";
7
8
  /**
8
9
  * Main entry point for the Graph Knowledge Headless API.
9
10
  *
@@ -48,6 +49,7 @@ export declare class GraphKnowledgeAPI {
48
49
  private readonly _nodes;
49
50
  private readonly _elements;
50
51
  private readonly _batch;
52
+ private readonly _templates;
51
53
  /**
52
54
  * Creates a new GraphKnowledgeAPI instance.
53
55
  * @param config Configuration including Firebase credentials
@@ -73,6 +75,12 @@ export declare class GraphKnowledgeAPI {
73
75
  * Batch element operations (createElements, updateElements, deleteElements).
74
76
  */
75
77
  get batch(): IBatchOperations;
78
+ /**
79
+ * Template operations (list, get, clone, publish, unpublish).
80
+ * Templates are documents that can be cloned by premium users.
81
+ * Admins can publish/unpublish templates to control visibility.
82
+ */
83
+ get templates(): ITemplateOperations;
76
84
  /**
77
85
  * Signs in with email and password.
78
86
  * @param email User email
@@ -10,6 +10,7 @@ const document_operations_1 = require("./operations/document-operations");
10
10
  const node_operations_1 = require("./operations/node-operations");
11
11
  const element_operations_1 = require("./operations/element-operations");
12
12
  const batch_operations_1 = require("./operations/batch-operations");
13
+ const template_operations_1 = require("./operations/template-operations");
13
14
  const document_validator_1 = require("./validators/document-validator");
14
15
  const node_validator_1 = require("./validators/node-validator");
15
16
  const element_validator_registry_1 = require("./validators/element-validator-registry");
@@ -57,6 +58,7 @@ class GraphKnowledgeAPI {
57
58
  _nodes;
58
59
  _elements;
59
60
  _batch;
61
+ _templates;
60
62
  /**
61
63
  * Creates a new GraphKnowledgeAPI instance.
62
64
  * @param config Configuration including Firebase credentials
@@ -78,6 +80,7 @@ class GraphKnowledgeAPI {
78
80
  this._nodes = new node_operations_1.NodeOperations(firestoreClient, this._authClient, nodeValidator);
79
81
  this._elements = new element_operations_1.ElementOperations(firestoreClient, this._authClient, elementValidatorRegistry);
80
82
  this._batch = new batch_operations_1.BatchOperations(firestoreClient, this._authClient, elementValidatorRegistry);
83
+ this._templates = new template_operations_1.TemplateOperations(firestoreClient, this._authClient);
81
84
  }
82
85
  /**
83
86
  * Authentication client for sign-in/sign-out operations.
@@ -109,6 +112,14 @@ class GraphKnowledgeAPI {
109
112
  get batch() {
110
113
  return this._batch;
111
114
  }
115
+ /**
116
+ * Template operations (list, get, clone, publish, unpublish).
117
+ * Templates are documents that can be cloned by premium users.
118
+ * Admins can publish/unpublish templates to control visibility.
119
+ */
120
+ get templates() {
121
+ return this._templates;
122
+ }
112
123
  // =========================================================================
113
124
  // Convenience methods delegating to authClient
114
125
  // =========================================================================
@@ -32,4 +32,15 @@ export interface IAuthClient {
32
32
  * @throws PermissionError if not premium
33
33
  */
34
34
  requirePremium(): Promise<void>;
35
+ /**
36
+ * Checks if the current user has admin status.
37
+ * @returns Promise resolving to true if user is admin
38
+ */
39
+ isAdmin(): Promise<boolean>;
40
+ /**
41
+ * Requires the current user to have admin status.
42
+ * @throws AuthenticationError if not authenticated
43
+ * @throws PermissionError if not admin
44
+ */
45
+ requireAdmin(): Promise<void>;
35
46
  }
@@ -51,8 +51,9 @@ export interface IFirestoreClient {
51
51
  })[]>;
52
52
  /**
53
53
  * Queries documents in a collection where a field equals a value.
54
+ * Supports string, number, boolean, and null values.
54
55
  */
55
- queryCollection<T>(path: string, field: string, value: string): Promise<(T & {
56
+ queryCollection<T>(path: string, field: string, value: string | number | boolean | null): Promise<(T & {
56
57
  id: string;
57
58
  })[]>;
58
59
  /**
@@ -0,0 +1,55 @@
1
+ import { DocumentResult } from "../types/api-types";
2
+ /**
3
+ * Interface for template operations.
4
+ *
5
+ * Templates are documents with `isTemplate: true` that can be cloned
6
+ * by premium users to create new documents.
7
+ */
8
+ export interface ITemplateOperations {
9
+ /**
10
+ * Lists all published templates available to users.
11
+ * @returns Array of published template documents (shallow, without nodes)
12
+ * @throws AuthenticationError if not authenticated
13
+ */
14
+ list(): Promise<DocumentResult[]>;
15
+ /**
16
+ * Gets a single template by ID, including all its nodes.
17
+ * @param templateId The template document ID
18
+ * @returns The template with all nodes
19
+ * @throws AuthenticationError if not authenticated
20
+ * @throws NotFoundError if template doesn't exist
21
+ */
22
+ get(templateId: string): Promise<DocumentResult>;
23
+ /**
24
+ * Clones a template into a new document owned by the current user.
25
+ * Requires premium access.
26
+ *
27
+ * @param templateId The template to clone
28
+ * @param title Optional custom title (defaults to "Copy of {template.title}")
29
+ * @returns The newly created document
30
+ * @throws AuthenticationError if not authenticated
31
+ * @throws PermissionError if not premium user
32
+ * @throws NotFoundError if template doesn't exist
33
+ */
34
+ clone(templateId: string, title?: string): Promise<DocumentResult>;
35
+ /**
36
+ * Publishes a template, making it visible to all users.
37
+ * Requires admin access.
38
+ *
39
+ * @param templateId The template ID to publish
40
+ * @throws AuthenticationError if not authenticated
41
+ * @throws PermissionError if not admin
42
+ * @throws NotFoundError if template doesn't exist
43
+ */
44
+ publish(templateId: string): Promise<void>;
45
+ /**
46
+ * Unpublishes a template, hiding it from regular users.
47
+ * Requires admin access.
48
+ *
49
+ * @param templateId The template ID to unpublish
50
+ * @throws AuthenticationError if not authenticated
51
+ * @throws PermissionError if not admin
52
+ * @throws NotFoundError if template doesn't exist
53
+ */
54
+ unpublish(templateId: string): Promise<void>;
55
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -42,6 +42,21 @@ export interface Document {
42
42
  nodes?: GraphNode[];
43
43
  rootNodeId?: string;
44
44
  isPremiumContent?: boolean;
45
+ /**
46
+ * Template flag - only admins can set this to true.
47
+ * Templates are visible to all authenticated users.
48
+ */
49
+ isTemplate?: boolean;
50
+ /**
51
+ * Whether the template is published and visible to users (templates only).
52
+ * Unpublished templates are only visible to admins.
53
+ */
54
+ isPublished?: boolean;
55
+ /**
56
+ * Source template ID if this document was cloned from a template.
57
+ * Provides audit trail for template usage.
58
+ */
59
+ clonedFromTemplateId?: string;
45
60
  }
46
61
  export interface GraphNode {
47
62
  id: string;
@@ -0,0 +1,30 @@
1
+ import { ITemplateOperations } from "../interfaces/template-operations.interface";
2
+ import { IFirestoreClient } from "../interfaces/firestore-client.interface";
3
+ import { IAuthClient } from "../interfaces/auth-client.interface";
4
+ import { DocumentResult } from "../types/api-types";
5
+ /**
6
+ * Implementation of template operations.
7
+ *
8
+ * Templates are documents with `isTemplate: true` that can be cloned
9
+ * by premium users to create new documents.
10
+ */
11
+ export declare class TemplateOperations implements ITemplateOperations {
12
+ private readonly firestore;
13
+ private readonly auth;
14
+ constructor(firestore: IFirestoreClient, auth: IAuthClient);
15
+ list(): Promise<DocumentResult[]>;
16
+ get(templateId: string): Promise<DocumentResult>;
17
+ clone(templateId: string, title?: string): Promise<DocumentResult>;
18
+ publish(templateId: string): Promise<void>;
19
+ unpublish(templateId: string): Promise<void>;
20
+ /**
21
+ * Clones a node with updated IDs and references.
22
+ * Note: Firestore doesn't allow undefined values, so we conditionally include optional fields.
23
+ */
24
+ private _cloneNode;
25
+ /**
26
+ * Clones an element with updated IDs and references.
27
+ * Note: Firestore doesn't allow undefined values, so we conditionally include optional fields.
28
+ */
29
+ private _cloneElement;
30
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TemplateOperations = void 0;
4
+ const models_1 = require("../models");
5
+ const uuid_1 = require("../utils/uuid");
6
+ const api_errors_1 = require("../errors/api-errors");
7
+ // Default canvas dimensions
8
+ const DEFAULT_CANVAS_WIDTH = 1920;
9
+ const DEFAULT_CANVAS_HEIGHT = 1080;
10
+ /**
11
+ * Implementation of template operations.
12
+ *
13
+ * Templates are documents with `isTemplate: true` that can be cloned
14
+ * by premium users to create new documents.
15
+ */
16
+ class TemplateOperations {
17
+ firestore;
18
+ auth;
19
+ constructor(firestore, auth) {
20
+ this.firestore = firestore;
21
+ this.auth = auth;
22
+ }
23
+ async list() {
24
+ this.auth.requireAuth();
25
+ // Check if user is admin (admins can see all templates including unpublished)
26
+ const isAdmin = await this.auth.isAdmin();
27
+ // Query templates where isTemplate == true
28
+ const documents = await this.firestore.queryCollection("documents", "isTemplate", true);
29
+ // Filter based on user role:
30
+ // - Admins can see all templates (published and unpublished)
31
+ // - Regular users can only see published templates
32
+ const templates = isAdmin
33
+ ? documents
34
+ : documents.filter((doc) => {
35
+ const d = doc;
36
+ return d.isPublished === true;
37
+ });
38
+ // Return shallow documents without nodes
39
+ return templates.map((doc) => ({
40
+ ...doc,
41
+ nodes: []
42
+ }));
43
+ }
44
+ async get(templateId) {
45
+ this.auth.requireAuth();
46
+ const docData = await this.firestore.getDocument(`documents/${templateId}`);
47
+ if (!docData) {
48
+ throw new api_errors_1.NotFoundError(`Template ${templateId} not found`);
49
+ }
50
+ const docWithFlags = docData;
51
+ // Verify it's actually a template
52
+ if (!docWithFlags.isTemplate) {
53
+ throw new api_errors_1.NotFoundError(`Document ${templateId} is not a template`);
54
+ }
55
+ // Check visibility: unpublished templates are only visible to admins
56
+ if (!docWithFlags.isPublished) {
57
+ const isAdmin = await this.auth.isAdmin();
58
+ if (!isAdmin) {
59
+ throw new api_errors_1.NotFoundError(`Template ${templateId} not found`);
60
+ }
61
+ }
62
+ const nodes = await this.firestore.getCollection(`documents/${templateId}/nodes`);
63
+ return {
64
+ ...docData,
65
+ nodes
66
+ };
67
+ }
68
+ async clone(templateId, title) {
69
+ const userId = this.auth.requireAuth();
70
+ // Require premium access for cloning
71
+ await this.auth.requirePremium();
72
+ // Get the template with all its nodes
73
+ const template = await this.get(templateId);
74
+ // Build ID mapping for nodes and elements
75
+ const idMapping = new Map();
76
+ const nodes = template.nodes || [];
77
+ nodes.forEach((node) => {
78
+ idMapping.set(node.id, (0, uuid_1.generateUuid)());
79
+ node.elements.forEach((element) => {
80
+ idMapping.set(element.id, (0, uuid_1.generateUuid)());
81
+ });
82
+ });
83
+ // Determine new root node ID - fail fast if template's root node is missing
84
+ if (template.rootNodeId && !idMapping.has(template.rootNodeId)) {
85
+ throw new api_errors_1.ValidationError(`Template's root node (${template.rootNodeId}) is missing from template nodes`);
86
+ }
87
+ const newRootNodeId = template.rootNodeId
88
+ ? idMapping.get(template.rootNodeId)
89
+ : (0, uuid_1.generateUuid)();
90
+ // Generate new document ID
91
+ const newDocId = (0, uuid_1.generateUuid)();
92
+ // Create the cloned document
93
+ const clonedDocument = {
94
+ schemaVersion: models_1.CURRENT_DOCUMENT_SCHEMA_VERSION,
95
+ title: title || `Copy of ${template.title}`,
96
+ content: template.content || "",
97
+ owner: userId,
98
+ sharedWith: [],
99
+ createdAt: new Date(),
100
+ rootNodeId: newRootNodeId,
101
+ isPremiumContent: template.isPremiumContent || false,
102
+ isTemplate: false,
103
+ clonedFromTemplateId: templateId,
104
+ nodes: [] // Nodes stored in sub-collection
105
+ };
106
+ // Clone all nodes with new IDs
107
+ const clonedNodes = nodes.map((node) => {
108
+ const newNodeId = idMapping.get(node.id);
109
+ return this._cloneNode(node, newNodeId, userId, idMapping);
110
+ });
111
+ // Write everything in a batch
112
+ const batch = this.firestore.batch();
113
+ batch.set(`documents/${newDocId}`, clonedDocument);
114
+ for (const node of clonedNodes) {
115
+ batch.set(`documents/${newDocId}/nodes/${node.id}`, node);
116
+ }
117
+ await batch.commit();
118
+ return {
119
+ id: newDocId,
120
+ ...clonedDocument,
121
+ nodes: clonedNodes
122
+ };
123
+ }
124
+ async publish(templateId) {
125
+ this.auth.requireAuth();
126
+ await this.auth.requireAdmin();
127
+ // Verify template exists
128
+ const docData = await this.firestore.getDocument(`documents/${templateId}`);
129
+ if (!docData) {
130
+ throw new api_errors_1.NotFoundError(`Template ${templateId} not found`);
131
+ }
132
+ if (!docData.isTemplate) {
133
+ throw new api_errors_1.NotFoundError(`Document ${templateId} is not a template`);
134
+ }
135
+ await this.firestore.updateDocument(`documents/${templateId}`, {
136
+ isPublished: true
137
+ });
138
+ }
139
+ async unpublish(templateId) {
140
+ this.auth.requireAuth();
141
+ await this.auth.requireAdmin();
142
+ // Verify template exists
143
+ const docData = await this.firestore.getDocument(`documents/${templateId}`);
144
+ if (!docData) {
145
+ throw new api_errors_1.NotFoundError(`Template ${templateId} not found`);
146
+ }
147
+ if (!docData.isTemplate) {
148
+ throw new api_errors_1.NotFoundError(`Document ${templateId} is not a template`);
149
+ }
150
+ await this.firestore.updateDocument(`documents/${templateId}`, {
151
+ isPublished: false
152
+ });
153
+ }
154
+ /**
155
+ * Clones a node with updated IDs and references.
156
+ * Note: Firestore doesn't allow undefined values, so we conditionally include optional fields.
157
+ */
158
+ _cloneNode(node, newNodeId, userId, idMapping) {
159
+ const clonedElements = node.elements.map((element) => this._cloneElement(element, idMapping));
160
+ const clonedNode = {
161
+ id: newNodeId,
162
+ title: node.title,
163
+ content: node.content,
164
+ elements: clonedElements,
165
+ canvasWidth: node.canvasWidth || DEFAULT_CANVAS_WIDTH,
166
+ canvasHeight: node.canvasHeight || DEFAULT_CANVAS_HEIGHT,
167
+ level: node.level,
168
+ owner: userId
169
+ };
170
+ // Only include parentNodeId if it exists (Firestore doesn't allow undefined)
171
+ if (node.parentNodeId) {
172
+ clonedNode.parentNodeId = idMapping.get(node.parentNodeId) || node.parentNodeId;
173
+ }
174
+ return clonedNode;
175
+ }
176
+ /**
177
+ * Clones an element with updated IDs and references.
178
+ * Note: Firestore doesn't allow undefined values, so we conditionally include optional fields.
179
+ */
180
+ _cloneElement(element, idMapping) {
181
+ const newElementId = idMapping.get(element.id) || (0, uuid_1.generateUuid)();
182
+ // Clone properties and update any ID references
183
+ const clonedProperties = { ...element.properties };
184
+ // Update connector endpoint references if this is a connector
185
+ if (element.elementType === "connector" && clonedProperties) {
186
+ if (clonedProperties["startAnchor"]?.elementId &&
187
+ idMapping.has(clonedProperties["startAnchor"].elementId)) {
188
+ clonedProperties["startAnchor"] = {
189
+ ...clonedProperties["startAnchor"],
190
+ elementId: idMapping.get(clonedProperties["startAnchor"].elementId)
191
+ };
192
+ }
193
+ if (clonedProperties["endAnchor"]?.elementId &&
194
+ idMapping.has(clonedProperties["endAnchor"].elementId)) {
195
+ clonedProperties["endAnchor"] = {
196
+ ...clonedProperties["endAnchor"],
197
+ elementId: idMapping.get(clonedProperties["endAnchor"].elementId)
198
+ };
199
+ }
200
+ }
201
+ const clonedElement = {
202
+ id: newElementId,
203
+ elementType: element.elementType,
204
+ x: element.x,
205
+ y: element.y,
206
+ width: element.width,
207
+ height: element.height,
208
+ rotation: element.rotation,
209
+ properties: clonedProperties
210
+ };
211
+ // Only include optional fields if they have values (Firestore doesn't allow undefined)
212
+ if (element.isLink !== undefined) {
213
+ clonedElement.isLink = element.isLink;
214
+ }
215
+ if (element.linkTarget) {
216
+ clonedElement.linkTarget = idMapping.get(element.linkTarget) || element.linkTarget;
217
+ }
218
+ return clonedElement;
219
+ }
220
+ }
221
+ exports.TemplateOperations = TemplateOperations;
@@ -5,9 +5,11 @@ import { IAuthClient } from "../interfaces/auth-client.interface";
5
5
  export declare class MockAuthClient implements IAuthClient {
6
6
  private _currentUserId;
7
7
  private _isPremium;
8
+ private _isAdmin;
8
9
  constructor(options?: {
9
10
  userId?: string;
10
11
  isPremium?: boolean;
12
+ isAdmin?: boolean;
11
13
  });
12
14
  signIn(email: string, _password: string): Promise<void>;
13
15
  signOut(): Promise<void>;
@@ -23,4 +25,10 @@ export declare class MockAuthClient implements IAuthClient {
23
25
  * Sets the premium status for testing.
24
26
  */
25
27
  setPremium(isPremium: boolean): void;
28
+ isAdmin(): Promise<boolean>;
29
+ requireAdmin(): Promise<void>;
30
+ /**
31
+ * Sets the admin status for testing.
32
+ */
33
+ setAdmin(isAdmin: boolean): void;
26
34
  }
@@ -8,9 +8,11 @@ const api_errors_1 = require("../errors/api-errors");
8
8
  class MockAuthClient {
9
9
  _currentUserId = null;
10
10
  _isPremium = false;
11
+ _isAdmin = false;
11
12
  constructor(options) {
12
13
  this._currentUserId = options?.userId ?? null;
13
14
  this._isPremium = options?.isPremium ?? false;
15
+ this._isAdmin = options?.isAdmin ?? false;
14
16
  }
15
17
  async signIn(email, _password) {
16
18
  // Simulate successful sign-in
@@ -49,5 +51,20 @@ class MockAuthClient {
49
51
  setPremium(isPremium) {
50
52
  this._isPremium = isPremium;
51
53
  }
54
+ async isAdmin() {
55
+ return this._isAdmin;
56
+ }
57
+ async requireAdmin() {
58
+ this.requireAuth();
59
+ if (!this._isAdmin) {
60
+ throw new api_errors_1.PermissionError("Admin access required");
61
+ }
62
+ }
63
+ /**
64
+ * Sets the admin status for testing.
65
+ */
66
+ setAdmin(isAdmin) {
67
+ this._isAdmin = isAdmin;
68
+ }
52
69
  }
53
70
  exports.MockAuthClient = MockAuthClient;
@@ -13,7 +13,7 @@ export declare class MockFirestoreClient implements IFirestoreClient {
13
13
  getCollection<T>(path: string): Promise<(T & {
14
14
  id: string;
15
15
  })[]>;
16
- queryCollection<T>(path: string, field: string, value: string): Promise<(T & {
16
+ queryCollection<T>(path: string, field: string, value: string | number | boolean | null): Promise<(T & {
17
17
  id: string;
18
18
  })[]>;
19
19
  batch(): IBatchWriter;