@anvailabs/proximadb-client 0.3.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.
Files changed (65) hide show
  1. package/README.md +160 -0
  2. package/dist/client.d.ts +319 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +681 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/collection.d.ts +227 -0
  7. package/dist/collection.d.ts.map +1 -0
  8. package/dist/collection.js +436 -0
  9. package/dist/collection.js.map +1 -0
  10. package/dist/esm/client.js +674 -0
  11. package/dist/esm/client.js.map +1 -0
  12. package/dist/esm/collection.js +428 -0
  13. package/dist/esm/collection.js.map +1 -0
  14. package/dist/esm/filter.js +228 -0
  15. package/dist/esm/filter.js.map +1 -0
  16. package/dist/esm/generated/schema.js +6 -0
  17. package/dist/esm/generated/schema.js.map +1 -0
  18. package/dist/esm/graph.js +678 -0
  19. package/dist/esm/graph.js.map +1 -0
  20. package/dist/esm/index.js +86 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/search.js +317 -0
  23. package/dist/esm/search.js.map +1 -0
  24. package/dist/esm/transport.js +33 -0
  25. package/dist/esm/transport.js.map +1 -0
  26. package/dist/esm/types.js +181 -0
  27. package/dist/esm/types.js.map +1 -0
  28. package/dist/filter.d.ts +51 -0
  29. package/dist/filter.d.ts.map +1 -0
  30. package/dist/filter.js +241 -0
  31. package/dist/filter.js.map +1 -0
  32. package/dist/generated/schema.d.ts +4271 -0
  33. package/dist/generated/schema.d.ts.map +1 -0
  34. package/dist/generated/schema.js +7 -0
  35. package/dist/generated/schema.js.map +1 -0
  36. package/dist/graph.d.ts +350 -0
  37. package/dist/graph.d.ts.map +1 -0
  38. package/dist/graph.js +690 -0
  39. package/dist/graph.js.map +1 -0
  40. package/dist/index.d.ts +58 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +126 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/search.d.ts +160 -0
  45. package/dist/search.d.ts.map +1 -0
  46. package/dist/search.js +322 -0
  47. package/dist/search.js.map +1 -0
  48. package/dist/transport.d.ts +40 -0
  49. package/dist/transport.d.ts.map +1 -0
  50. package/dist/transport.js +39 -0
  51. package/dist/transport.js.map +1 -0
  52. package/dist/types.d.ts +632 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +185 -0
  55. package/dist/types.js.map +1 -0
  56. package/package.json +72 -0
  57. package/src/client.ts +856 -0
  58. package/src/collection.ts +578 -0
  59. package/src/filter.ts +280 -0
  60. package/src/generated/schema.ts +4271 -0
  61. package/src/graph.ts +811 -0
  62. package/src/index.ts +184 -0
  63. package/src/search.ts +401 -0
  64. package/src/transport.ts +52 -0
  65. package/src/types.ts +726 -0
