@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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for Firestore batch write operations.
|
|
3
|
+
*/
|
|
4
|
+
export interface IBatchWriter {
|
|
5
|
+
/**
|
|
6
|
+
* Sets a document in the batch.
|
|
7
|
+
*/
|
|
8
|
+
set<T extends object>(path: string, data: T): void;
|
|
9
|
+
/**
|
|
10
|
+
* Updates a document in the batch.
|
|
11
|
+
*/
|
|
12
|
+
update<T extends object>(path: string, data: Partial<T>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Deletes a document in the batch.
|
|
15
|
+
*/
|
|
16
|
+
delete(path: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Commits all batched operations atomically.
|
|
19
|
+
*/
|
|
20
|
+
commit(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Interface for Firestore operations.
|
|
24
|
+
* Abstracts Firebase Firestore for dependency inversion.
|
|
25
|
+
*/
|
|
26
|
+
export interface IFirestoreClient {
|
|
27
|
+
/**
|
|
28
|
+
* Gets a single document by path.
|
|
29
|
+
* @returns The document data or null if not found
|
|
30
|
+
*/
|
|
31
|
+
getDocument<T>(path: string): Promise<(T & {
|
|
32
|
+
id: string;
|
|
33
|
+
}) | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Sets a document at the given path.
|
|
36
|
+
*/
|
|
37
|
+
setDocument<T extends object>(path: string, data: T): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Updates a document at the given path.
|
|
40
|
+
*/
|
|
41
|
+
updateDocument<T extends object>(path: string, data: Partial<T>): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Deletes a document at the given path.
|
|
44
|
+
*/
|
|
45
|
+
deleteDocument(path: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Gets all documents in a collection.
|
|
48
|
+
*/
|
|
49
|
+
getCollection<T>(path: string): Promise<(T & {
|
|
50
|
+
id: string;
|
|
51
|
+
})[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Queries documents in a collection where a field equals a value.
|
|
54
|
+
*/
|
|
55
|
+
queryCollection<T>(path: string, field: string, value: string): Promise<(T & {
|
|
56
|
+
id: string;
|
|
57
|
+
})[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Creates a new batch writer for atomic operations.
|
|
60
|
+
*/
|
|
61
|
+
batch(): IBatchWriter;
|
|
62
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { GraphNode } from "@graph-knowledge/domain";
|
|
2
|
+
import { CreateNodeInput, UpdateNodeInput } from "../types/api-types";
|
|
3
|
+
/**
|
|
4
|
+
* Interface for node CRUD operations.
|
|
5
|
+
*/
|
|
6
|
+
export interface INodeOperations {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new node within a document.
|
|
9
|
+
* @param documentId The parent document ID
|
|
10
|
+
* @param input Node creation parameters
|
|
11
|
+
* @returns The created node
|
|
12
|
+
* @throws AuthenticationError if not authenticated
|
|
13
|
+
* @throws NotFoundError if document doesn't exist
|
|
14
|
+
* @throws ValidationError if input is invalid
|
|
15
|
+
*/
|
|
16
|
+
create(documentId: string, input: CreateNodeInput): Promise<GraphNode>;
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new node with a specific ID.
|
|
19
|
+
* Useful when you need to reference the node ID before creation (e.g., for linking).
|
|
20
|
+
* @param documentId The parent document ID
|
|
21
|
+
* @param nodeId The specific ID to use for the node
|
|
22
|
+
* @param input Node creation parameters
|
|
23
|
+
* @returns The created node
|
|
24
|
+
* @throws AuthenticationError if not authenticated
|
|
25
|
+
* @throws NotFoundError if document doesn't exist
|
|
26
|
+
* @throws ValidationError if input is invalid
|
|
27
|
+
*/
|
|
28
|
+
createWithId(documentId: string, nodeId: string, input: CreateNodeInput): Promise<GraphNode>;
|
|
29
|
+
/**
|
|
30
|
+
* Gets a node by ID.
|
|
31
|
+
* @param documentId The parent document ID
|
|
32
|
+
* @param nodeId The node ID
|
|
33
|
+
* @returns The node
|
|
34
|
+
* @throws AuthenticationError if not authenticated
|
|
35
|
+
* @throws NotFoundError if node doesn't exist
|
|
36
|
+
*/
|
|
37
|
+
get(documentId: string, nodeId: string): Promise<GraphNode>;
|
|
38
|
+
/**
|
|
39
|
+
* Lists all nodes in a document.
|
|
40
|
+
* @param documentId The parent document ID
|
|
41
|
+
* @returns Array of nodes
|
|
42
|
+
* @throws AuthenticationError if not authenticated
|
|
43
|
+
*/
|
|
44
|
+
list(documentId: string): Promise<GraphNode[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Updates a node's properties.
|
|
47
|
+
* @param documentId The parent document ID
|
|
48
|
+
* @param nodeId The node ID
|
|
49
|
+
* @param input Fields to update
|
|
50
|
+
* @throws AuthenticationError if not authenticated
|
|
51
|
+
* @throws NotFoundError if node doesn't exist
|
|
52
|
+
* @throws ValidationError if input is invalid
|
|
53
|
+
*/
|
|
54
|
+
update(documentId: string, nodeId: string, input: UpdateNodeInput): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Deletes a node.
|
|
57
|
+
* @param documentId The parent document ID
|
|
58
|
+
* @param nodeId The node ID
|
|
59
|
+
* @throws AuthenticationError if not authenticated
|
|
60
|
+
*/
|
|
61
|
+
delete(documentId: string, nodeId: string): Promise<void>;
|
|
62
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain models bundled for npm package self-containment.
|
|
3
|
+
* These are copies of the types from @graph-knowledge/domain.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Current document schema version.
|
|
7
|
+
* Increment this when making breaking changes to Document, GraphNode, or GraphElement structure.
|
|
8
|
+
*
|
|
9
|
+
* Version History:
|
|
10
|
+
* - 0: Legacy documents (no schemaVersion field)
|
|
11
|
+
* - 1: Initial versioned schema (added schemaVersion tracking)
|
|
12
|
+
*/
|
|
13
|
+
export declare const CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
|
|
14
|
+
export interface Document {
|
|
15
|
+
id: string;
|
|
16
|
+
/**
|
|
17
|
+
* Schema version for migration support.
|
|
18
|
+
* Documents without this field are treated as version 0 (legacy).
|
|
19
|
+
*/
|
|
20
|
+
schemaVersion?: number;
|
|
21
|
+
title: string;
|
|
22
|
+
content: string;
|
|
23
|
+
/**
|
|
24
|
+
* UID of the document owner.
|
|
25
|
+
* OWNER-ONLY: Only the owner can modify this field.
|
|
26
|
+
*/
|
|
27
|
+
owner: string;
|
|
28
|
+
/**
|
|
29
|
+
* Email addresses of users with whom this document is shared.
|
|
30
|
+
* OWNER-ONLY: Only the owner can modify this field.
|
|
31
|
+
* Changes trigger Cloud Function to sync sharedWithUser arrays.
|
|
32
|
+
*/
|
|
33
|
+
sharedWith: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Document creation timestamp.
|
|
36
|
+
* OWNER-ONLY: Set on creation, cannot be modified by shared users.
|
|
37
|
+
*/
|
|
38
|
+
createdAt: Date | {
|
|
39
|
+
seconds: number;
|
|
40
|
+
nanoseconds: number;
|
|
41
|
+
} | null;
|
|
42
|
+
nodes?: GraphNode[];
|
|
43
|
+
rootNodeId?: string;
|
|
44
|
+
isPremiumContent?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface GraphNode {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
content: string;
|
|
50
|
+
elements: GraphElement[];
|
|
51
|
+
parentNodeId?: string;
|
|
52
|
+
canvasWidth?: number;
|
|
53
|
+
canvasHeight?: number;
|
|
54
|
+
level?: number;
|
|
55
|
+
owner?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface GraphElement {
|
|
58
|
+
id: string;
|
|
59
|
+
elementType: string;
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
/**
|
|
65
|
+
* Rotation in degrees, always an integer in the range [0, 360).
|
|
66
|
+
* This enables reliable grouping and comparison for snapping operations.
|
|
67
|
+
* Legacy documents with floating-point values are normalized during loading.
|
|
68
|
+
*/
|
|
69
|
+
rotation: number;
|
|
70
|
+
properties: Record<string, any>;
|
|
71
|
+
isLink?: boolean;
|
|
72
|
+
linkTarget?: string;
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain models bundled for npm package self-containment.
|
|
3
|
+
* These are copies of the types from @graph-knowledge/domain.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Current document schema version.
|
|
7
|
+
* Increment this when making breaking changes to Document, GraphNode, or GraphElement structure.
|
|
8
|
+
*
|
|
9
|
+
* Version History:
|
|
10
|
+
* - 0: Legacy documents (no schemaVersion field)
|
|
11
|
+
* - 1: Initial versioned schema (added schemaVersion tracking)
|
|
12
|
+
*/
|
|
13
|
+
export const CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { GraphElement } from "@graph-knowledge/domain";
|
|
2
|
+
import { IBatchOperations, BatchUpdateElementInput } from "../interfaces/batch-operations.interface";
|
|
3
|
+
import { IFirestoreClient } from "../interfaces/firestore-client.interface";
|
|
4
|
+
import { IAuthClient } from "../interfaces/auth-client.interface";
|
|
5
|
+
import { IElementValidatorRegistry } from "../interfaces/element-validator.interface";
|
|
6
|
+
import { AnyElementInput } from "../types/element-input-types";
|
|
7
|
+
/**
|
|
8
|
+
* Implementation of batch element operations.
|
|
9
|
+
*/
|
|
10
|
+
export declare class BatchOperations implements IBatchOperations {
|
|
11
|
+
private readonly firestore;
|
|
12
|
+
private readonly auth;
|
|
13
|
+
private readonly validatorRegistry;
|
|
14
|
+
constructor(firestore: IFirestoreClient, auth: IAuthClient, validatorRegistry: IElementValidatorRegistry);
|
|
15
|
+
createElements(documentId: string, nodeId: string, inputs: AnyElementInput[]): Promise<GraphElement[]>;
|
|
16
|
+
updateElements(documentId: string, nodeId: string, updates: BatchUpdateElementInput[]): Promise<void>;
|
|
17
|
+
deleteElements(documentId: string, nodeId: string, elementIds: string[]): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Gets default dimensions for an element type.
|
|
20
|
+
*/
|
|
21
|
+
private getDefaultDimensions;
|
|
22
|
+
/**
|
|
23
|
+
* Normalizes rotation to integer in range [0, 360).
|
|
24
|
+
*/
|
|
25
|
+
private normalizeRotation;
|
|
26
|
+
/**
|
|
27
|
+
* Builds properties object from element input.
|
|
28
|
+
*/
|
|
29
|
+
private buildProperties;
|
|
30
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { generateUuid } from "../utils/uuid";
|
|
2
|
+
import { NotFoundError } from "../errors/api-errors";
|
|
3
|
+
import { DEFAULT_ELEMENT_DIMENSIONS, DEFAULT_CUSTOM_SHAPE_DIMENSIONS } from "../constants/element-defaults";
|
|
4
|
+
/**
|
|
5
|
+
* Implementation of batch element operations.
|
|
6
|
+
*/
|
|
7
|
+
export class BatchOperations {
|
|
8
|
+
firestore;
|
|
9
|
+
auth;
|
|
10
|
+
validatorRegistry;
|
|
11
|
+
constructor(firestore, auth, validatorRegistry) {
|
|
12
|
+
this.firestore = firestore;
|
|
13
|
+
this.auth = auth;
|
|
14
|
+
this.validatorRegistry = validatorRegistry;
|
|
15
|
+
}
|
|
16
|
+
async createElements(documentId, nodeId, inputs) {
|
|
17
|
+
this.auth.requireAuth();
|
|
18
|
+
if (inputs.length === 0) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
// Validate ALL inputs first (before creating any)
|
|
22
|
+
for (const input of inputs) {
|
|
23
|
+
// Custom shapes require premium
|
|
24
|
+
if (input.type.startsWith("custom:")) {
|
|
25
|
+
await this.auth.requirePremium();
|
|
26
|
+
}
|
|
27
|
+
this.validatorRegistry.validate(input);
|
|
28
|
+
}
|
|
29
|
+
const nodePath = `documents/${documentId}/nodes/${nodeId}`;
|
|
30
|
+
const node = await this.firestore.getDocument(nodePath);
|
|
31
|
+
if (!node) {
|
|
32
|
+
throw new NotFoundError(`Node ${nodeId} not found`);
|
|
33
|
+
}
|
|
34
|
+
// Build all elements
|
|
35
|
+
const newElements = inputs.map((input) => {
|
|
36
|
+
const defaults = this.getDefaultDimensions(input.type);
|
|
37
|
+
const x = input.x ?? 0;
|
|
38
|
+
const y = input.y ?? 0;
|
|
39
|
+
const element = {
|
|
40
|
+
id: generateUuid(),
|
|
41
|
+
elementType: input.type,
|
|
42
|
+
x,
|
|
43
|
+
y,
|
|
44
|
+
width: input.width ?? defaults.width,
|
|
45
|
+
height: input.height ?? defaults.height,
|
|
46
|
+
rotation: this.normalizeRotation(input.rotation ?? 0),
|
|
47
|
+
properties: this.buildProperties(input)
|
|
48
|
+
};
|
|
49
|
+
if (input.isLink !== undefined) {
|
|
50
|
+
element.isLink = input.isLink;
|
|
51
|
+
}
|
|
52
|
+
if (input.linkTarget !== undefined) {
|
|
53
|
+
element.linkTarget = input.linkTarget;
|
|
54
|
+
}
|
|
55
|
+
return element;
|
|
56
|
+
});
|
|
57
|
+
// Update node with all new elements atomically
|
|
58
|
+
const updatedElements = [...(node.elements || []), ...newElements];
|
|
59
|
+
await this.firestore.updateDocument(nodePath, {
|
|
60
|
+
elements: updatedElements
|
|
61
|
+
});
|
|
62
|
+
return newElements;
|
|
63
|
+
}
|
|
64
|
+
async updateElements(documentId, nodeId, updates) {
|
|
65
|
+
this.auth.requireAuth();
|
|
66
|
+
if (updates.length === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const nodePath = `documents/${documentId}/nodes/${nodeId}`;
|
|
70
|
+
const node = await this.firestore.getDocument(nodePath);
|
|
71
|
+
if (!node) {
|
|
72
|
+
throw new NotFoundError(`Node ${nodeId} not found`);
|
|
73
|
+
}
|
|
74
|
+
// Create a map for quick lookup
|
|
75
|
+
const updateMap = new Map(updates.map((u) => [u.elementId, u]));
|
|
76
|
+
// Verify all elements exist before updating any
|
|
77
|
+
for (const update of updates) {
|
|
78
|
+
const exists = node.elements.some((e) => e.id === update.elementId);
|
|
79
|
+
if (!exists) {
|
|
80
|
+
throw new NotFoundError(`Element ${update.elementId} not found`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Apply all updates
|
|
84
|
+
const updatedElements = node.elements.map((element) => {
|
|
85
|
+
const update = updateMap.get(element.id);
|
|
86
|
+
if (!update) {
|
|
87
|
+
return element;
|
|
88
|
+
}
|
|
89
|
+
const updated = {
|
|
90
|
+
...element,
|
|
91
|
+
x: update.x ?? element.x,
|
|
92
|
+
y: update.y ?? element.y,
|
|
93
|
+
width: update.width ?? element.width,
|
|
94
|
+
height: update.height ?? element.height,
|
|
95
|
+
rotation: update.rotation !== undefined
|
|
96
|
+
? this.normalizeRotation(update.rotation)
|
|
97
|
+
: element.rotation,
|
|
98
|
+
properties: {
|
|
99
|
+
...element.properties,
|
|
100
|
+
...(update.properties ?? {})
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Handle link properties: only change if the property is present on the update
|
|
104
|
+
if ("isLink" in update) {
|
|
105
|
+
updated.isLink = update.isLink;
|
|
106
|
+
}
|
|
107
|
+
else if (element.isLink !== undefined) {
|
|
108
|
+
updated.isLink = element.isLink;
|
|
109
|
+
}
|
|
110
|
+
if ("linkTarget" in update) {
|
|
111
|
+
updated.linkTarget = update.linkTarget;
|
|
112
|
+
}
|
|
113
|
+
else if (element.linkTarget !== undefined) {
|
|
114
|
+
updated.linkTarget = element.linkTarget;
|
|
115
|
+
}
|
|
116
|
+
return updated;
|
|
117
|
+
});
|
|
118
|
+
await this.firestore.updateDocument(nodePath, {
|
|
119
|
+
elements: updatedElements
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async deleteElements(documentId, nodeId, elementIds) {
|
|
123
|
+
this.auth.requireAuth();
|
|
124
|
+
if (elementIds.length === 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const nodePath = `documents/${documentId}/nodes/${nodeId}`;
|
|
128
|
+
const node = await this.firestore.getDocument(nodePath);
|
|
129
|
+
if (!node) {
|
|
130
|
+
throw new NotFoundError(`Node ${nodeId} not found`);
|
|
131
|
+
}
|
|
132
|
+
const idsToDelete = new Set(elementIds);
|
|
133
|
+
const updatedElements = node.elements.filter((e) => !idsToDelete.has(e.id));
|
|
134
|
+
await this.firestore.updateDocument(nodePath, {
|
|
135
|
+
elements: updatedElements
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Gets default dimensions for an element type.
|
|
140
|
+
*/
|
|
141
|
+
getDefaultDimensions(type) {
|
|
142
|
+
if (type.startsWith("custom:")) {
|
|
143
|
+
return DEFAULT_CUSTOM_SHAPE_DIMENSIONS;
|
|
144
|
+
}
|
|
145
|
+
return (DEFAULT_ELEMENT_DIMENSIONS[type] ?? DEFAULT_CUSTOM_SHAPE_DIMENSIONS);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Normalizes rotation to integer in range [0, 360).
|
|
149
|
+
*/
|
|
150
|
+
normalizeRotation(rotation) {
|
|
151
|
+
let normalized = rotation % 360;
|
|
152
|
+
if (normalized < 0) {
|
|
153
|
+
normalized += 360;
|
|
154
|
+
}
|
|
155
|
+
return Math.round(normalized);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Builds properties object from element input.
|
|
159
|
+
*/
|
|
160
|
+
buildProperties(input) {
|
|
161
|
+
const props = {
|
|
162
|
+
__schemaVersion: 1
|
|
163
|
+
};
|
|
164
|
+
const commonFields = new Set([
|
|
165
|
+
"type",
|
|
166
|
+
"x",
|
|
167
|
+
"y",
|
|
168
|
+
"width",
|
|
169
|
+
"height",
|
|
170
|
+
"rotation",
|
|
171
|
+
"isLink",
|
|
172
|
+
"linkTarget"
|
|
173
|
+
]);
|
|
174
|
+
for (const [key, value] of Object.entries(input)) {
|
|
175
|
+
if (!commonFields.has(key) && value !== undefined) {
|
|
176
|
+
props[key] = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return props;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IDocumentOperations } from "../interfaces/document-operations.interface";
|
|
2
|
+
import { IFirestoreClient } from "../interfaces/firestore-client.interface";
|
|
3
|
+
import { IAuthClient } from "../interfaces/auth-client.interface";
|
|
4
|
+
import { DocumentValidator } from "../validators/document-validator";
|
|
5
|
+
import { CreateDocumentInput, UpdateDocumentInput, DocumentResult } from "../types/api-types";
|
|
6
|
+
/**
|
|
7
|
+
* Implementation of document CRUD operations.
|
|
8
|
+
*/
|
|
9
|
+
export declare class DocumentOperations implements IDocumentOperations {
|
|
10
|
+
private readonly firestore;
|
|
11
|
+
private readonly auth;
|
|
12
|
+
private readonly validator;
|
|
13
|
+
constructor(firestore: IFirestoreClient, auth: IAuthClient, validator: DocumentValidator);
|
|
14
|
+
create(input: CreateDocumentInput): Promise<DocumentResult>;
|
|
15
|
+
get(documentId: string): Promise<DocumentResult>;
|
|
16
|
+
list(): Promise<DocumentResult[]>;
|
|
17
|
+
update(documentId: string, input: UpdateDocumentInput): Promise<void>;
|
|
18
|
+
delete(documentId: string): Promise<void>;
|
|
19
|
+
share(documentId: string, userIds: string[]): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CURRENT_DOCUMENT_SCHEMA_VERSION } from "@graph-knowledge/domain";
|
|
2
|
+
import { generateUuid } from "../utils/uuid";
|
|
3
|
+
import { NotFoundError } from "../errors/api-errors";
|
|
4
|
+
// Default canvas dimensions
|
|
5
|
+
const DEFAULT_CANVAS_WIDTH = 1920;
|
|
6
|
+
const DEFAULT_CANVAS_HEIGHT = 1080;
|
|
7
|
+
/**
|
|
8
|
+
* Implementation of document CRUD operations.
|
|
9
|
+
*/
|
|
10
|
+
export class DocumentOperations {
|
|
11
|
+
firestore;
|
|
12
|
+
auth;
|
|
13
|
+
validator;
|
|
14
|
+
constructor(firestore, auth, validator) {
|
|
15
|
+
this.firestore = firestore;
|
|
16
|
+
this.auth = auth;
|
|
17
|
+
this.validator = validator;
|
|
18
|
+
}
|
|
19
|
+
async create(input) {
|
|
20
|
+
const userId = this.auth.requireAuth();
|
|
21
|
+
this.validator.validateCreate(input);
|
|
22
|
+
const docId = generateUuid();
|
|
23
|
+
const rootNodeId = generateUuid();
|
|
24
|
+
const rootNode = {
|
|
25
|
+
id: rootNodeId,
|
|
26
|
+
title: input.title,
|
|
27
|
+
content: "",
|
|
28
|
+
elements: [],
|
|
29
|
+
canvasWidth: input.canvasWidth ?? DEFAULT_CANVAS_WIDTH,
|
|
30
|
+
canvasHeight: input.canvasHeight ?? DEFAULT_CANVAS_HEIGHT,
|
|
31
|
+
level: 0,
|
|
32
|
+
owner: userId
|
|
33
|
+
};
|
|
34
|
+
const document = {
|
|
35
|
+
schemaVersion: CURRENT_DOCUMENT_SCHEMA_VERSION,
|
|
36
|
+
title: input.title,
|
|
37
|
+
content: input.content ?? "",
|
|
38
|
+
owner: userId,
|
|
39
|
+
sharedWith: [],
|
|
40
|
+
createdAt: new Date(),
|
|
41
|
+
nodes: [], // Nodes stored in sub-collection
|
|
42
|
+
rootNodeId: rootNodeId,
|
|
43
|
+
isPremiumContent: input.isPremiumContent ?? false
|
|
44
|
+
};
|
|
45
|
+
const batch = this.firestore.batch();
|
|
46
|
+
batch.set(`documents/${docId}`, document);
|
|
47
|
+
batch.set(`documents/${docId}/nodes/${rootNodeId}`, rootNode);
|
|
48
|
+
await batch.commit();
|
|
49
|
+
return {
|
|
50
|
+
id: docId,
|
|
51
|
+
...document,
|
|
52
|
+
nodes: [rootNode]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async get(documentId) {
|
|
56
|
+
this.auth.requireAuth();
|
|
57
|
+
const docData = await this.firestore.getDocument(`documents/${documentId}`);
|
|
58
|
+
if (!docData) {
|
|
59
|
+
throw new NotFoundError(`Document ${documentId} not found`);
|
|
60
|
+
}
|
|
61
|
+
const nodes = await this.firestore.getCollection(`documents/${documentId}/nodes`);
|
|
62
|
+
return {
|
|
63
|
+
...docData,
|
|
64
|
+
nodes
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async list() {
|
|
68
|
+
const userId = this.auth.requireAuth();
|
|
69
|
+
const documents = await this.firestore.queryCollection("documents", "owner", userId);
|
|
70
|
+
// Return shallow documents without nodes
|
|
71
|
+
return documents.map((doc) => ({
|
|
72
|
+
...doc,
|
|
73
|
+
nodes: []
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
async update(documentId, input) {
|
|
77
|
+
this.auth.requireAuth();
|
|
78
|
+
this.validator.validateUpdate(input);
|
|
79
|
+
const docData = await this.firestore.getDocument(`documents/${documentId}`);
|
|
80
|
+
if (!docData) {
|
|
81
|
+
throw new NotFoundError(`Document ${documentId} not found`);
|
|
82
|
+
}
|
|
83
|
+
const updates = {};
|
|
84
|
+
if (input.title !== undefined)
|
|
85
|
+
updates.title = input.title;
|
|
86
|
+
if (input.content !== undefined)
|
|
87
|
+
updates.content = input.content;
|
|
88
|
+
if (Object.keys(updates).length > 0) {
|
|
89
|
+
await this.firestore.updateDocument(`documents/${documentId}`, updates);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async delete(documentId) {
|
|
93
|
+
this.auth.requireAuth();
|
|
94
|
+
await this.firestore.deleteDocument(`documents/${documentId}`);
|
|
95
|
+
// Note: Sub-collection nodes are not automatically deleted
|
|
96
|
+
// A Cloud Function should handle cascading deletion
|
|
97
|
+
}
|
|
98
|
+
async share(documentId, userIds) {
|
|
99
|
+
this.auth.requireAuth();
|
|
100
|
+
const docData = await this.firestore.getDocument(`documents/${documentId}`);
|
|
101
|
+
if (!docData) {
|
|
102
|
+
throw new NotFoundError(`Document ${documentId} not found`);
|
|
103
|
+
}
|
|
104
|
+
await this.firestore.updateDocument(`documents/${documentId}`, {
|
|
105
|
+
sharedWith: userIds
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { GraphElement } from "@graph-knowledge/domain";
|
|
2
|
+
import { IElementOperations } from "../interfaces/element-operations.interface";
|
|
3
|
+
import { IFirestoreClient } from "../interfaces/firestore-client.interface";
|
|
4
|
+
import { IAuthClient } from "../interfaces/auth-client.interface";
|
|
5
|
+
import { IElementValidatorRegistry } from "../interfaces/element-validator.interface";
|
|
6
|
+
import { AnyElementInput, UpdateElementInput } from "../types/element-input-types";
|
|
7
|
+
/**
|
|
8
|
+
* Implementation of element CRUD operations.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ElementOperations implements IElementOperations {
|
|
11
|
+
private readonly firestore;
|
|
12
|
+
private readonly auth;
|
|
13
|
+
private readonly validatorRegistry;
|
|
14
|
+
constructor(firestore: IFirestoreClient, auth: IAuthClient, validatorRegistry: IElementValidatorRegistry);
|
|
15
|
+
get(documentId: string, nodeId: string, elementId: string): Promise<GraphElement>;
|
|
16
|
+
list(documentId: string, nodeId: string): Promise<GraphElement[]>;
|
|
17
|
+
create(documentId: string, nodeId: string, input: AnyElementInput): Promise<GraphElement>;
|
|
18
|
+
update(documentId: string, nodeId: string, elementId: string, input: UpdateElementInput): Promise<void>;
|
|
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
|
+
* Builds properties object from element input.
|
|
30
|
+
* Extracts type-specific properties, excluding common fields.
|
|
31
|
+
*/
|
|
32
|
+
private buildProperties;
|
|
33
|
+
}
|