@wiscale/velesdb-sdk 1.2.0 → 1.4.1

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/dist/index.mjs CHANGED
@@ -133,7 +133,7 @@ var WasmBackend = class {
133
133
  throw new NotFoundError(`Collection '${collectionName}'`);
134
134
  }
135
135
  for (const doc of docs) {
136
- const vectorLen = Array.isArray(doc.vector) ? doc.vector.length : doc.vector.length;
136
+ const vectorLen = doc.vector.length;
137
137
  if (vectorLen !== collection.config.dimension) {
138
138
  throw new VelesDBError(
139
139
  `Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
@@ -239,7 +239,7 @@ var WasmBackend = class {
239
239
  "NOT_SUPPORTED"
240
240
  );
241
241
  }
242
- async query(_queryString, _params) {
242
+ async query(_collection, _queryString, _params, _options) {
243
243
  throw new VelesDBError(
244
244
  "VelesQL queries are not supported in WASM backend. Use REST backend for query support.",
245
245
  "NOT_SUPPORTED"
@@ -289,6 +289,60 @@ var WasmBackend = class {
289
289
  }
290
290
  return Math.abs(hash);
291
291
  }
292
+ // ========================================================================
293
+ // Index Management (EPIC-009) - Stubs for WASM backend
294
+ // Note: Full implementation requires velesdb-wasm support
295
+ // ========================================================================
296
+ async createIndex(_collection, _options) {
297
+ this.ensureInitialized();
298
+ throw new Error(
299
+ "WasmBackend: createIndex is not yet supported. Index operations require the REST backend with velesdb-server."
300
+ );
301
+ }
302
+ async listIndexes(_collection) {
303
+ this.ensureInitialized();
304
+ return [];
305
+ }
306
+ async hasIndex(_collection, _label, _property) {
307
+ this.ensureInitialized();
308
+ return false;
309
+ }
310
+ async dropIndex(_collection, _label, _property) {
311
+ this.ensureInitialized();
312
+ return false;
313
+ }
314
+ // ========================================================================
315
+ // Knowledge Graph (EPIC-016 US-041) - Stubs for WASM backend
316
+ // Note: Graph operations require server-side EdgeStore
317
+ // ========================================================================
318
+ async addEdge(_collection, _edge) {
319
+ this.ensureInitialized();
320
+ throw new VelesDBError(
321
+ "Knowledge Graph operations are not supported in WASM backend. Use REST backend for graph features.",
322
+ "NOT_SUPPORTED"
323
+ );
324
+ }
325
+ async getEdges(_collection, _options) {
326
+ this.ensureInitialized();
327
+ throw new VelesDBError(
328
+ "Knowledge Graph operations are not supported in WASM backend. Use REST backend for graph features.",
329
+ "NOT_SUPPORTED"
330
+ );
331
+ }
332
+ async traverseGraph(_collection, _request) {
333
+ this.ensureInitialized();
334
+ throw new VelesDBError(
335
+ "Graph traversal is not supported in WASM backend. Use REST backend for graph features.",
336
+ "NOT_SUPPORTED"
337
+ );
338
+ }
339
+ async getNodeDegree(_collection, _nodeId) {
340
+ this.ensureInitialized();
341
+ throw new VelesDBError(
342
+ "Graph degree query is not supported in WASM backend. Use REST backend for graph features.",
343
+ "NOT_SUPPORTED"
344
+ );
345
+ }
292
346
  };
293
347
 
294
348
  // src/backends/rest.ts
@@ -324,6 +378,64 @@ var RestBackend = class {
324
378
  throw new ConnectionError("REST backend not initialized");
325
379
  }
326
380
  }
381
+ mapStatusToErrorCode(status) {
382
+ switch (status) {
383
+ case 400:
384
+ return "BAD_REQUEST";
385
+ case 401:
386
+ return "UNAUTHORIZED";
387
+ case 403:
388
+ return "FORBIDDEN";
389
+ case 404:
390
+ return "NOT_FOUND";
391
+ case 409:
392
+ return "CONFLICT";
393
+ case 429:
394
+ return "RATE_LIMITED";
395
+ case 500:
396
+ return "INTERNAL_ERROR";
397
+ case 503:
398
+ return "SERVICE_UNAVAILABLE";
399
+ default:
400
+ return "UNKNOWN_ERROR";
401
+ }
402
+ }
403
+ extractErrorPayload(data) {
404
+ if (!data || typeof data !== "object") {
405
+ return {};
406
+ }
407
+ const payload = data;
408
+ const code = typeof payload.code === "string" ? payload.code : void 0;
409
+ const messageField = payload.message ?? payload.error;
410
+ const message = typeof messageField === "string" ? messageField : void 0;
411
+ return { code, message };
412
+ }
413
+ /**
414
+ * Parse node ID safely to handle u64 values above Number.MAX_SAFE_INTEGER.
415
+ * Returns bigint for large values, number for safe values.
416
+ */
417
+ parseNodeId(value) {
418
+ if (value === null || value === void 0) {
419
+ return 0;
420
+ }
421
+ if (typeof value === "bigint") {
422
+ return value;
423
+ }
424
+ if (typeof value === "string") {
425
+ const num = Number(value);
426
+ if (num > Number.MAX_SAFE_INTEGER) {
427
+ return BigInt(value);
428
+ }
429
+ return num;
430
+ }
431
+ if (typeof value === "number") {
432
+ if (value > Number.MAX_SAFE_INTEGER) {
433
+ return value;
434
+ }
435
+ return value;
436
+ }
437
+ return 0;
438
+ }
327
439
  async request(method, path, body) {
328
440
  const url = `${this.baseUrl}${path}`;
329
441
  const headers = {
@@ -342,12 +454,13 @@ var RestBackend = class {
342
454
  signal: controller.signal
343
455
  });
344
456
  clearTimeout(timeoutId);
345
- const data = await response.json();
457
+ const data = await response.json().catch(() => ({}));
346
458
  if (!response.ok) {
459
+ const errorPayload = this.extractErrorPayload(data);
347
460
  return {
348
461
  error: {
349
- code: data.code ?? "UNKNOWN_ERROR",
350
- message: data.message ?? `HTTP ${response.status}`
462
+ code: errorPayload.code ?? this.mapStatusToErrorCode(response.status),
463
+ message: errorPayload.message ?? `HTTP ${response.status}`
351
464
  }
352
465
  };
353
466
  }
@@ -414,11 +527,13 @@ var RestBackend = class {
414
527
  const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
415
528
  const response = await this.request(
416
529
  "POST",
417
- `/collections/${encodeURIComponent(collection)}/vectors`,
530
+ `/collections/${encodeURIComponent(collection)}/points`,
418
531
  {
419
- id: doc.id,
420
- vector,
421
- payload: doc.payload
532
+ points: [{
533
+ id: doc.id,
534
+ vector,
535
+ payload: doc.payload
536
+ }]
422
537
  }
423
538
  );
424
539
  if (response.error) {
@@ -437,8 +552,8 @@ var RestBackend = class {
437
552
  }));
438
553
  const response = await this.request(
439
554
  "POST",
440
- `/collections/${encodeURIComponent(collection)}/vectors/batch`,
441
- { vectors }
555
+ `/collections/${encodeURIComponent(collection)}/points`,
556
+ { points: vectors }
442
557
  );
443
558
  if (response.error) {
444
559
  if (response.error.code === "NOT_FOUND") {
@@ -492,7 +607,7 @@ var RestBackend = class {
492
607
  this.ensureInitialized();
493
608
  const response = await this.request(
494
609
  "DELETE",
495
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
610
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
496
611
  );
497
612
  if (response.error) {
498
613
  if (response.error.code === "NOT_FOUND") {
@@ -506,7 +621,7 @@ var RestBackend = class {
506
621
  this.ensureInitialized();
507
622
  const response = await this.request(
508
623
  "GET",
509
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
624
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
510
625
  );
511
626
  if (response.error) {
512
627
  if (response.error.code === "NOT_FOUND") {
@@ -557,7 +672,7 @@ var RestBackend = class {
557
672
  }
558
673
  return response.data?.results ?? [];
559
674
  }
560
- async query(queryString, params) {
675
+ async query(collection, queryString, params, _options) {
561
676
  this.ensureInitialized();
562
677
  const response = await this.request(
563
678
  "POST",
@@ -568,9 +683,32 @@ var RestBackend = class {
568
683
  }
569
684
  );
570
685
  if (response.error) {
686
+ if (response.error.code === "NOT_FOUND") {
687
+ throw new NotFoundError(`Collection '${collection}'`);
688
+ }
571
689
  throw new VelesDBError(response.error.message, response.error.code);
572
690
  }
573
- return response.data?.results ?? [];
691
+ const rawData = response.data;
692
+ return {
693
+ results: (rawData?.results ?? []).map((r) => ({
694
+ // Server returns `id` (u64), map to nodeId with precision handling
695
+ nodeId: this.parseNodeId(r.id ?? r.node_id ?? r.nodeId),
696
+ // Server returns `score`, map to vectorScore (primary score for SELECT queries)
697
+ vectorScore: r.score ?? r.vector_score ?? r.vectorScore,
698
+ // graph_score not returned by SELECT queries, only by future MATCH queries
699
+ graphScore: r.graph_score ?? r.graphScore,
700
+ // Use score as fusedScore for compatibility
701
+ fusedScore: r.score ?? r.fused_score ?? r.fusedScore ?? 0,
702
+ // payload maps to bindings for compatibility
703
+ bindings: r.payload ?? r.bindings ?? {},
704
+ columnData: r.column_data ?? r.columnData
705
+ })),
706
+ stats: {
707
+ executionTimeMs: rawData?.timing_ms ?? 0,
708
+ strategy: "select",
709
+ scannedNodes: rawData?.rows_returned ?? 0
710
+ }
711
+ };
574
712
  }
575
713
  async multiQuerySearch(collection, vectors, options) {
576
714
  this.ensureInitialized();
@@ -583,8 +721,8 @@ var RestBackend = class {
583
721
  {
584
722
  vectors: formattedVectors,
585
723
  top_k: options?.k ?? 10,
586
- fusion: options?.fusion ?? "rrf",
587
- fusion_params: options?.fusionParams ?? { k: 60 },
724
+ strategy: options?.fusion ?? "rrf",
725
+ rrf_k: options?.fusionParams?.k ?? 60,
588
726
  filter: options?.filter
589
727
  }
590
728
  );
@@ -626,6 +764,158 @@ var RestBackend = class {
626
764
  async close() {
627
765
  this._initialized = false;
628
766
  }
767
+ // ========================================================================
768
+ // Index Management (EPIC-009)
769
+ // ========================================================================
770
+ async createIndex(collection, options) {
771
+ this.ensureInitialized();
772
+ const response = await this.request(
773
+ "POST",
774
+ `/collections/${encodeURIComponent(collection)}/indexes`,
775
+ {
776
+ label: options.label,
777
+ property: options.property,
778
+ index_type: options.indexType ?? "hash"
779
+ }
780
+ );
781
+ if (response.error) {
782
+ if (response.error.code === "NOT_FOUND") {
783
+ throw new NotFoundError(`Collection '${collection}'`);
784
+ }
785
+ throw new VelesDBError(response.error.message, response.error.code);
786
+ }
787
+ }
788
+ async listIndexes(collection) {
789
+ this.ensureInitialized();
790
+ const response = await this.request(
791
+ "GET",
792
+ `/collections/${encodeURIComponent(collection)}/indexes`
793
+ );
794
+ if (response.error) {
795
+ if (response.error.code === "NOT_FOUND") {
796
+ throw new NotFoundError(`Collection '${collection}'`);
797
+ }
798
+ throw new VelesDBError(response.error.message, response.error.code);
799
+ }
800
+ return (response.data?.indexes ?? []).map((idx) => ({
801
+ label: idx.label,
802
+ property: idx.property,
803
+ indexType: idx.index_type,
804
+ cardinality: idx.cardinality,
805
+ memoryBytes: idx.memory_bytes
806
+ }));
807
+ }
808
+ async hasIndex(collection, label, property) {
809
+ const indexes = await this.listIndexes(collection);
810
+ return indexes.some((idx) => idx.label === label && idx.property === property);
811
+ }
812
+ async dropIndex(collection, label, property) {
813
+ this.ensureInitialized();
814
+ const response = await this.request(
815
+ "DELETE",
816
+ `/collections/${encodeURIComponent(collection)}/indexes/${encodeURIComponent(label)}/${encodeURIComponent(property)}`
817
+ );
818
+ if (response.error) {
819
+ if (response.error.code === "NOT_FOUND") {
820
+ return false;
821
+ }
822
+ throw new VelesDBError(response.error.message, response.error.code);
823
+ }
824
+ return response.data?.dropped ?? true;
825
+ }
826
+ // ========================================================================
827
+ // Knowledge Graph (EPIC-016 US-041)
828
+ // ========================================================================
829
+ async addEdge(collection, edge) {
830
+ this.ensureInitialized();
831
+ const response = await this.request(
832
+ "POST",
833
+ `/collections/${encodeURIComponent(collection)}/graph/edges`,
834
+ {
835
+ id: edge.id,
836
+ source: edge.source,
837
+ target: edge.target,
838
+ label: edge.label,
839
+ properties: edge.properties ?? {}
840
+ }
841
+ );
842
+ if (response.error) {
843
+ if (response.error.code === "NOT_FOUND") {
844
+ throw new NotFoundError(`Collection '${collection}'`);
845
+ }
846
+ throw new VelesDBError(response.error.message, response.error.code);
847
+ }
848
+ }
849
+ async getEdges(collection, options) {
850
+ this.ensureInitialized();
851
+ const queryParams = options?.label ? `?label=${encodeURIComponent(options.label)}` : "";
852
+ const response = await this.request(
853
+ "GET",
854
+ `/collections/${encodeURIComponent(collection)}/graph/edges${queryParams}`
855
+ );
856
+ if (response.error) {
857
+ if (response.error.code === "NOT_FOUND") {
858
+ throw new NotFoundError(`Collection '${collection}'`);
859
+ }
860
+ throw new VelesDBError(response.error.message, response.error.code);
861
+ }
862
+ return response.data?.edges ?? [];
863
+ }
864
+ // ========================================================================
865
+ // Graph Traversal (EPIC-016 US-050)
866
+ // ========================================================================
867
+ async traverseGraph(collection, request) {
868
+ this.ensureInitialized();
869
+ const response = await this.request(
870
+ "POST",
871
+ `/collections/${encodeURIComponent(collection)}/graph/traverse`,
872
+ {
873
+ source: request.source,
874
+ strategy: request.strategy ?? "bfs",
875
+ max_depth: request.maxDepth ?? 3,
876
+ limit: request.limit ?? 100,
877
+ cursor: request.cursor,
878
+ rel_types: request.relTypes ?? []
879
+ }
880
+ );
881
+ if (response.error) {
882
+ if (response.error.code === "NOT_FOUND") {
883
+ throw new NotFoundError(`Collection '${collection}'`);
884
+ }
885
+ throw new VelesDBError(response.error.message, response.error.code);
886
+ }
887
+ const data = response.data;
888
+ return {
889
+ results: data.results.map((r) => ({
890
+ targetId: r.target_id,
891
+ depth: r.depth,
892
+ path: r.path
893
+ })),
894
+ nextCursor: data.next_cursor ?? void 0,
895
+ hasMore: data.has_more,
896
+ stats: {
897
+ visited: data.stats.visited,
898
+ depthReached: data.stats.depth_reached
899
+ }
900
+ };
901
+ }
902
+ async getNodeDegree(collection, nodeId) {
903
+ this.ensureInitialized();
904
+ const response = await this.request(
905
+ "GET",
906
+ `/collections/${encodeURIComponent(collection)}/graph/nodes/${nodeId}/degree`
907
+ );
908
+ if (response.error) {
909
+ if (response.error.code === "NOT_FOUND") {
910
+ throw new NotFoundError(`Collection '${collection}'`);
911
+ }
912
+ throw new VelesDBError(response.error.message, response.error.code);
913
+ }
914
+ return {
915
+ inDegree: response.data?.in_degree ?? 0,
916
+ outDegree: response.data?.out_degree ?? 0
917
+ };
918
+ }
629
919
  };
630
920
 
631
921
  // src/client.ts
@@ -879,18 +1169,36 @@ var VelesDB = class {
879
1169
  return this.backend.hybridSearch(collection, vector, textQuery, options);
880
1170
  }
881
1171
  /**
882
- * Execute a VelesQL query
1172
+ * Execute a VelesQL multi-model query (EPIC-031 US-011)
883
1173
  *
1174
+ * Supports hybrid vector + graph queries with VelesQL syntax.
1175
+ *
1176
+ * @param collection - Collection name
884
1177
  * @param queryString - VelesQL query string
885
- * @param params - Optional query parameters
886
- * @returns Query results
1178
+ * @param params - Query parameters (vectors, scalars)
1179
+ * @param options - Query options (timeout, streaming)
1180
+ * @returns Query response with results and execution stats
1181
+ *
1182
+ * @example
1183
+ * ```typescript
1184
+ * const response = await db.query('docs', `
1185
+ * MATCH (d:Doc) WHERE vector NEAR $q LIMIT 20
1186
+ * `, { q: queryVector });
1187
+ *
1188
+ * for (const r of response.results) {
1189
+ * console.log(`Node ${r.nodeId}: ${r.fusedScore}`);
1190
+ * }
1191
+ * ```
887
1192
  */
888
- async query(queryString, params) {
1193
+ async query(collection, queryString, params, options) {
889
1194
  this.ensureInitialized();
1195
+ if (!collection || typeof collection !== "string") {
1196
+ throw new ValidationError("Collection name must be a non-empty string");
1197
+ }
890
1198
  if (!queryString || typeof queryString !== "string") {
891
1199
  throw new ValidationError("Query string must be a non-empty string");
892
1200
  }
893
- return this.backend.query(queryString, params);
1201
+ return this.backend.query(collection, queryString, params, options);
894
1202
  }
895
1203
  /**
896
1204
  * Multi-query fusion search combining results from multiple query vectors
@@ -965,7 +1273,471 @@ var VelesDB = class {
965
1273
  get backendType() {
966
1274
  return this.config.backend;
967
1275
  }
1276
+ // ========================================================================
1277
+ // Index Management (EPIC-009)
1278
+ // ========================================================================
1279
+ /**
1280
+ * Create a property index for O(1) equality lookups or O(log n) range queries
1281
+ *
1282
+ * @param collection - Collection name
1283
+ * @param options - Index configuration (label, property, indexType)
1284
+ *
1285
+ * @example
1286
+ * ```typescript
1287
+ * // Create hash index for fast email lookups
1288
+ * await db.createIndex('users', { label: 'Person', property: 'email' });
1289
+ *
1290
+ * // Create range index for timestamp queries
1291
+ * await db.createIndex('events', { label: 'Event', property: 'timestamp', indexType: 'range' });
1292
+ * ```
1293
+ */
1294
+ async createIndex(collection, options) {
1295
+ this.ensureInitialized();
1296
+ if (!options.label || !options.property) {
1297
+ throw new ValidationError("Index requires label and property");
1298
+ }
1299
+ await this.backend.createIndex(collection, options);
1300
+ }
1301
+ /**
1302
+ * List all indexes on a collection
1303
+ *
1304
+ * @param collection - Collection name
1305
+ * @returns Array of index information
1306
+ */
1307
+ async listIndexes(collection) {
1308
+ this.ensureInitialized();
1309
+ return this.backend.listIndexes(collection);
1310
+ }
1311
+ /**
1312
+ * Check if an index exists
1313
+ *
1314
+ * @param collection - Collection name
1315
+ * @param label - Node label
1316
+ * @param property - Property name
1317
+ * @returns true if index exists
1318
+ */
1319
+ async hasIndex(collection, label, property) {
1320
+ this.ensureInitialized();
1321
+ return this.backend.hasIndex(collection, label, property);
1322
+ }
1323
+ /**
1324
+ * Drop an index
1325
+ *
1326
+ * @param collection - Collection name
1327
+ * @param label - Node label
1328
+ * @param property - Property name
1329
+ * @returns true if index was dropped, false if it didn't exist
1330
+ */
1331
+ async dropIndex(collection, label, property) {
1332
+ this.ensureInitialized();
1333
+ return this.backend.dropIndex(collection, label, property);
1334
+ }
1335
+ // ========================================================================
1336
+ // Knowledge Graph (EPIC-016 US-041)
1337
+ // ========================================================================
1338
+ /**
1339
+ * Add an edge to the collection's knowledge graph
1340
+ *
1341
+ * @param collection - Collection name
1342
+ * @param edge - Edge to add (id, source, target, label, properties)
1343
+ *
1344
+ * @example
1345
+ * ```typescript
1346
+ * await db.addEdge('social', {
1347
+ * id: 1,
1348
+ * source: 100,
1349
+ * target: 200,
1350
+ * label: 'FOLLOWS',
1351
+ * properties: { since: '2024-01-01' }
1352
+ * });
1353
+ * ```
1354
+ */
1355
+ async addEdge(collection, edge) {
1356
+ this.ensureInitialized();
1357
+ if (!edge.label || typeof edge.label !== "string") {
1358
+ throw new ValidationError("Edge label is required and must be a string");
1359
+ }
1360
+ if (typeof edge.source !== "number" || typeof edge.target !== "number") {
1361
+ throw new ValidationError("Edge source and target must be numbers");
1362
+ }
1363
+ await this.backend.addEdge(collection, edge);
1364
+ }
1365
+ /**
1366
+ * Get edges from the collection's knowledge graph
1367
+ *
1368
+ * @param collection - Collection name
1369
+ * @param options - Query options (filter by label)
1370
+ * @returns Array of edges
1371
+ *
1372
+ * @example
1373
+ * ```typescript
1374
+ * // Get all edges with label "FOLLOWS"
1375
+ * const edges = await db.getEdges('social', { label: 'FOLLOWS' });
1376
+ * ```
1377
+ */
1378
+ async getEdges(collection, options) {
1379
+ this.ensureInitialized();
1380
+ return this.backend.getEdges(collection, options);
1381
+ }
1382
+ // ========================================================================
1383
+ // Graph Traversal (EPIC-016 US-050)
1384
+ // ========================================================================
1385
+ /**
1386
+ * Traverse the graph using BFS or DFS from a source node
1387
+ *
1388
+ * @param collection - Collection name
1389
+ * @param request - Traversal request options
1390
+ * @returns Traversal response with results and stats
1391
+ *
1392
+ * @example
1393
+ * ```typescript
1394
+ * // BFS traversal from node 100
1395
+ * const result = await db.traverseGraph('social', {
1396
+ * source: 100,
1397
+ * strategy: 'bfs',
1398
+ * maxDepth: 3,
1399
+ * limit: 100,
1400
+ * relTypes: ['FOLLOWS', 'KNOWS']
1401
+ * });
1402
+ *
1403
+ * for (const node of result.results) {
1404
+ * console.log(`Reached node ${node.targetId} at depth ${node.depth}`);
1405
+ * }
1406
+ * ```
1407
+ */
1408
+ async traverseGraph(collection, request) {
1409
+ this.ensureInitialized();
1410
+ if (typeof request.source !== "number") {
1411
+ throw new ValidationError("Source node ID must be a number");
1412
+ }
1413
+ if (request.strategy && !["bfs", "dfs"].includes(request.strategy)) {
1414
+ throw new ValidationError("Strategy must be 'bfs' or 'dfs'");
1415
+ }
1416
+ return this.backend.traverseGraph(collection, request);
1417
+ }
1418
+ /**
1419
+ * Get the in-degree and out-degree of a node
1420
+ *
1421
+ * @param collection - Collection name
1422
+ * @param nodeId - Node ID
1423
+ * @returns Degree response with inDegree and outDegree
1424
+ *
1425
+ * @example
1426
+ * ```typescript
1427
+ * const degree = await db.getNodeDegree('social', 100);
1428
+ * console.log(`In: ${degree.inDegree}, Out: ${degree.outDegree}`);
1429
+ * ```
1430
+ */
1431
+ async getNodeDegree(collection, nodeId) {
1432
+ this.ensureInitialized();
1433
+ if (typeof nodeId !== "number") {
1434
+ throw new ValidationError("Node ID must be a number");
1435
+ }
1436
+ return this.backend.getNodeDegree(collection, nodeId);
1437
+ }
1438
+ };
1439
+
1440
+ // src/query-builder.ts
1441
+ var VelesQLBuilder = class _VelesQLBuilder {
1442
+ constructor(state) {
1443
+ this.state = {
1444
+ matchClauses: state?.matchClauses ?? [],
1445
+ whereClauses: state?.whereClauses ?? [],
1446
+ whereOperators: state?.whereOperators ?? [],
1447
+ params: state?.params ?? {},
1448
+ limitValue: state?.limitValue,
1449
+ offsetValue: state?.offsetValue,
1450
+ orderByClause: state?.orderByClause,
1451
+ returnClause: state?.returnClause,
1452
+ fusionOptions: state?.fusionOptions,
1453
+ currentNode: state?.currentNode,
1454
+ pendingRel: state?.pendingRel
1455
+ };
1456
+ }
1457
+ clone(updates) {
1458
+ return new _VelesQLBuilder({
1459
+ ...this.state,
1460
+ matchClauses: [...this.state.matchClauses],
1461
+ whereClauses: [...this.state.whereClauses],
1462
+ whereOperators: [...this.state.whereOperators],
1463
+ params: { ...this.state.params },
1464
+ ...updates
1465
+ });
1466
+ }
1467
+ /**
1468
+ * Start a MATCH clause with a node pattern
1469
+ *
1470
+ * @param alias - Node alias (e.g., 'n', 'person')
1471
+ * @param label - Optional node label(s)
1472
+ */
1473
+ match(alias, label) {
1474
+ const labelStr = this.formatLabel(label);
1475
+ const nodePattern = `(${alias}${labelStr})`;
1476
+ return this.clone({
1477
+ matchClauses: [...this.state.matchClauses, nodePattern],
1478
+ currentNode: alias
1479
+ });
1480
+ }
1481
+ /**
1482
+ * Add a relationship pattern
1483
+ *
1484
+ * @param type - Relationship type (e.g., 'KNOWS', 'FOLLOWS')
1485
+ * @param alias - Optional relationship alias
1486
+ * @param options - Relationship options (direction, hops)
1487
+ */
1488
+ rel(type, alias, options) {
1489
+ return this.clone({
1490
+ pendingRel: { type, alias, options }
1491
+ });
1492
+ }
1493
+ /**
1494
+ * Complete a relationship pattern with target node
1495
+ *
1496
+ * @param alias - Target node alias
1497
+ * @param label - Optional target node label(s)
1498
+ */
1499
+ to(alias, label) {
1500
+ if (!this.state.pendingRel) {
1501
+ throw new Error("to() must be called after rel()");
1502
+ }
1503
+ const { type, alias: relAlias, options } = this.state.pendingRel;
1504
+ const direction = options?.direction ?? "outgoing";
1505
+ const labelStr = this.formatLabel(label);
1506
+ const relPattern = this.formatRelationship(type, relAlias, options);
1507
+ const targetNode = `(${alias}${labelStr})`;
1508
+ let fullPattern;
1509
+ switch (direction) {
1510
+ case "incoming":
1511
+ fullPattern = `<-${relPattern}-${targetNode}`;
1512
+ break;
1513
+ case "both":
1514
+ fullPattern = `-${relPattern}-${targetNode}`;
1515
+ break;
1516
+ default:
1517
+ fullPattern = `-${relPattern}->${targetNode}`;
1518
+ }
1519
+ const lastMatch = this.state.matchClauses[this.state.matchClauses.length - 1];
1520
+ const updatedMatch = lastMatch + fullPattern;
1521
+ const newMatchClauses = [...this.state.matchClauses.slice(0, -1), updatedMatch];
1522
+ return this.clone({
1523
+ matchClauses: newMatchClauses,
1524
+ currentNode: alias,
1525
+ pendingRel: void 0
1526
+ });
1527
+ }
1528
+ /**
1529
+ * Add a WHERE clause
1530
+ *
1531
+ * @param condition - WHERE condition
1532
+ * @param params - Optional parameters
1533
+ */
1534
+ where(condition, params) {
1535
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1536
+ return this.clone({
1537
+ whereClauses: [...this.state.whereClauses, condition],
1538
+ whereOperators: [...this.state.whereOperators],
1539
+ params: newParams
1540
+ });
1541
+ }
1542
+ /**
1543
+ * Add an AND WHERE clause
1544
+ *
1545
+ * @param condition - WHERE condition
1546
+ * @param params - Optional parameters
1547
+ */
1548
+ andWhere(condition, params) {
1549
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1550
+ return this.clone({
1551
+ whereClauses: [...this.state.whereClauses, condition],
1552
+ whereOperators: [...this.state.whereOperators, "AND"],
1553
+ params: newParams
1554
+ });
1555
+ }
1556
+ /**
1557
+ * Add an OR WHERE clause
1558
+ *
1559
+ * @param condition - WHERE condition
1560
+ * @param params - Optional parameters
1561
+ */
1562
+ orWhere(condition, params) {
1563
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1564
+ return this.clone({
1565
+ whereClauses: [...this.state.whereClauses, condition],
1566
+ whereOperators: [...this.state.whereOperators, "OR"],
1567
+ params: newParams
1568
+ });
1569
+ }
1570
+ /**
1571
+ * Add a vector NEAR clause for similarity search
1572
+ *
1573
+ * @param paramName - Parameter name (e.g., '$query', '$embedding')
1574
+ * @param vector - Vector data
1575
+ * @param options - NEAR options (topK)
1576
+ */
1577
+ nearVector(paramName, vector, options) {
1578
+ const cleanParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
1579
+ const topKSuffix = options?.topK ? ` TOP ${options.topK}` : "";
1580
+ const condition = `vector NEAR $${cleanParamName}${topKSuffix}`;
1581
+ const newParams = { ...this.state.params, [cleanParamName]: vector };
1582
+ if (this.state.whereClauses.length === 0) {
1583
+ return this.clone({
1584
+ whereClauses: [condition],
1585
+ params: newParams
1586
+ });
1587
+ }
1588
+ return this.clone({
1589
+ whereClauses: [...this.state.whereClauses, condition],
1590
+ whereOperators: [...this.state.whereOperators, "AND"],
1591
+ params: newParams
1592
+ });
1593
+ }
1594
+ /**
1595
+ * Add LIMIT clause
1596
+ *
1597
+ * @param value - Maximum number of results
1598
+ */
1599
+ limit(value) {
1600
+ if (value < 0) {
1601
+ throw new Error("LIMIT must be non-negative");
1602
+ }
1603
+ return this.clone({ limitValue: value });
1604
+ }
1605
+ /**
1606
+ * Add OFFSET clause
1607
+ *
1608
+ * @param value - Number of results to skip
1609
+ */
1610
+ offset(value) {
1611
+ if (value < 0) {
1612
+ throw new Error("OFFSET must be non-negative");
1613
+ }
1614
+ return this.clone({ offsetValue: value });
1615
+ }
1616
+ /**
1617
+ * Add ORDER BY clause
1618
+ *
1619
+ * @param field - Field to order by
1620
+ * @param direction - Sort direction (ASC or DESC)
1621
+ */
1622
+ orderBy(field, direction) {
1623
+ const orderClause = direction ? `${field} ${direction}` : field;
1624
+ return this.clone({ orderByClause: orderClause });
1625
+ }
1626
+ /**
1627
+ * Add RETURN clause with specific fields
1628
+ *
1629
+ * @param fields - Fields to return (array or object with aliases)
1630
+ */
1631
+ return(fields) {
1632
+ let returnClause;
1633
+ if (Array.isArray(fields)) {
1634
+ returnClause = fields.join(", ");
1635
+ } else {
1636
+ returnClause = Object.entries(fields).map(([field, alias]) => `${field} AS ${alias}`).join(", ");
1637
+ }
1638
+ return this.clone({ returnClause });
1639
+ }
1640
+ /**
1641
+ * Add RETURN * clause
1642
+ */
1643
+ returnAll() {
1644
+ return this.clone({ returnClause: "*" });
1645
+ }
1646
+ /**
1647
+ * Set fusion strategy for hybrid queries
1648
+ *
1649
+ * @param strategy - Fusion strategy
1650
+ * @param options - Fusion parameters
1651
+ */
1652
+ fusion(strategy, options) {
1653
+ return this.clone({
1654
+ fusionOptions: {
1655
+ strategy,
1656
+ ...options
1657
+ }
1658
+ });
1659
+ }
1660
+ /**
1661
+ * Get the fusion options
1662
+ */
1663
+ getFusionOptions() {
1664
+ return this.state.fusionOptions;
1665
+ }
1666
+ /**
1667
+ * Get all parameters
1668
+ */
1669
+ getParams() {
1670
+ return { ...this.state.params };
1671
+ }
1672
+ /**
1673
+ * Build the VelesQL query string
1674
+ */
1675
+ toVelesQL() {
1676
+ if (this.state.matchClauses.length === 0) {
1677
+ throw new Error("Query must have at least one MATCH clause");
1678
+ }
1679
+ const parts = [];
1680
+ parts.push(`MATCH ${this.state.matchClauses.join(", ")}`);
1681
+ if (this.state.whereClauses.length > 0) {
1682
+ const whereStr = this.buildWhereClause();
1683
+ parts.push(`WHERE ${whereStr}`);
1684
+ }
1685
+ if (this.state.orderByClause) {
1686
+ parts.push(`ORDER BY ${this.state.orderByClause}`);
1687
+ }
1688
+ if (this.state.limitValue !== void 0) {
1689
+ parts.push(`LIMIT ${this.state.limitValue}`);
1690
+ }
1691
+ if (this.state.offsetValue !== void 0) {
1692
+ parts.push(`OFFSET ${this.state.offsetValue}`);
1693
+ }
1694
+ if (this.state.returnClause) {
1695
+ parts.push(`RETURN ${this.state.returnClause}`);
1696
+ }
1697
+ if (this.state.fusionOptions) {
1698
+ parts.push(`/* FUSION ${this.state.fusionOptions.strategy} */`);
1699
+ }
1700
+ return parts.join(" ");
1701
+ }
1702
+ formatLabel(label) {
1703
+ if (!label) return "";
1704
+ if (Array.isArray(label)) {
1705
+ return label.map((l) => `:${l}`).join("");
1706
+ }
1707
+ return `:${label}`;
1708
+ }
1709
+ formatRelationship(type, alias, options) {
1710
+ const aliasStr = alias ? alias : "";
1711
+ const hopsStr = this.formatHops(options);
1712
+ if (alias) {
1713
+ return `[${aliasStr}:${type}${hopsStr}]`;
1714
+ }
1715
+ return `[:${type}${hopsStr}]`;
1716
+ }
1717
+ formatHops(options) {
1718
+ if (!options?.minHops && !options?.maxHops) return "";
1719
+ const min = options.minHops ?? 1;
1720
+ const max = options.maxHops ?? "";
1721
+ return `*${min}..${max}`;
1722
+ }
1723
+ buildWhereClause() {
1724
+ if (this.state.whereClauses.length === 0) return "";
1725
+ const first = this.state.whereClauses[0];
1726
+ if (!first) return "";
1727
+ let result = first;
1728
+ for (let i = 1; i < this.state.whereClauses.length; i++) {
1729
+ const operator = this.state.whereOperators[i - 1] ?? "AND";
1730
+ const clause = this.state.whereClauses[i];
1731
+ if (clause) {
1732
+ result += ` ${operator} ${clause}`;
1733
+ }
1734
+ }
1735
+ return result;
1736
+ }
968
1737
  };
1738
+ function velesql() {
1739
+ return new VelesQLBuilder();
1740
+ }
969
1741
  export {
970
1742
  ConnectionError,
971
1743
  NotFoundError,
@@ -973,5 +1745,7 @@ export {
973
1745
  ValidationError,
974
1746
  VelesDB,
975
1747
  VelesDBError,
976
- WasmBackend
1748
+ VelesQLBuilder,
1749
+ WasmBackend,
1750
+ velesql
977
1751
  };