@memorylayerai/sdk 0.1.0 → 0.2.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 +487 -18
- package/package.json +1 -1
- package/src/client.ts +12 -0
- package/src/index.ts +15 -0
- package/src/resources/graph.ts +212 -0
- package/src/types.ts +179 -0
- package/tests/graph.test.ts +260 -0
- package/tests/graph.unit.test.ts +513 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { HTTPClient } from '../http-client.js';
|
|
2
|
+
import {
|
|
3
|
+
GraphData,
|
|
4
|
+
NodeDetails,
|
|
5
|
+
GetGraphRequest,
|
|
6
|
+
GetNodeDetailsRequest,
|
|
7
|
+
GetNodeEdgesRequest,
|
|
8
|
+
GetNodeEdgesResponse,
|
|
9
|
+
} from '../types.js';
|
|
10
|
+
import { ValidationError } from '../errors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resource for graph visualization operations
|
|
14
|
+
*
|
|
15
|
+
* Provides methods to fetch graph data (nodes and edges) for visualization.
|
|
16
|
+
*
|
|
17
|
+
* Requirements: 6.1, 6.2, 6.3, 6.4
|
|
18
|
+
*/
|
|
19
|
+
export class GraphResource {
|
|
20
|
+
constructor(private httpClient: HTTPClient) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get graph data for a space/project
|
|
24
|
+
*
|
|
25
|
+
* Fetches nodes (memories, documents, entities) and edges (relationships)
|
|
26
|
+
* for visualization. Supports pagination and filtering.
|
|
27
|
+
*
|
|
28
|
+
* @param request - Graph data request with filters
|
|
29
|
+
* @returns Graph data with nodes, edges, metadata, and pagination
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const graphData = await client.graph.getGraph({
|
|
34
|
+
* spaceId: 'project-123',
|
|
35
|
+
* limit: 100,
|
|
36
|
+
* nodeTypes: ['memory', 'document'],
|
|
37
|
+
* relationshipTypes: ['extends', 'updates']
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* console.log(`Found ${graphData.nodes.length} nodes`);
|
|
41
|
+
* console.log(`Found ${graphData.edges.length} edges`);
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Requirements: 6.1
|
|
45
|
+
*/
|
|
46
|
+
async getGraph(request: GetGraphRequest): Promise<GraphData> {
|
|
47
|
+
// Validate request
|
|
48
|
+
if (!request.spaceId || request.spaceId.trim().length === 0) {
|
|
49
|
+
throw new ValidationError(
|
|
50
|
+
'Space ID is required',
|
|
51
|
+
[{ field: 'spaceId', message: 'Space ID is required' }]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build query parameters
|
|
56
|
+
const query: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
if (request.cursor) {
|
|
59
|
+
query.cursor = request.cursor;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (request.limit !== undefined) {
|
|
63
|
+
query.limit = request.limit.toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (request.nodeTypes && request.nodeTypes.length > 0) {
|
|
67
|
+
query.nodeTypes = request.nodeTypes.join(',');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (request.relationshipTypes && request.relationshipTypes.length > 0) {
|
|
71
|
+
query.relationshipTypes = request.relationshipTypes.join(',');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (request.startDate) {
|
|
75
|
+
query.startDate = request.startDate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (request.endDate) {
|
|
79
|
+
query.endDate = request.endDate;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.httpClient.request<GraphData>({
|
|
83
|
+
method: 'GET',
|
|
84
|
+
path: `/v1/graph/spaces/${request.spaceId}`,
|
|
85
|
+
query,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get detailed information for a specific node
|
|
91
|
+
*
|
|
92
|
+
* Fetches node data, connected edges, and neighboring nodes.
|
|
93
|
+
*
|
|
94
|
+
* @param request - Node details request
|
|
95
|
+
* @returns Node details with edges and connected nodes
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const details = await client.graph.getNodeDetails({
|
|
100
|
+
* nodeId: 'memory-456'
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* console.log(`Node: ${details.node.label}`);
|
|
104
|
+
* console.log(`Connected to ${details.connectedNodes.length} nodes`);
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* Requirements: 6.2
|
|
108
|
+
*/
|
|
109
|
+
async getNodeDetails(request: GetNodeDetailsRequest): Promise<NodeDetails> {
|
|
110
|
+
// Validate request
|
|
111
|
+
if (!request.nodeId || request.nodeId.trim().length === 0) {
|
|
112
|
+
throw new ValidationError(
|
|
113
|
+
'Node ID is required',
|
|
114
|
+
[{ field: 'nodeId', message: 'Node ID is required' }]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return this.httpClient.request<NodeDetails>({
|
|
119
|
+
method: 'GET',
|
|
120
|
+
path: `/v1/graph/nodes/${request.nodeId}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get edges connected to a specific node
|
|
126
|
+
*
|
|
127
|
+
* Fetches edges and connected nodes, optionally filtered by edge type.
|
|
128
|
+
*
|
|
129
|
+
* @param request - Node edges request
|
|
130
|
+
* @returns Edges and connected nodes
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const edges = await client.graph.getNodeEdges({
|
|
135
|
+
* nodeId: 'memory-456',
|
|
136
|
+
* edgeTypes: ['extends', 'updates']
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* console.log(`Found ${edges.edges.length} edges`);
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* Requirements: 6.3
|
|
143
|
+
*/
|
|
144
|
+
async getNodeEdges(request: GetNodeEdgesRequest): Promise<GetNodeEdgesResponse> {
|
|
145
|
+
// Validate request
|
|
146
|
+
if (!request.nodeId || request.nodeId.trim().length === 0) {
|
|
147
|
+
throw new ValidationError(
|
|
148
|
+
'Node ID is required',
|
|
149
|
+
[{ field: 'nodeId', message: 'Node ID is required' }]
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build query parameters
|
|
154
|
+
const query: Record<string, string> = {};
|
|
155
|
+
|
|
156
|
+
if (request.edgeTypes && request.edgeTypes.length > 0) {
|
|
157
|
+
query.edgeTypes = request.edgeTypes.join(',');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this.httpClient.request<GetNodeEdgesResponse>({
|
|
161
|
+
method: 'GET',
|
|
162
|
+
path: `/v1/graph/nodes/${request.nodeId}/edges`,
|
|
163
|
+
query,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all graph pages using async iteration
|
|
169
|
+
*
|
|
170
|
+
* Automatically handles pagination to fetch all nodes and edges.
|
|
171
|
+
* Yields each page of results as they are fetched.
|
|
172
|
+
*
|
|
173
|
+
* @param request - Initial graph request (without cursor)
|
|
174
|
+
* @yields Graph data for each page
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* for await (const page of client.graph.getAllGraphPages({ spaceId: 'project-123' })) {
|
|
179
|
+
* console.log(`Page has ${page.nodes.length} nodes`);
|
|
180
|
+
* // Process nodes...
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // Collect all nodes
|
|
187
|
+
* const allNodes: GraphNode[] = [];
|
|
188
|
+
* for await (const page of client.graph.getAllGraphPages({ spaceId: 'project-123' })) {
|
|
189
|
+
* allNodes.push(...page.nodes);
|
|
190
|
+
* }
|
|
191
|
+
* console.log(`Total nodes: ${allNodes.length}`);
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* Requirements: 6.4
|
|
195
|
+
*/
|
|
196
|
+
async *getAllGraphPages(request: Omit<GetGraphRequest, 'cursor'>): AsyncGenerator<GraphData, void, undefined> {
|
|
197
|
+
let cursor: string | undefined;
|
|
198
|
+
let hasMore = true;
|
|
199
|
+
|
|
200
|
+
while (hasMore) {
|
|
201
|
+
const page = await this.getGraph({
|
|
202
|
+
...request,
|
|
203
|
+
cursor,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
yield page;
|
|
207
|
+
|
|
208
|
+
cursor = page.pagination.nextCursor;
|
|
209
|
+
hasMore = page.pagination.hasMore;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -223,3 +223,182 @@ export interface StreamChunk {
|
|
|
223
223
|
/** Array of streaming choices */
|
|
224
224
|
choices: StreamChoice[];
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Graph Visualization Types
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Memory status for visualization
|
|
233
|
+
*/
|
|
234
|
+
export type MemoryStatus = 'latest' | 'older' | 'forgotten' | 'expiring' | 'new';
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Node types in the graph
|
|
238
|
+
*/
|
|
239
|
+
export type NodeType = 'memory' | 'document' | 'entity';
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Edge types in the graph
|
|
243
|
+
*/
|
|
244
|
+
export type EdgeType = 'updates' | 'extends' | 'derives' | 'similarity';
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Node in the graph (memory, document, or entity)
|
|
248
|
+
*/
|
|
249
|
+
export interface GraphNode {
|
|
250
|
+
/** Unique identifier for the node */
|
|
251
|
+
id: string;
|
|
252
|
+
/** Type of node */
|
|
253
|
+
type: NodeType;
|
|
254
|
+
/** Display label (truncated content or title) */
|
|
255
|
+
label: string;
|
|
256
|
+
/** Node data */
|
|
257
|
+
data: {
|
|
258
|
+
/** Full content (for memory nodes) */
|
|
259
|
+
content?: string;
|
|
260
|
+
/** Title (for document nodes) */
|
|
261
|
+
title?: string;
|
|
262
|
+
/** Memory status */
|
|
263
|
+
status?: MemoryStatus;
|
|
264
|
+
/** Creation timestamp (ISO 8601) */
|
|
265
|
+
createdAt: string;
|
|
266
|
+
/** Expiration timestamp (ISO 8601) */
|
|
267
|
+
expiresAt?: string;
|
|
268
|
+
/** Source reference */
|
|
269
|
+
source?: string;
|
|
270
|
+
/** Additional metadata */
|
|
271
|
+
metadata?: Record<string, any>;
|
|
272
|
+
};
|
|
273
|
+
/** Optional position hints for layout */
|
|
274
|
+
position?: { x: number; y: number };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Edge in the graph (relationship or similarity)
|
|
279
|
+
*/
|
|
280
|
+
export interface GraphEdge {
|
|
281
|
+
/** Unique identifier for the edge */
|
|
282
|
+
id: string;
|
|
283
|
+
/** Source node ID */
|
|
284
|
+
source: string;
|
|
285
|
+
/** Target node ID */
|
|
286
|
+
target: string;
|
|
287
|
+
/** Type of edge */
|
|
288
|
+
type: EdgeType;
|
|
289
|
+
/** Display label for the edge */
|
|
290
|
+
label?: string;
|
|
291
|
+
/** Edge data */
|
|
292
|
+
data: {
|
|
293
|
+
/** Strength of the relationship (0.0 to 1.0) */
|
|
294
|
+
strength: number;
|
|
295
|
+
/** Additional metadata */
|
|
296
|
+
metadata?: Record<string, any>;
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Graph metadata and statistics
|
|
302
|
+
*/
|
|
303
|
+
export interface GraphMetadata {
|
|
304
|
+
/** Total number of nodes */
|
|
305
|
+
totalNodes: number;
|
|
306
|
+
/** Number of memory nodes */
|
|
307
|
+
memoryCount: number;
|
|
308
|
+
/** Number of document nodes */
|
|
309
|
+
documentCount: number;
|
|
310
|
+
/** Number of entity nodes */
|
|
311
|
+
entityCount: number;
|
|
312
|
+
/** Total number of edges */
|
|
313
|
+
totalEdges: number;
|
|
314
|
+
/** Number of relationship edges */
|
|
315
|
+
relationshipCount: number;
|
|
316
|
+
/** Number of similarity edges */
|
|
317
|
+
similarityCount: number;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Pagination information
|
|
322
|
+
*/
|
|
323
|
+
export interface PaginationInfo {
|
|
324
|
+
/** Whether there are more results */
|
|
325
|
+
hasMore: boolean;
|
|
326
|
+
/** Cursor for the next page */
|
|
327
|
+
nextCursor?: string;
|
|
328
|
+
/** Total count (optional, may be expensive to compute) */
|
|
329
|
+
totalCount?: number;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Complete graph data structure
|
|
334
|
+
*/
|
|
335
|
+
export interface GraphData {
|
|
336
|
+
/** Array of nodes */
|
|
337
|
+
nodes: GraphNode[];
|
|
338
|
+
/** Array of edges */
|
|
339
|
+
edges: GraphEdge[];
|
|
340
|
+
/** Graph metadata and statistics */
|
|
341
|
+
metadata: GraphMetadata;
|
|
342
|
+
/** Pagination information */
|
|
343
|
+
pagination: PaginationInfo;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Request to get graph data
|
|
348
|
+
*/
|
|
349
|
+
export interface GetGraphRequest {
|
|
350
|
+
/** Space/project ID to fetch graph for */
|
|
351
|
+
spaceId: string;
|
|
352
|
+
/** Pagination cursor (optional) */
|
|
353
|
+
cursor?: string;
|
|
354
|
+
/** Maximum number of nodes to return (default: 500, max: 2000) */
|
|
355
|
+
limit?: number;
|
|
356
|
+
/** Filter by node types */
|
|
357
|
+
nodeTypes?: NodeType[];
|
|
358
|
+
/** Filter by relationship types */
|
|
359
|
+
relationshipTypes?: EdgeType[];
|
|
360
|
+
/** Filter by start date (ISO 8601) */
|
|
361
|
+
startDate?: string;
|
|
362
|
+
/** Filter by end date (ISO 8601) */
|
|
363
|
+
endDate?: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Node details response
|
|
368
|
+
*/
|
|
369
|
+
export interface NodeDetails {
|
|
370
|
+
/** The node */
|
|
371
|
+
node: GraphNode;
|
|
372
|
+
/** Edges connected to this node */
|
|
373
|
+
edges: GraphEdge[];
|
|
374
|
+
/** Neighboring nodes */
|
|
375
|
+
connectedNodes: GraphNode[];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Request to get node details
|
|
380
|
+
*/
|
|
381
|
+
export interface GetNodeDetailsRequest {
|
|
382
|
+
/** Node ID */
|
|
383
|
+
nodeId: string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Request to get node edges
|
|
388
|
+
*/
|
|
389
|
+
export interface GetNodeEdgesRequest {
|
|
390
|
+
/** Node ID */
|
|
391
|
+
nodeId: string;
|
|
392
|
+
/** Filter by edge types (optional) */
|
|
393
|
+
edgeTypes?: EdgeType[];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Response for get node edges
|
|
398
|
+
*/
|
|
399
|
+
export interface GetNodeEdgesResponse {
|
|
400
|
+
/** Edges connected to the node */
|
|
401
|
+
edges: GraphEdge[];
|
|
402
|
+
/** Nodes connected via these edges */
|
|
403
|
+
connectedNodes: GraphNode[];
|
|
404
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { MemoryLayerClient } from '../src/index.js';
|
|
3
|
+
import type { GraphData, GraphNode } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Graph SDK Tests
|
|
7
|
+
*
|
|
8
|
+
* These tests verify the Node.js SDK graph methods work correctly.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: These tests require a running backend with test data.
|
|
11
|
+
* They are integration tests, not unit tests.
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 6.1, 6.2, 6.3, 6.4
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe('Graph SDK Tests', () => {
|
|
17
|
+
let client: MemoryLayerClient;
|
|
18
|
+
const testSpaceId = process.env.TEST_SPACE_ID || 'test-space-123';
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
const apiKey = process.env.MEMORYLAYER_API_KEY || process.env.TEST_API_KEY;
|
|
22
|
+
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new Error('API key required for tests. Set MEMORYLAYER_API_KEY or TEST_API_KEY environment variable.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
client = new MemoryLayerClient({
|
|
28
|
+
apiKey,
|
|
29
|
+
baseURL: process.env.TEST_BASE_URL || 'http://localhost:3001',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('getGraph', () => {
|
|
34
|
+
it('should fetch graph data for a space', async () => {
|
|
35
|
+
const graphData = await client.graph.getGraph({
|
|
36
|
+
spaceId: testSpaceId,
|
|
37
|
+
limit: 10,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(graphData).toBeDefined();
|
|
41
|
+
expect(Array.isArray(graphData.nodes)).toBe(true);
|
|
42
|
+
expect(Array.isArray(graphData.edges)).toBe(true);
|
|
43
|
+
expect(graphData.metadata).toBeDefined();
|
|
44
|
+
expect(graphData.pagination).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should support node type filtering', async () => {
|
|
48
|
+
const graphData = await client.graph.getGraph({
|
|
49
|
+
spaceId: testSpaceId,
|
|
50
|
+
nodeTypes: ['memory'],
|
|
51
|
+
limit: 10,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(graphData.nodes.every((n: GraphNode) => n.type === 'memory')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should support relationship type filtering', async () => {
|
|
58
|
+
const graphData = await client.graph.getGraph({
|
|
59
|
+
spaceId: testSpaceId,
|
|
60
|
+
relationshipTypes: ['extends', 'updates'],
|
|
61
|
+
limit: 10,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(graphData.edges.every((e: import('../src/types.js').GraphEdge) =>
|
|
65
|
+
['extends', 'updates'].includes(e.type)
|
|
66
|
+
)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should support date range filtering', async () => {
|
|
70
|
+
const startDate = '2026-01-01T00:00:00Z';
|
|
71
|
+
const graphData = await client.graph.getGraph({
|
|
72
|
+
spaceId: testSpaceId,
|
|
73
|
+
startDate,
|
|
74
|
+
limit: 10,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(graphData.nodes.every((n: GraphNode) =>
|
|
78
|
+
new Date(n.data.createdAt) >= new Date(startDate)
|
|
79
|
+
)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw validation error for missing spaceId', async () => {
|
|
83
|
+
await expect(
|
|
84
|
+
client.graph.getGraph({ spaceId: '' })
|
|
85
|
+
).rejects.toThrow('Space ID is required');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('getNodeDetails', () => {
|
|
90
|
+
it('should fetch node details', async () => {
|
|
91
|
+
// First get a node ID
|
|
92
|
+
const graphData = await client.graph.getGraph({
|
|
93
|
+
spaceId: testSpaceId,
|
|
94
|
+
limit: 1,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (graphData.nodes.length === 0) {
|
|
98
|
+
console.warn('No nodes available for testing getNodeDetails');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nodeId = graphData.nodes[0].id;
|
|
103
|
+
const details = await client.graph.getNodeDetails({ nodeId });
|
|
104
|
+
|
|
105
|
+
expect(details).toBeDefined();
|
|
106
|
+
expect(details.node).toBeDefined();
|
|
107
|
+
expect(details.node.id).toBe(nodeId);
|
|
108
|
+
expect(Array.isArray(details.edges)).toBe(true);
|
|
109
|
+
expect(Array.isArray(details.connectedNodes)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should throw validation error for missing nodeId', async () => {
|
|
113
|
+
await expect(
|
|
114
|
+
client.graph.getNodeDetails({ nodeId: '' })
|
|
115
|
+
).rejects.toThrow('Node ID is required');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getNodeEdges', () => {
|
|
120
|
+
it('should fetch node edges', async () => {
|
|
121
|
+
// First get a node ID
|
|
122
|
+
const graphData = await client.graph.getGraph({
|
|
123
|
+
spaceId: testSpaceId,
|
|
124
|
+
limit: 1,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (graphData.nodes.length === 0) {
|
|
128
|
+
console.warn('No nodes available for testing getNodeEdges');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const nodeId = graphData.nodes[0].id;
|
|
133
|
+
const result = await client.graph.getNodeEdges({ nodeId });
|
|
134
|
+
|
|
135
|
+
expect(result).toBeDefined();
|
|
136
|
+
expect(Array.isArray(result.edges)).toBe(true);
|
|
137
|
+
expect(Array.isArray(result.connectedNodes)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should support edge type filtering', async () => {
|
|
141
|
+
// First get a node ID
|
|
142
|
+
const graphData = await client.graph.getGraph({
|
|
143
|
+
spaceId: testSpaceId,
|
|
144
|
+
limit: 1,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (graphData.nodes.length === 0) {
|
|
148
|
+
console.warn('No nodes available for testing edge filtering');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nodeId = graphData.nodes[0].id;
|
|
153
|
+
const result = await client.graph.getNodeEdges({
|
|
154
|
+
nodeId,
|
|
155
|
+
edgeTypes: ['extends'],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.edges.every(e => e.type === 'extends')).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should throw validation error for missing nodeId', async () => {
|
|
162
|
+
await expect(
|
|
163
|
+
client.graph.getNodeEdges({ nodeId: '' })
|
|
164
|
+
).rejects.toThrow('Node ID is required');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('getAllGraphPages', () => {
|
|
169
|
+
it('should paginate through all pages', async () => {
|
|
170
|
+
const allNodes: GraphNode[] = [];
|
|
171
|
+
const allNodeIds = new Set<string>();
|
|
172
|
+
let pageCount = 0;
|
|
173
|
+
|
|
174
|
+
for await (const page of client.graph.getAllGraphPages({
|
|
175
|
+
spaceId: testSpaceId,
|
|
176
|
+
limit: 5,
|
|
177
|
+
})) {
|
|
178
|
+
expect(page).toBeDefined();
|
|
179
|
+
expect(Array.isArray(page.nodes)).toBe(true);
|
|
180
|
+
|
|
181
|
+
// Collect nodes
|
|
182
|
+
for (const node of page.nodes) {
|
|
183
|
+
// Verify no duplicates
|
|
184
|
+
expect(allNodeIds.has(node.id)).toBe(false);
|
|
185
|
+
allNodeIds.add(node.id);
|
|
186
|
+
allNodes.push(node);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pageCount++;
|
|
190
|
+
|
|
191
|
+
// Safety limit
|
|
192
|
+
if (pageCount >= 10) break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Should have collected some nodes
|
|
196
|
+
expect(allNodes.length).toBeGreaterThan(0);
|
|
197
|
+
|
|
198
|
+
// All node IDs should be unique
|
|
199
|
+
expect(allNodeIds.size).toBe(allNodes.length);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle empty results', async () => {
|
|
203
|
+
const pages: GraphData[] = [];
|
|
204
|
+
|
|
205
|
+
for await (const page of client.graph.getAllGraphPages({
|
|
206
|
+
spaceId: 'non-existent-space',
|
|
207
|
+
limit: 5,
|
|
208
|
+
})) {
|
|
209
|
+
pages.push(page);
|
|
210
|
+
|
|
211
|
+
// Should only get one empty page
|
|
212
|
+
if (pages.length >= 1) break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(pages.length).toBeGreaterThanOrEqual(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should respect filters across pages', async () => {
|
|
219
|
+
const allNodes: GraphNode[] = [];
|
|
220
|
+
|
|
221
|
+
for await (const page of client.graph.getAllGraphPages({
|
|
222
|
+
spaceId: testSpaceId,
|
|
223
|
+
nodeTypes: ['memory'],
|
|
224
|
+
limit: 5,
|
|
225
|
+
})) {
|
|
226
|
+
allNodes.push(...page.nodes);
|
|
227
|
+
|
|
228
|
+
// Verify all nodes match filter
|
|
229
|
+
expect(page.nodes.every(n => n.type === 'memory')).toBe(true);
|
|
230
|
+
|
|
231
|
+
// Safety limit
|
|
232
|
+
if (allNodes.length >= 20) break;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// All collected nodes should match filter
|
|
236
|
+
expect(allNodes.every(n => n.type === 'memory')).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Type Safety', () => {
|
|
241
|
+
it('should have correct TypeScript types', async () => {
|
|
242
|
+
const graphData = await client.graph.getGraph({
|
|
243
|
+
spaceId: testSpaceId,
|
|
244
|
+
limit: 1,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// These should compile without errors
|
|
248
|
+
const node: GraphNode = graphData.nodes[0];
|
|
249
|
+
const nodeId: string = node.id;
|
|
250
|
+
const nodeType: 'memory' | 'document' | 'entity' = node.type;
|
|
251
|
+
const label: string = node.label;
|
|
252
|
+
const createdAt: string = node.data.createdAt;
|
|
253
|
+
|
|
254
|
+
expect(nodeId).toBeDefined();
|
|
255
|
+
expect(nodeType).toBeDefined();
|
|
256
|
+
expect(label).toBeDefined();
|
|
257
|
+
expect(createdAt).toBeDefined();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|