@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.
@@ -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
+ });