@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 +43 -1
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
- package/src/lib/clients/firebase-auth-client.d.ts +2 -0
- package/src/lib/clients/firebase-auth-client.js +20 -0
- package/src/lib/clients/firebase-firestore-client.d.ts +1 -1
- package/src/lib/graph-knowledge-api.d.ts +8 -0
- package/src/lib/graph-knowledge-api.js +11 -0
- package/src/lib/interfaces/auth-client.interface.d.ts +11 -0
- package/src/lib/interfaces/firestore-client.interface.d.ts +2 -1
- package/src/lib/interfaces/template-operations.interface.d.ts +55 -0
- package/src/lib/interfaces/template-operations.interface.js +2 -0
- package/src/lib/models/document.model.d.ts +15 -0
- package/src/lib/operations/template-operations.d.ts +30 -0
- package/src/lib/operations/template-operations.js +221 -0
- package/src/lib/testing/mock-auth-client.d.ts +8 -0
- package/src/lib/testing/mock-auth-client.js +17 -0
- package/src/lib/testing/mock-firestore-client.d.ts +1 -1
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
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";
|
|
@@ -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
|
+
}
|
|
@@ -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;
|