package/src/graph.ts ADDED
@@ -0,0 +1,811 @@
1
+ /**
2
+ * ProximaDB TypeScript SDK - Graph Operations
3
+ *
4
+ * Provides a fluent API for graph database operations including
5
+ * node and edge management, traversal queries, and graph analytics.
6
+ *
7
+ * Copyright 2025 ProximaDB Contributors
8
+ * Licensed under the Apache License, Version 2.0
9
+ */
10
+
11
+ import {
12
+ GraphNode,
13
+ GraphEdge,
14
+ TraversalResult,
15
+ TraversalDirection,
16
+ JsonValue,
17
+ GraphInfo,
18
+ NodeInput,
19
+ EdgeInput,
20
+ EmbeddingInput,
21
+ } from "./types";
22
+
23
+ /**
24
+ * HTTP client interface for graph operations.
25
+ *
26
+ * Wire calls route through the typed `*Request` seam methods (TD-126 Phase 4),
27
+ * which dispatch via the generated openapi-fetch transport. The generic
28
+ * `get`/`delete` + `url()` remain ONLY for `deleteEdge`, whose 3-segment route
29
+ * shape (`/edges/{source}/{target}/{rel}`) has no matching generated op (the
30
+ * spec uses `/edges/{edge_id}`); see `GraphHandle.deleteEdge`.
31
+ */
32
+ export interface GraphHttpClient {
33
+ get<T>(url: string): Promise<T>;
34
+ delete<T>(url: string): Promise<T>;
35
+ url(): string;
36
+ // Typed transport seams (route through the generated client).
37
+ createGraphRequest(body: Record<string, unknown>): Promise<unknown>;
38
+ getGraphRequest(graphId: string): Promise<unknown>;
39
+ getGraphStatsRequest(graphId: string): Promise<unknown>;
40
+ createNodeRequest(graphId: string, body: Record<string, unknown>): Promise<unknown>;
41
+ batchCreateNodesRequest(graphId: string, body: Record<string, unknown>): Promise<unknown>;
42
+ getNodeRequest(graphId: string, nodeId: string): Promise<unknown>;
43
+ deleteNodeRequest(graphId: string, nodeId: string): Promise<void>;
44
+ createEdgeRequest(graphId: string, body: Record<string, unknown>): Promise<unknown>;
45
+ batchCreateEdgesRequest(graphId: string, body: Record<string, unknown>): Promise<unknown>;
46
+ traverseGraphRequest(graphId: string, body: Record<string, unknown>): Promise<unknown>;
47
+ }
48
+
49
+ // ============================================================================
50
+ // NODE BUILDER
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Builder for creating graph nodes
55
+ */
56
+ export class NodeBuilder {
57
+ private handle: GraphHandle;
58
+ private nodeId: string | null = null;
59
+ private nodeLabel: string | null = null;
60
+ private nodeProperties: Record<string, JsonValue> = {};
61
+ private nodeVector: number[] | null = null;
62
+
63
+ constructor(handle: GraphHandle) {
64
+ this.handle = handle;
65
+ }
66
+
67
+ /**
68
+ * Set the node ID
69
+ */
70
+ id(nodeId: string): NodeBuilder {
71
+ this.nodeId = nodeId;
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Set the node label (type)
77
+ */
78
+ label(nodeLabel: string): NodeBuilder {
79
+ this.nodeLabel = nodeLabel;
80
+ return this;
81
+ }
82
+
83
+ /**
84
+ * Add a property to the node
85
+ */
86
+ property(key: string, value: JsonValue): NodeBuilder {
87
+ this.nodeProperties[key] = value;
88
+ return this;
89
+ }
90
+
91
+ /**
92
+ * Add multiple properties at once
93
+ */
94
+ properties(props: Record<string, JsonValue>): NodeBuilder {
95
+ Object.assign(this.nodeProperties, props);
96
+ return this;
97
+ }
98
+
99
+ /**
100
+ * Set the embedding vector for semantic operations
101
+ */
102
+ vector(vec: number[]): NodeBuilder {
103
+ this.nodeVector = vec;
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Execute the node addition.
109
+ *
110
+ * Wire endpoint: POST /api/v2/graphs/{graph_id}/nodes
111
+ * OpenAPI operationId: createNode
112
+ *
113
+ * Body shape (server-true, per `CreateNodeRequest`):
114
+ * `{ node: NodeInput }` — NodeInput = `{ id, labels?, properties?, embedding? }`.
115
+ */
116
+ async execute(): Promise<void> {
117
+ if (this.nodeId === null) {
118
+ throw new Error("Node ID is required");
119
+ }
120
+
121
+ const node: NodeInput = { id: this.nodeId };
122
+ if (this.nodeLabel !== null) {
123
+ node.labels = [this.nodeLabel];
124
+ }
125
+ if (Object.keys(this.nodeProperties).length > 0) {
126
+ node.properties = this.nodeProperties;
127
+ }
128
+ if (this.nodeVector !== null) {
129
+ const embedding: EmbeddingInput = { vector: this.nodeVector };
130
+ node.embedding = embedding;
131
+ }
132
+
133
+ const request = { node };
134
+ await this.handle.getClient().createNodeRequest(
135
+ this.handle.getName(),
136
+ request as unknown as Record<string, unknown>,
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Build the node without executing
142
+ */
143
+ build(): GraphNode {
144
+ if (this.nodeId === null) {
145
+ throw new Error("Node ID is required");
146
+ }
147
+
148
+ return {
149
+ id: this.nodeId,
150
+ label: this.nodeLabel ?? undefined,
151
+ properties: this.nodeProperties,
152
+ vector: this.nodeVector ?? undefined,
153
+ };
154
+ }
155
+ }
156
+
157
+ // ============================================================================
158
+ // EDGE BUILDER
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Builder for creating graph edges
163
+ */
164
+ export class EdgeBuilder {
165
+ private handle: GraphHandle;
166
+ private edgeId: string | null = null;
167
+ private sourceNode: string | null = null;
168
+ private targetNode: string | null = null;
169
+ private relationshipType: string | null = null;
170
+ private edgeProperties: Record<string, JsonValue> = {};
171
+ private edgeWeight: number | null = null;
172
+
173
+ constructor(handle: GraphHandle) {
174
+ this.handle = handle;
175
+ }
176
+
177
+ /**
178
+ * Set the edge ID (required by server `EdgeInput.id`).
179
+ */
180
+ id(edgeId: string): EdgeBuilder {
181
+ this.edgeId = edgeId;
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Set the source node ID
187
+ */
188
+ from(nodeId: string): EdgeBuilder {
189
+ this.sourceNode = nodeId;
190
+ return this;
191
+ }
192
+
193
+ /**
194
+ * Set the target node ID
195
+ */
196
+ to(nodeId: string): EdgeBuilder {
197
+ this.targetNode = nodeId;
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Set the relationship / edge type (server field: `edge_type`).
203
+ */
204
+ relationship(relType: string): EdgeBuilder {
205
+ this.relationshipType = relType;
206
+ return this;
207
+ }
208
+
209
+ /**
210
+ * Alias for `relationship(...)` matching the server-side field name.
211
+ */
212
+ edgeType(edgeType: string): EdgeBuilder {
213
+ this.relationshipType = edgeType;
214
+ return this;
215
+ }
216
+
217
+ /**
218
+ * Add a property to the edge
219
+ */
220
+ property(key: string, value: JsonValue): EdgeBuilder {
221
+ this.edgeProperties[key] = value;
222
+ return this;
223
+ }
224
+
225
+ /**
226
+ * Add multiple properties at once
227
+ */
228
+ properties(props: Record<string, JsonValue>): EdgeBuilder {
229
+ Object.assign(this.edgeProperties, props);
230
+ return this;
231
+ }
232
+
233
+ /**
234
+ * Set the edge weight
235
+ */
236
+ weight(w: number): EdgeBuilder {
237
+ this.edgeWeight = w;
238
+ return this;
239
+ }
240
+
241
+ /**
242
+ * Execute the edge addition.
243
+ *
244
+ * Wire endpoint: POST /api/v2/graphs/{graph_id}/edges
245
+ * OpenAPI operationId: createEdge
246
+ *
247
+ * Body shape (server-true, per `CreateEdgeRequest`):
248
+ * `{ edge: EdgeInput }` — EdgeInput = `{ id, from_node_id, to_node_id,
249
+ * edge_type, properties?, weight? }`.
250
+ */
251
+ async execute(): Promise<void> {
252
+ if (this.edgeId === null) {
253
+ throw new Error("Edge ID is required (call .id(...))");
254
+ }
255
+ if (this.sourceNode === null) {
256
+ throw new Error("Source node ID is required");
257
+ }
258
+ if (this.targetNode === null) {
259
+ throw new Error("Target node ID is required");
260
+ }
261
+ if (this.relationshipType === null) {
262
+ throw new Error("Edge type / relationship is required");
263
+ }
264
+
265
+ const edge: EdgeInput = {
266
+ id: this.edgeId,
267
+ from_node_id: this.sourceNode,
268
+ to_node_id: this.targetNode,
269
+ edge_type: this.relationshipType,
270
+ };
271
+ if (Object.keys(this.edgeProperties).length > 0) {
272
+ edge.properties = this.edgeProperties;
273
+ }
274
+ if (this.edgeWeight !== null) {
275
+ edge.weight = this.edgeWeight;
276
+ }
277
+
278
+ const request = { edge };
279
+ await this.handle.getClient().createEdgeRequest(
280
+ this.handle.getName(),
281
+ request as unknown as Record<string, unknown>,
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Build the edge without executing
287
+ */
288
+ build(): GraphEdge {
289
+ if (this.sourceNode === null) {
290
+ throw new Error("Source node ID is required");
291
+ }
292
+ if (this.targetNode === null) {
293
+ throw new Error("Target node ID is required");
294
+ }
295
+ if (this.relationshipType === null) {
296
+ throw new Error("Relationship type is required");
297
+ }
298
+
299
+ return {
300
+ source: this.sourceNode,
301
+ target: this.targetNode,
302
+ relationship: this.relationshipType,
303
+ properties: this.edgeProperties,
304
+ weight: this.edgeWeight ?? undefined,
305
+ };
306
+ }
307
+ }
308
+
309
+ // ============================================================================
310
+ // TRAVERSAL BUILDER
311
+ // ============================================================================
312
+
313
+ /**
314
+ * Builder for graph traversal queries
315
+ */
316
+ export class TraversalBuilder {
317
+ private handle: GraphHandle;
318
+ private startNodeId: string | null = null;
319
+ private relationshipTypes: string[] = [];
320
+ private nodeLabelsList: string[] = [];
321
+ private traversalDirection: TraversalDirection = TraversalDirection.Outgoing;
322
+ private maxDepthValue: number = 3;
323
+ private limitValue: number | null = null;
324
+ private algorithmValue: string | null = null;
325
+ private filterExpr: string | null = null;
326
+
327
+ constructor(handle: GraphHandle) {
328
+ this.handle = handle;
329
+ }
330
+
331
+ /**
332
+ * Set the starting node
333
+ */
334
+ start(nodeId: string): TraversalBuilder {
335
+ this.startNodeId = nodeId;
336
+ return this;
337
+ }
338
+
339
+ /**
340
+ * Add a relationship type to follow
341
+ */
342
+ relationship(relType: string): TraversalBuilder {
343
+ this.relationshipTypes.push(relType);
344
+ return this;
345
+ }
346
+
347
+ /**
348
+ * Add multiple relationship types
349
+ */
350
+ relationships(relTypes: string[]): TraversalBuilder {
351
+ this.relationshipTypes.push(...relTypes);
352
+ return this;
353
+ }
354
+
355
+ /**
356
+ * Set the traversal direction
357
+ */
358
+ direction(dir: TraversalDirection): TraversalBuilder {
359
+ this.traversalDirection = dir;
360
+ return this;
361
+ }
362
+
363
+ /**
364
+ * Traverse outgoing edges only
365
+ */
366
+ outgoing(): TraversalBuilder {
367
+ this.traversalDirection = TraversalDirection.Outgoing;
368
+ return this;
369
+ }
370
+
371
+ /**
372
+ * Traverse incoming edges only
373
+ */
374
+ incoming(): TraversalBuilder {
375
+ this.traversalDirection = TraversalDirection.Incoming;
376
+ return this;
377
+ }
378
+
379
+ /**
380
+ * Traverse both directions
381
+ */
382
+ both(): TraversalBuilder {
383
+ this.traversalDirection = TraversalDirection.Both;
384
+ return this;
385
+ }
386
+
387
+ /**
388
+ * Set the maximum traversal depth
389
+ */
390
+ maxDepth(depth: number): TraversalBuilder {
391
+ this.maxDepthValue = depth;
392
+ return this;
393
+ }
394
+
395
+ /**
396
+ * Set the maximum number of results
397
+ */
398
+ limit(lim: number): TraversalBuilder {
399
+ this.limitValue = lim;
400
+ return this;
401
+ }
402
+
403
+ /**
404
+ * Add a label to filter visited nodes by.
405
+ */
406
+ nodeLabel(label: string): TraversalBuilder {
407
+ this.nodeLabelsList.push(label);
408
+ return this;
409
+ }
410
+
411
+ /**
412
+ * Replace the set of node-label filters.
413
+ */
414
+ nodeLabels(labels: string[]): TraversalBuilder {
415
+ this.nodeLabelsList = [...labels];
416
+ return this;
417
+ }
418
+
419
+ /**
420
+ * Select traversal algorithm (server accepts `bfs`, `dfs`, or `shortest_path`).
421
+ */
422
+ algorithm(name: string): TraversalBuilder {
423
+ this.algorithmValue = name;
424
+ return this;
425
+ }
426
+
427
+ /**
428
+ * Add a filter expression for nodes (client-side helper, not sent on wire).
429
+ */
430
+ filter(filterStr: string): TraversalBuilder {
431
+ this.filterExpr = filterStr;
432
+ return this;
433
+ }
434
+
435
+ /**
436
+ * Execute the traversal.
437
+ *
438
+ * Wire endpoint: POST /api/v2/graphs/{graph_id}/traverse
439
+ * OpenAPI operationId: traverseGraph
440
+ *
441
+ * Body shape (server-true, per `TraverseRequest`):
442
+ * `{ start_node_id, max_depth, edge_types?, node_labels?, algorithm?, limit? }`.
443
+ * Note: spec is flat (no `graph` wrapper — `graph_id` is the URL path
444
+ * parameter) and has no `direction` field; legacy `direction` /
445
+ * `start_node` / `relationships` keys are intentionally not sent.
446
+ */
447
+ async execute(): Promise<TraversalResult> {
448
+ if (this.startNodeId === null) {
449
+ throw new Error("Start node is required for traversal");
450
+ }
451
+
452
+ const request: Record<string, unknown> = {
453
+ start_node_id: this.startNodeId,
454
+ max_depth: this.maxDepthValue,
455
+ };
456
+ if (this.relationshipTypes.length > 0) {
457
+ request.edge_types = this.relationshipTypes;
458
+ }
459
+ if (this.nodeLabelsList.length > 0) {
460
+ request.node_labels = this.nodeLabelsList;
461
+ }
462
+ if (this.algorithmValue !== null) {
463
+ request.algorithm = this.algorithmValue;
464
+ }
465
+ if (this.limitValue !== null) {
466
+ request.limit = this.limitValue;
467
+ }
468
+ // Note: direction (this.traversalDirection) and filter (this.filterExpr)
469
+ // are intentionally NOT sent — the server `TraverseRequest` does not
470
+ // accept them. The setters remain for SDK ergonomic continuity.
471
+ void this.traversalDirection;
472
+ void this.filterExpr;
473
+
474
+ const data = await this.handle.getClient().traverseGraphRequest(
475
+ this.handle.getName(),
476
+ request,
477
+ );
478
+ return data as TraversalResult;
479
+ }
480
+ }
481
+
482
+ // ============================================================================
483
+ // GRAPH HANDLE
484
+ // ============================================================================
485
+
486
+ /**
487
+ * Handle to a graph for fluent operations
488
+ */
489
+ export class GraphHandle {
490
+ private client: GraphHttpClient;
491
+ private graphName: string;
492
+
493
+ constructor(client: GraphHttpClient, name: string) {
494
+ this.client = client;
495
+ this.graphName = name;
496
+ }
497
+
498
+ /**
499
+ * Get the graph name
500
+ */
501
+ getName(): string {
502
+ return this.graphName;
503
+ }
504
+
505
+ /**
506
+ * Get the HTTP client (internal use)
507
+ */
508
+ getClient(): GraphHttpClient {
509
+ return this.client;
510
+ }
511
+
512
+ /**
513
+ * Start building a node addition
514
+ */
515
+ addNode(): NodeBuilder {
516
+ return new NodeBuilder(this);
517
+ }
518
+
519
+ /**
520
+ * Start building an edge addition
521
+ */
522
+ addEdge(): EdgeBuilder {
523
+ return new EdgeBuilder(this);
524
+ }
525
+
526
+ /**
527
+ * Start building a traversal query
528
+ */
529
+ traverse(): TraversalBuilder {
530
+ return new TraversalBuilder(this);
531
+ }
532
+
533
+ /**
534
+ * Add a batch of nodes.
535
+ *
536
+ * Wire endpoint: POST /api/v2/graphs/{graph_id}/nodes/batch
537
+ * OpenAPI operationId: batchCreateNodes
538
+ *
539
+ * Body shape (server-true, per `BatchCreateNodesRequest`):
540
+ * `{ nodes: [NodeInput, ...] }`. `graph_id` lives in the URL path.
541
+ *
542
+ * Accepts either spec-true `NodeInput[]` or legacy `GraphNode[]`; the
543
+ * latter is normalized (singular `label` → `labels: [label]`,
544
+ * `vector` → `embedding.vector`).
545
+ */
546
+ async addNodes(nodes: NodeInput[] | GraphNode[]): Promise<number> {
547
+ const normalized: NodeInput[] = nodes.map(n => normalizeNodeInput(n));
548
+ const request = { nodes: normalized };
549
+ const response = (await this.client.batchCreateNodesRequest(
550
+ this.graphName,
551
+ request as unknown as Record<string, unknown>,
552
+ )) as { data?: { count?: number }; count?: number } | null;
553
+ return response?.data?.count ?? response?.count ?? normalized.length;
554
+ }
555
+
556
+ /**
557
+ * Add a batch of edges.
558
+ *
559
+ * Wire endpoint: POST /api/v2/graphs/{graph_id}/edges/batch
560
+ * OpenAPI operationId: batchCreateEdges
561
+ *
562
+ * Body shape (server-true, per `BatchCreateEdgesRequest`):
563
+ * `{ edges: [EdgeInput, ...] }`. `graph_id` lives in the URL path.
564
+ *
565
+ * Accepts either spec-true `EdgeInput[]` or legacy `GraphEdge[]`; the
566
+ * latter is normalized (`source` → `from_node_id`, `target` →
567
+ * `to_node_id`, `relationship` → `edge_type`). Legacy `GraphEdge` has no
568
+ * `id` field; callers MUST upgrade to `EdgeInput` for batch use (the
569
+ * normalizer throws if `id` is missing).
570
+ */
571
+ async addEdges(edges: EdgeInput[] | GraphEdge[]): Promise<number> {
572
+ const normalized: EdgeInput[] = edges.map(e => normalizeEdgeInput(e));
573
+ const request = { edges: normalized };
574
+ const response = (await this.client.batchCreateEdgesRequest(
575
+ this.graphName,
576
+ request as unknown as Record<string, unknown>,
577
+ )) as { data?: { count?: number }; count?: number } | null;
578
+ return response?.data?.count ?? response?.count ?? normalized.length;
579
+ }
580
+
581
+ /**
582
+ * Get a node by ID
583
+ */
584
+ async getNode(nodeId: string): Promise<GraphNode | null> {
585
+ try {
586
+ const data = await this.client.getNodeRequest(this.graphName, nodeId);
587
+ return data as GraphNode;
588
+ } catch (e: unknown) {
589
+ // The facade maps a 404 to a ProximaDBError (statusCode 404, message =
590
+ // server body). Treat "not found" as a null result, preserving the
591
+ // pre-rebase behavior; match on status or the body text.
592
+ const status = (e as { statusCode?: number }).statusCode;
593
+ if (status === 404) {
594
+ return null;
595
+ }
596
+ if (e instanceof Error && e.message.includes("404")) {
597
+ return null;
598
+ }
599
+ throw e;
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Delete a node by ID
605
+ */
606
+ async deleteNode(nodeId: string): Promise<void> {
607
+ await this.client.deleteNodeRequest(this.graphName, nodeId);
608
+ }
609
+
610
+ // deleteEdge is intentionally LEFT facade-built (generic URL path): the
611
+ // generated REST client has no matching typed op for this 3-segment route
612
+ // shape. The OpenAPI spec / generated client model edge deletion as
613
+ // `DELETE /api/v2/graphs/{graph_id}/edges/{edge_id}` (single `edge_id`),
614
+ // whereas this SDK's public signature is `(source, target, relationship)` →
615
+ // `/edges/{source}/{target}/{relationship}`. Until the SDK signature is
616
+ // reconciled to `{edge_id}` (separate change), this cannot route through the
617
+ // generated client. Tracked separately.
618
+ /**
619
+ * Delete an edge
620
+ */
621
+ async deleteEdge(source: string, target: string, relationship: string): Promise<void> {
622
+ const url = this.client.url() + "/api/v2/graphs/" + this.graphName + "/edges/" + source + "/" + target + "/" + relationship;
623
+ await this.client.delete<unknown>(url);
624
+ }
625
+
626
+ /**
627
+ * Get graph statistics.
628
+ *
629
+ * Routed through the generated typed transport (TD-126 Phase 4).
630
+ * Wire endpoint: GET /api/v2/graphs/{graph_id} (getGraph)
631
+ */
632
+ async info(): Promise<GraphInfo> {
633
+ const data = await this.client.getGraphRequest(this.graphName);
634
+ return data as GraphInfo;
635
+ }
636
+ }
637
+
638
+ // ============================================================================
639
+ // GRAPH BUILDER
640
+ // ============================================================================
641
+
642
+ /**
643
+ * Builder for creating graphs
644
+ */
645
+ export class GraphBuilder {
646
+ private client: GraphHttpClient;
647
+ private graphId: string;
648
+ private graphDisplayName: string | null = null;
649
+ private graphDescription: string | null = null;
650
+
651
+ constructor(client: GraphHttpClient, graphId: string) {
652
+ this.client = client;
653
+ this.graphId = graphId;
654
+ }
655
+
656
+ /**
657
+ * Set an optional human-readable name (defaults server-side to graph_id).
658
+ */
659
+ name(displayName: string): GraphBuilder {
660
+ this.graphDisplayName = displayName;
661
+ return this;
662
+ }
663
+
664
+ /**
665
+ * Set the graph description
666
+ */
667
+ description(desc: string): GraphBuilder {
668
+ this.graphDescription = desc;
669
+ return this;
670
+ }
671
+
672
+ /**
673
+ * Execute the graph creation.
674
+ *
675
+ * Wire endpoint: POST /api/v2/graphs
676
+ * OpenAPI operationId: createGraph
677
+ *
678
+ * Body shape (server-true, per `CreateGraphRequest`):
679
+ * `{ graph_id, name?, description? }`.
680
+ */
681
+ async execute(): Promise<void> {
682
+ const request: Record<string, unknown> = { graph_id: this.graphId };
683
+ if (this.graphDisplayName !== null) {
684
+ request.name = this.graphDisplayName;
685
+ }
686
+ if (this.graphDescription !== null) {
687
+ request.description = this.graphDescription;
688
+ }
689
+
690
+ await this.client.createGraphRequest(request);
691
+ }
692
+ }
693
+
694
+ // ============================================================================
695
+ // INTERNAL: legacy -> spec normalizers
696
+ // ============================================================================
697
+
698
+ function normalizeNodeInput(input: NodeInput | GraphNode): NodeInput {
699
+ // Distinguish legacy `GraphNode` (has singular `label` and/or `vector`) from
700
+ // spec-true `NodeInput` (has `labels[]` / `embedding`). Field absence is
701
+ // fine — `NodeInput` only requires `id`.
702
+ const out: NodeInput = { id: input.id };
703
+ // Inline structural check avoids a hard cross-cast.
704
+ const legacy = input as GraphNode;
705
+ const spec = input as NodeInput;
706
+ if (spec.labels !== undefined) {
707
+ out.labels = spec.labels;
708
+ } else if (legacy.label !== undefined) {
709
+ out.labels = [legacy.label];
710
+ }
711
+ if (spec.properties !== undefined) {
712
+ out.properties = spec.properties;
713
+ } else if (legacy.properties !== undefined) {
714
+ out.properties = legacy.properties;
715
+ }
716
+ if (spec.embedding !== undefined) {
717
+ out.embedding = spec.embedding;
718
+ } else if (legacy.vector !== undefined) {
719
+ out.embedding = { vector: legacy.vector };
720
+ }
721
+ return out;
722
+ }
723
+
724
+ function normalizeEdgeInput(input: EdgeInput | GraphEdge): EdgeInput {
725
+ const spec = input as EdgeInput;
726
+ const legacy = input as GraphEdge;
727
+ // Spec edge has `id` (required), legacy has none. If absent on a legacy
728
+ // value, surface a hard error — server `EdgeInput.id` is required.
729
+ if (spec.id === undefined) {
730
+ throw new Error(
731
+ "addEdges: edge.id is required by EdgeInput (legacy GraphEdge has no id; pass an EdgeInput)",
732
+ );
733
+ }
734
+ const out: EdgeInput = {
735
+ id: spec.id,
736
+ from_node_id: spec.from_node_id ?? legacy.source,
737
+ to_node_id: spec.to_node_id ?? legacy.target,
738
+ edge_type: spec.edge_type ?? legacy.relationship,
739
+ };
740
+ if (spec.properties !== undefined) {
741
+ out.properties = spec.properties;
742
+ } else if (legacy.properties !== undefined) {
743
+ out.properties = legacy.properties;
744
+ }
745
+ if (spec.weight !== undefined) {
746
+ out.weight = spec.weight;
747
+ } else if (legacy.weight !== undefined) {
748
+ out.weight = legacy.weight;
749
+ }
750
+ return out;
751
+ }
752
+
753
+ // ============================================================================
754
+ // CONVENIENCE FUNCTIONS
755
+ // ============================================================================
756
+
757
+ /**
758
+ * Create a new GraphNode
759
+ */
760
+ export function createNode(id: string): GraphNode {
761
+ return {
762
+ id,
763
+ properties: {},
764
+ };
765
+ }
766
+
767
+ /**
768
+ * Create a new GraphNode with builder pattern
769
+ */
770
+ export function node(id: string): {
771
+ label: (l: string) => ReturnType<typeof node>;
772
+ property: (k: string, v: JsonValue) => ReturnType<typeof node>;
773
+ vector: (v: number[]) => ReturnType<typeof node>;
774
+ build: () => GraphNode;
775
+ } {
776
+ const n: GraphNode = { id, properties: {} };
777
+ return {
778
+ label: (l: string) => { n.label = l; return node(id); },
779
+ property: (k: string, v: JsonValue) => { n.properties[k] = v; return node(id); },
780
+ vector: (v: number[]) => { n.vector = v; return node(id); },
781
+ build: () => n,
782
+ };
783
+ }
784
+
785
+ /**
786
+ * Create a new GraphEdge
787
+ */
788
+ export function createEdge(source: string, target: string, relationship: string): GraphEdge {
789
+ return {
790
+ source,
791
+ target,
792
+ relationship,
793
+ properties: {},
794
+ };
795
+ }
796
+
797
+ /**
798
+ * Create a new GraphEdge with builder pattern
799
+ */
800
+ export function edge(source: string, target: string, relationship: string): {
801
+ property: (k: string, v: JsonValue) => ReturnType<typeof edge>;
802
+ weight: (w: number) => ReturnType<typeof edge>;
803
+ build: () => GraphEdge;
804
+ } {
805
+ const e: GraphEdge = { source, target, relationship, properties: {} };
806
+ return {
807
+ property: (k: string, v: JsonValue) => { e.properties[k] = v; return edge(source, target, relationship); },
808
+ weight: (w: number) => { e.weight = w; return edge(source, target, relationship); },
809
+ build: () => e,
810
+ };
811
+ }