@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.js CHANGED
@@ -36,7 +36,9 @@ __export(index_exports, {
36
36
  ValidationError: () => ValidationError,
37
37
  VelesDB: () => VelesDB,
38
38
  VelesDBError: () => VelesDBError,
39
- WasmBackend: () => WasmBackend
39
+ VelesQLBuilder: () => VelesQLBuilder,
40
+ WasmBackend: () => WasmBackend,
41
+ velesql: () => velesql
40
42
  });
41
43
  module.exports = __toCommonJS(index_exports);
42
44
 
@@ -175,7 +177,7 @@ var WasmBackend = class {
175
177
  throw new NotFoundError(`Collection '${collectionName}'`);
176
178
  }
177
179
  for (const doc of docs) {
178
- const vectorLen = Array.isArray(doc.vector) ? doc.vector.length : doc.vector.length;
180
+ const vectorLen = doc.vector.length;
179
181
  if (vectorLen !== collection.config.dimension) {
180
182
  throw new VelesDBError(
181
183
  `Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
@@ -281,7 +283,7 @@ var WasmBackend = class {
281
283
  "NOT_SUPPORTED"
282
284
  );
283
285
  }
284
- async query(_queryString, _params) {
286
+ async query(_collection, _queryString, _params, _options) {
285
287
  throw new VelesDBError(
286
288
  "VelesQL queries are not supported in WASM backend. Use REST backend for query support.",
287
289
  "NOT_SUPPORTED"
@@ -331,6 +333,60 @@ var WasmBackend = class {
331
333
  }
332
334
  return Math.abs(hash);
333
335
  }
336
+ // ========================================================================
337
+ // Index Management (EPIC-009) - Stubs for WASM backend
338
+ // Note: Full implementation requires velesdb-wasm support
339
+ // ========================================================================
340
+ async createIndex(_collection, _options) {
341
+ this.ensureInitialized();
342
+ throw new Error(
343
+ "WasmBackend: createIndex is not yet supported. Index operations require the REST backend with velesdb-server."
344
+ );
345
+ }
346
+ async listIndexes(_collection) {
347
+ this.ensureInitialized();
348
+ return [];
349
+ }
350
+ async hasIndex(_collection, _label, _property) {
351
+ this.ensureInitialized();
352
+ return false;
353
+ }
354
+ async dropIndex(_collection, _label, _property) {
355
+ this.ensureInitialized();
356
+ return false;
357
+ }
358
+ // ========================================================================
359
+ // Knowledge Graph (EPIC-016 US-041) - Stubs for WASM backend
360
+ // Note: Graph operations require server-side EdgeStore
361
+ // ========================================================================
362
+ async addEdge(_collection, _edge) {
363
+ this.ensureInitialized();
364
+ throw new VelesDBError(
365
+ "Knowledge Graph operations are not supported in WASM backend. Use REST backend for graph features.",
366
+ "NOT_SUPPORTED"
367
+ );
368
+ }
369
+ async getEdges(_collection, _options) {
370
+ this.ensureInitialized();
371
+ throw new VelesDBError(
372
+ "Knowledge Graph operations are not supported in WASM backend. Use REST backend for graph features.",
373
+ "NOT_SUPPORTED"
374
+ );
375
+ }
376
+ async traverseGraph(_collection, _request) {
377
+ this.ensureInitialized();
378
+ throw new VelesDBError(
379
+ "Graph traversal is not supported in WASM backend. Use REST backend for graph features.",
380
+ "NOT_SUPPORTED"
381
+ );
382
+ }
383
+ async getNodeDegree(_collection, _nodeId) {
384
+ this.ensureInitialized();
385
+ throw new VelesDBError(
386
+ "Graph degree query is not supported in WASM backend. Use REST backend for graph features.",
387
+ "NOT_SUPPORTED"
388
+ );
389
+ }
334
390
  };
335
391
 
336
392
  // src/backends/rest.ts
@@ -366,6 +422,64 @@ var RestBackend = class {
366
422
  throw new ConnectionError("REST backend not initialized");
367
423
  }
368
424
  }
425
+ mapStatusToErrorCode(status) {
426
+ switch (status) {
427
+ case 400:
428
+ return "BAD_REQUEST";
429
+ case 401:
430
+ return "UNAUTHORIZED";
431
+ case 403:
432
+ return "FORBIDDEN";
433
+ case 404:
434
+ return "NOT_FOUND";
435
+ case 409:
436
+ return "CONFLICT";
437
+ case 429:
438
+ return "RATE_LIMITED";
439
+ case 500:
440
+ return "INTERNAL_ERROR";
441
+ case 503:
442
+ return "SERVICE_UNAVAILABLE";
443
+ default:
444
+ return "UNKNOWN_ERROR";
445
+ }
446
+ }
447
+ extractErrorPayload(data) {
448
+ if (!data || typeof data !== "object") {
449
+ return {};
450
+ }
451
+ const payload = data;
452
+ const code = typeof payload.code === "string" ? payload.code : void 0;
453
+ const messageField = payload.message ?? payload.error;
454
+ const message = typeof messageField === "string" ? messageField : void 0;
455
+ return { code, message };
456
+ }
457
+ /**
458
+ * Parse node ID safely to handle u64 values above Number.MAX_SAFE_INTEGER.
459
+ * Returns bigint for large values, number for safe values.
460
+ */
461
+ parseNodeId(value) {
462
+ if (value === null || value === void 0) {
463
+ return 0;
464
+ }
465
+ if (typeof value === "bigint") {
466
+ return value;
467
+ }
468
+ if (typeof value === "string") {
469
+ const num = Number(value);
470
+ if (num > Number.MAX_SAFE_INTEGER) {
471
+ return BigInt(value);
472
+ }
473
+ return num;
474
+ }
475
+ if (typeof value === "number") {
476
+ if (value > Number.MAX_SAFE_INTEGER) {
477
+ return value;
478
+ }
479
+ return value;
480
+ }
481
+ return 0;
482
+ }
369
483
  async request(method, path, body) {
370
484
  const url = `${this.baseUrl}${path}`;
371
485
  const headers = {
@@ -384,12 +498,13 @@ var RestBackend = class {
384
498
  signal: controller.signal
385
499
  });
386
500
  clearTimeout(timeoutId);
387
- const data = await response.json();
501
+ const data = await response.json().catch(() => ({}));
388
502
  if (!response.ok) {
503
+ const errorPayload = this.extractErrorPayload(data);
389
504
  return {
390
505
  error: {
391
- code: data.code ?? "UNKNOWN_ERROR",
392
- message: data.message ?? `HTTP ${response.status}`
506
+ code: errorPayload.code ?? this.mapStatusToErrorCode(response.status),
507
+ message: errorPayload.message ?? `HTTP ${response.status}`
393
508
  }
394
509
  };
395
510
  }
@@ -456,11 +571,13 @@ var RestBackend = class {
456
571
  const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
457
572
  const response = await this.request(
458
573
  "POST",
459
- `/collections/${encodeURIComponent(collection)}/vectors`,
574
+ `/collections/${encodeURIComponent(collection)}/points`,
460
575
  {
461
- id: doc.id,
462
- vector,
463
- payload: doc.payload
576
+ points: [{
577
+ id: doc.id,
578
+ vector,
579
+ payload: doc.payload
580
+ }]
464
581
  }
465
582
  );
466
583
  if (response.error) {
@@ -479,8 +596,8 @@ var RestBackend = class {
479
596
  }));
480
597
  const response = await this.request(
481
598
  "POST",
482
- `/collections/${encodeURIComponent(collection)}/vectors/batch`,
483
- { vectors }
599
+ `/collections/${encodeURIComponent(collection)}/points`,
600
+ { points: vectors }
484
601
  );
485
602
  if (response.error) {
486
603
  if (response.error.code === "NOT_FOUND") {
@@ -534,7 +651,7 @@ var RestBackend = class {
534
651
  this.ensureInitialized();
535
652
  const response = await this.request(
536
653
  "DELETE",
537
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
654
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
538
655
  );
539
656
  if (response.error) {
540
657
  if (response.error.code === "NOT_FOUND") {
@@ -548,7 +665,7 @@ var RestBackend = class {
548
665
  this.ensureInitialized();
549
666
  const response = await this.request(
550
667
  "GET",
551
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
668
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
552
669
  );
553
670
  if (response.error) {
554
671
  if (response.error.code === "NOT_FOUND") {
@@ -599,7 +716,7 @@ var RestBackend = class {
599
716
  }
600
717
  return response.data?.results ?? [];
601
718
  }
602
- async query(queryString, params) {
719
+ async query(collection, queryString, params, _options) {
603
720
  this.ensureInitialized();
604
721
  const response = await this.request(
605
722
  "POST",
@@ -610,9 +727,32 @@ var RestBackend = class {
610
727
  }
611
728
  );
612
729
  if (response.error) {
730
+ if (response.error.code === "NOT_FOUND") {
731
+ throw new NotFoundError(`Collection '${collection}'`);
732
+ }
613
733
  throw new VelesDBError(response.error.message, response.error.code);
614
734
  }
615
- return response.data?.results ?? [];
735
+ const rawData = response.data;
736
+ return {
737
+ results: (rawData?.results ?? []).map((r) => ({
738
+ // Server returns `id` (u64), map to nodeId with precision handling
739
+ nodeId: this.parseNodeId(r.id ?? r.node_id ?? r.nodeId),
740
+ // Server returns `score`, map to vectorScore (primary score for SELECT queries)
741
+ vectorScore: r.score ?? r.vector_score ?? r.vectorScore,
742
+ // graph_score not returned by SELECT queries, only by future MATCH queries
743
+ graphScore: r.graph_score ?? r.graphScore,
744
+ // Use score as fusedScore for compatibility
745
+ fusedScore: r.score ?? r.fused_score ?? r.fusedScore ?? 0,
746
+ // payload maps to bindings for compatibility
747
+ bindings: r.payload ?? r.bindings ?? {},
748
+ columnData: r.column_data ?? r.columnData
749
+ })),
750
+ stats: {
751
+ executionTimeMs: rawData?.timing_ms ?? 0,
752
+ strategy: "select",
753
+ scannedNodes: rawData?.rows_returned ?? 0
754
+ }
755
+ };
616
756
  }
617
757
  async multiQuerySearch(collection, vectors, options) {
618
758
  this.ensureInitialized();
@@ -625,8 +765,8 @@ var RestBackend = class {
625
765
  {
626
766
  vectors: formattedVectors,
627
767
  top_k: options?.k ?? 10,
628
- fusion: options?.fusion ?? "rrf",
629
- fusion_params: options?.fusionParams ?? { k: 60 },
768
+ strategy: options?.fusion ?? "rrf",
769
+ rrf_k: options?.fusionParams?.k ?? 60,
630
770
  filter: options?.filter
631
771
  }
632
772
  );
@@ -668,6 +808,158 @@ var RestBackend = class {
668
808
  async close() {
669
809
  this._initialized = false;
670
810
  }
811
+ // ========================================================================
812
+ // Index Management (EPIC-009)
813
+ // ========================================================================
814
+ async createIndex(collection, options) {
815
+ this.ensureInitialized();
816
+ const response = await this.request(
817
+ "POST",
818
+ `/collections/${encodeURIComponent(collection)}/indexes`,
819
+ {
820
+ label: options.label,
821
+ property: options.property,
822
+ index_type: options.indexType ?? "hash"
823
+ }
824
+ );
825
+ if (response.error) {
826
+ if (response.error.code === "NOT_FOUND") {
827
+ throw new NotFoundError(`Collection '${collection}'`);
828
+ }
829
+ throw new VelesDBError(response.error.message, response.error.code);
830
+ }
831
+ }
832
+ async listIndexes(collection) {
833
+ this.ensureInitialized();
834
+ const response = await this.request(
835
+ "GET",
836
+ `/collections/${encodeURIComponent(collection)}/indexes`
837
+ );
838
+ if (response.error) {
839
+ if (response.error.code === "NOT_FOUND") {
840
+ throw new NotFoundError(`Collection '${collection}'`);
841
+ }
842
+ throw new VelesDBError(response.error.message, response.error.code);
843
+ }
844
+ return (response.data?.indexes ?? []).map((idx) => ({
845
+ label: idx.label,
846
+ property: idx.property,
847
+ indexType: idx.index_type,
848
+ cardinality: idx.cardinality,
849
+ memoryBytes: idx.memory_bytes
850
+ }));
851
+ }
852
+ async hasIndex(collection, label, property) {
853
+ const indexes = await this.listIndexes(collection);
854
+ return indexes.some((idx) => idx.label === label && idx.property === property);
855
+ }
856
+ async dropIndex(collection, label, property) {
857
+ this.ensureInitialized();
858
+ const response = await this.request(
859
+ "DELETE",
860
+ `/collections/${encodeURIComponent(collection)}/indexes/${encodeURIComponent(label)}/${encodeURIComponent(property)}`
861
+ );
862
+ if (response.error) {
863
+ if (response.error.code === "NOT_FOUND") {
864
+ return false;
865
+ }
866
+ throw new VelesDBError(response.error.message, response.error.code);
867
+ }
868
+ return response.data?.dropped ?? true;
869
+ }
870
+ // ========================================================================
871
+ // Knowledge Graph (EPIC-016 US-041)
872
+ // ========================================================================
873
+ async addEdge(collection, edge) {
874
+ this.ensureInitialized();
875
+ const response = await this.request(
876
+ "POST",
877
+ `/collections/${encodeURIComponent(collection)}/graph/edges`,
878
+ {
879
+ id: edge.id,
880
+ source: edge.source,
881
+ target: edge.target,
882
+ label: edge.label,
883
+ properties: edge.properties ?? {}
884
+ }
885
+ );
886
+ if (response.error) {
887
+ if (response.error.code === "NOT_FOUND") {
888
+ throw new NotFoundError(`Collection '${collection}'`);
889
+ }
890
+ throw new VelesDBError(response.error.message, response.error.code);
891
+ }
892
+ }
893
+ async getEdges(collection, options) {
894
+ this.ensureInitialized();
895
+ const queryParams = options?.label ? `?label=${encodeURIComponent(options.label)}` : "";
896
+ const response = await this.request(
897
+ "GET",
898
+ `/collections/${encodeURIComponent(collection)}/graph/edges${queryParams}`
899
+ );
900
+ if (response.error) {
901
+ if (response.error.code === "NOT_FOUND") {
902
+ throw new NotFoundError(`Collection '${collection}'`);
903
+ }
904
+ throw new VelesDBError(response.error.message, response.error.code);
905
+ }
906
+ return response.data?.edges ?? [];
907
+ }
908
+ // ========================================================================
909
+ // Graph Traversal (EPIC-016 US-050)
910
+ // ========================================================================
911
+ async traverseGraph(collection, request) {
912
+ this.ensureInitialized();
913
+ const response = await this.request(
914
+ "POST",
915
+ `/collections/${encodeURIComponent(collection)}/graph/traverse`,
916
+ {
917
+ source: request.source,
918
+ strategy: request.strategy ?? "bfs",
919
+ max_depth: request.maxDepth ?? 3,
920
+ limit: request.limit ?? 100,
921
+ cursor: request.cursor,
922
+ rel_types: request.relTypes ?? []
923
+ }
924
+ );
925
+ if (response.error) {
926
+ if (response.error.code === "NOT_FOUND") {
927
+ throw new NotFoundError(`Collection '${collection}'`);
928
+ }
929
+ throw new VelesDBError(response.error.message, response.error.code);
930
+ }
931
+ const data = response.data;
932
+ return {
933
+ results: data.results.map((r) => ({
934
+ targetId: r.target_id,
935
+ depth: r.depth,
936
+ path: r.path
937
+ })),
938
+ nextCursor: data.next_cursor ?? void 0,
939
+ hasMore: data.has_more,
940
+ stats: {
941
+ visited: data.stats.visited,
942
+ depthReached: data.stats.depth_reached
943
+ }
944
+ };
945
+ }
946
+ async getNodeDegree(collection, nodeId) {
947
+ this.ensureInitialized();
948
+ const response = await this.request(
949
+ "GET",
950
+ `/collections/${encodeURIComponent(collection)}/graph/nodes/${nodeId}/degree`
951
+ );
952
+ if (response.error) {
953
+ if (response.error.code === "NOT_FOUND") {
954
+ throw new NotFoundError(`Collection '${collection}'`);
955
+ }
956
+ throw new VelesDBError(response.error.message, response.error.code);
957
+ }
958
+ return {
959
+ inDegree: response.data?.in_degree ?? 0,
960
+ outDegree: response.data?.out_degree ?? 0
961
+ };
962
+ }
671
963
  };
672
964
 
673
965
  // src/client.ts
@@ -921,18 +1213,36 @@ var VelesDB = class {
921
1213
  return this.backend.hybridSearch(collection, vector, textQuery, options);
922
1214
  }
923
1215
  /**
924
- * Execute a VelesQL query
1216
+ * Execute a VelesQL multi-model query (EPIC-031 US-011)
925
1217
  *
1218
+ * Supports hybrid vector + graph queries with VelesQL syntax.
1219
+ *
1220
+ * @param collection - Collection name
926
1221
  * @param queryString - VelesQL query string
927
- * @param params - Optional query parameters
928
- * @returns Query results
1222
+ * @param params - Query parameters (vectors, scalars)
1223
+ * @param options - Query options (timeout, streaming)
1224
+ * @returns Query response with results and execution stats
1225
+ *
1226
+ * @example
1227
+ * ```typescript
1228
+ * const response = await db.query('docs', `
1229
+ * MATCH (d:Doc) WHERE vector NEAR $q LIMIT 20
1230
+ * `, { q: queryVector });
1231
+ *
1232
+ * for (const r of response.results) {
1233
+ * console.log(`Node ${r.nodeId}: ${r.fusedScore}`);
1234
+ * }
1235
+ * ```
929
1236
  */
930
- async query(queryString, params) {
1237
+ async query(collection, queryString, params, options) {
931
1238
  this.ensureInitialized();
1239
+ if (!collection || typeof collection !== "string") {
1240
+ throw new ValidationError("Collection name must be a non-empty string");
1241
+ }
932
1242
  if (!queryString || typeof queryString !== "string") {
933
1243
  throw new ValidationError("Query string must be a non-empty string");
934
1244
  }
935
- return this.backend.query(queryString, params);
1245
+ return this.backend.query(collection, queryString, params, options);
936
1246
  }
937
1247
  /**
938
1248
  * Multi-query fusion search combining results from multiple query vectors
@@ -1007,7 +1317,471 @@ var VelesDB = class {
1007
1317
  get backendType() {
1008
1318
  return this.config.backend;
1009
1319
  }
1320
+ // ========================================================================
1321
+ // Index Management (EPIC-009)
1322
+ // ========================================================================
1323
+ /**
1324
+ * Create a property index for O(1) equality lookups or O(log n) range queries
1325
+ *
1326
+ * @param collection - Collection name
1327
+ * @param options - Index configuration (label, property, indexType)
1328
+ *
1329
+ * @example
1330
+ * ```typescript
1331
+ * // Create hash index for fast email lookups
1332
+ * await db.createIndex('users', { label: 'Person', property: 'email' });
1333
+ *
1334
+ * // Create range index for timestamp queries
1335
+ * await db.createIndex('events', { label: 'Event', property: 'timestamp', indexType: 'range' });
1336
+ * ```
1337
+ */
1338
+ async createIndex(collection, options) {
1339
+ this.ensureInitialized();
1340
+ if (!options.label || !options.property) {
1341
+ throw new ValidationError("Index requires label and property");
1342
+ }
1343
+ await this.backend.createIndex(collection, options);
1344
+ }
1345
+ /**
1346
+ * List all indexes on a collection
1347
+ *
1348
+ * @param collection - Collection name
1349
+ * @returns Array of index information
1350
+ */
1351
+ async listIndexes(collection) {
1352
+ this.ensureInitialized();
1353
+ return this.backend.listIndexes(collection);
1354
+ }
1355
+ /**
1356
+ * Check if an index exists
1357
+ *
1358
+ * @param collection - Collection name
1359
+ * @param label - Node label
1360
+ * @param property - Property name
1361
+ * @returns true if index exists
1362
+ */
1363
+ async hasIndex(collection, label, property) {
1364
+ this.ensureInitialized();
1365
+ return this.backend.hasIndex(collection, label, property);
1366
+ }
1367
+ /**
1368
+ * Drop an index
1369
+ *
1370
+ * @param collection - Collection name
1371
+ * @param label - Node label
1372
+ * @param property - Property name
1373
+ * @returns true if index was dropped, false if it didn't exist
1374
+ */
1375
+ async dropIndex(collection, label, property) {
1376
+ this.ensureInitialized();
1377
+ return this.backend.dropIndex(collection, label, property);
1378
+ }
1379
+ // ========================================================================
1380
+ // Knowledge Graph (EPIC-016 US-041)
1381
+ // ========================================================================
1382
+ /**
1383
+ * Add an edge to the collection's knowledge graph
1384
+ *
1385
+ * @param collection - Collection name
1386
+ * @param edge - Edge to add (id, source, target, label, properties)
1387
+ *
1388
+ * @example
1389
+ * ```typescript
1390
+ * await db.addEdge('social', {
1391
+ * id: 1,
1392
+ * source: 100,
1393
+ * target: 200,
1394
+ * label: 'FOLLOWS',
1395
+ * properties: { since: '2024-01-01' }
1396
+ * });
1397
+ * ```
1398
+ */
1399
+ async addEdge(collection, edge) {
1400
+ this.ensureInitialized();
1401
+ if (!edge.label || typeof edge.label !== "string") {
1402
+ throw new ValidationError("Edge label is required and must be a string");
1403
+ }
1404
+ if (typeof edge.source !== "number" || typeof edge.target !== "number") {
1405
+ throw new ValidationError("Edge source and target must be numbers");
1406
+ }
1407
+ await this.backend.addEdge(collection, edge);
1408
+ }
1409
+ /**
1410
+ * Get edges from the collection's knowledge graph
1411
+ *
1412
+ * @param collection - Collection name
1413
+ * @param options - Query options (filter by label)
1414
+ * @returns Array of edges
1415
+ *
1416
+ * @example
1417
+ * ```typescript
1418
+ * // Get all edges with label "FOLLOWS"
1419
+ * const edges = await db.getEdges('social', { label: 'FOLLOWS' });
1420
+ * ```
1421
+ */
1422
+ async getEdges(collection, options) {
1423
+ this.ensureInitialized();
1424
+ return this.backend.getEdges(collection, options);
1425
+ }
1426
+ // ========================================================================
1427
+ // Graph Traversal (EPIC-016 US-050)
1428
+ // ========================================================================
1429
+ /**
1430
+ * Traverse the graph using BFS or DFS from a source node
1431
+ *
1432
+ * @param collection - Collection name
1433
+ * @param request - Traversal request options
1434
+ * @returns Traversal response with results and stats
1435
+ *
1436
+ * @example
1437
+ * ```typescript
1438
+ * // BFS traversal from node 100
1439
+ * const result = await db.traverseGraph('social', {
1440
+ * source: 100,
1441
+ * strategy: 'bfs',
1442
+ * maxDepth: 3,
1443
+ * limit: 100,
1444
+ * relTypes: ['FOLLOWS', 'KNOWS']
1445
+ * });
1446
+ *
1447
+ * for (const node of result.results) {
1448
+ * console.log(`Reached node ${node.targetId} at depth ${node.depth}`);
1449
+ * }
1450
+ * ```
1451
+ */
1452
+ async traverseGraph(collection, request) {
1453
+ this.ensureInitialized();
1454
+ if (typeof request.source !== "number") {
1455
+ throw new ValidationError("Source node ID must be a number");
1456
+ }
1457
+ if (request.strategy && !["bfs", "dfs"].includes(request.strategy)) {
1458
+ throw new ValidationError("Strategy must be 'bfs' or 'dfs'");
1459
+ }
1460
+ return this.backend.traverseGraph(collection, request);
1461
+ }
1462
+ /**
1463
+ * Get the in-degree and out-degree of a node
1464
+ *
1465
+ * @param collection - Collection name
1466
+ * @param nodeId - Node ID
1467
+ * @returns Degree response with inDegree and outDegree
1468
+ *
1469
+ * @example
1470
+ * ```typescript
1471
+ * const degree = await db.getNodeDegree('social', 100);
1472
+ * console.log(`In: ${degree.inDegree}, Out: ${degree.outDegree}`);
1473
+ * ```
1474
+ */
1475
+ async getNodeDegree(collection, nodeId) {
1476
+ this.ensureInitialized();
1477
+ if (typeof nodeId !== "number") {
1478
+ throw new ValidationError("Node ID must be a number");
1479
+ }
1480
+ return this.backend.getNodeDegree(collection, nodeId);
1481
+ }
1482
+ };
1483
+
1484
+ // src/query-builder.ts
1485
+ var VelesQLBuilder = class _VelesQLBuilder {
1486
+ constructor(state) {
1487
+ this.state = {
1488
+ matchClauses: state?.matchClauses ?? [],
1489
+ whereClauses: state?.whereClauses ?? [],
1490
+ whereOperators: state?.whereOperators ?? [],
1491
+ params: state?.params ?? {},
1492
+ limitValue: state?.limitValue,
1493
+ offsetValue: state?.offsetValue,
1494
+ orderByClause: state?.orderByClause,
1495
+ returnClause: state?.returnClause,
1496
+ fusionOptions: state?.fusionOptions,
1497
+ currentNode: state?.currentNode,
1498
+ pendingRel: state?.pendingRel
1499
+ };
1500
+ }
1501
+ clone(updates) {
1502
+ return new _VelesQLBuilder({
1503
+ ...this.state,
1504
+ matchClauses: [...this.state.matchClauses],
1505
+ whereClauses: [...this.state.whereClauses],
1506
+ whereOperators: [...this.state.whereOperators],
1507
+ params: { ...this.state.params },
1508
+ ...updates
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Start a MATCH clause with a node pattern
1513
+ *
1514
+ * @param alias - Node alias (e.g., 'n', 'person')
1515
+ * @param label - Optional node label(s)
1516
+ */
1517
+ match(alias, label) {
1518
+ const labelStr = this.formatLabel(label);
1519
+ const nodePattern = `(${alias}${labelStr})`;
1520
+ return this.clone({
1521
+ matchClauses: [...this.state.matchClauses, nodePattern],
1522
+ currentNode: alias
1523
+ });
1524
+ }
1525
+ /**
1526
+ * Add a relationship pattern
1527
+ *
1528
+ * @param type - Relationship type (e.g., 'KNOWS', 'FOLLOWS')
1529
+ * @param alias - Optional relationship alias
1530
+ * @param options - Relationship options (direction, hops)
1531
+ */
1532
+ rel(type, alias, options) {
1533
+ return this.clone({
1534
+ pendingRel: { type, alias, options }
1535
+ });
1536
+ }
1537
+ /**
1538
+ * Complete a relationship pattern with target node
1539
+ *
1540
+ * @param alias - Target node alias
1541
+ * @param label - Optional target node label(s)
1542
+ */
1543
+ to(alias, label) {
1544
+ if (!this.state.pendingRel) {
1545
+ throw new Error("to() must be called after rel()");
1546
+ }
1547
+ const { type, alias: relAlias, options } = this.state.pendingRel;
1548
+ const direction = options?.direction ?? "outgoing";
1549
+ const labelStr = this.formatLabel(label);
1550
+ const relPattern = this.formatRelationship(type, relAlias, options);
1551
+ const targetNode = `(${alias}${labelStr})`;
1552
+ let fullPattern;
1553
+ switch (direction) {
1554
+ case "incoming":
1555
+ fullPattern = `<-${relPattern}-${targetNode}`;
1556
+ break;
1557
+ case "both":
1558
+ fullPattern = `-${relPattern}-${targetNode}`;
1559
+ break;
1560
+ default:
1561
+ fullPattern = `-${relPattern}->${targetNode}`;
1562
+ }
1563
+ const lastMatch = this.state.matchClauses[this.state.matchClauses.length - 1];
1564
+ const updatedMatch = lastMatch + fullPattern;
1565
+ const newMatchClauses = [...this.state.matchClauses.slice(0, -1), updatedMatch];
1566
+ return this.clone({
1567
+ matchClauses: newMatchClauses,
1568
+ currentNode: alias,
1569
+ pendingRel: void 0
1570
+ });
1571
+ }
1572
+ /**
1573
+ * Add a WHERE clause
1574
+ *
1575
+ * @param condition - WHERE condition
1576
+ * @param params - Optional parameters
1577
+ */
1578
+ where(condition, params) {
1579
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1580
+ return this.clone({
1581
+ whereClauses: [...this.state.whereClauses, condition],
1582
+ whereOperators: [...this.state.whereOperators],
1583
+ params: newParams
1584
+ });
1585
+ }
1586
+ /**
1587
+ * Add an AND WHERE clause
1588
+ *
1589
+ * @param condition - WHERE condition
1590
+ * @param params - Optional parameters
1591
+ */
1592
+ andWhere(condition, params) {
1593
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1594
+ return this.clone({
1595
+ whereClauses: [...this.state.whereClauses, condition],
1596
+ whereOperators: [...this.state.whereOperators, "AND"],
1597
+ params: newParams
1598
+ });
1599
+ }
1600
+ /**
1601
+ * Add an OR WHERE clause
1602
+ *
1603
+ * @param condition - WHERE condition
1604
+ * @param params - Optional parameters
1605
+ */
1606
+ orWhere(condition, params) {
1607
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1608
+ return this.clone({
1609
+ whereClauses: [...this.state.whereClauses, condition],
1610
+ whereOperators: [...this.state.whereOperators, "OR"],
1611
+ params: newParams
1612
+ });
1613
+ }
1614
+ /**
1615
+ * Add a vector NEAR clause for similarity search
1616
+ *
1617
+ * @param paramName - Parameter name (e.g., '$query', '$embedding')
1618
+ * @param vector - Vector data
1619
+ * @param options - NEAR options (topK)
1620
+ */
1621
+ nearVector(paramName, vector, options) {
1622
+ const cleanParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
1623
+ const topKSuffix = options?.topK ? ` TOP ${options.topK}` : "";
1624
+ const condition = `vector NEAR $${cleanParamName}${topKSuffix}`;
1625
+ const newParams = { ...this.state.params, [cleanParamName]: vector };
1626
+ if (this.state.whereClauses.length === 0) {
1627
+ return this.clone({
1628
+ whereClauses: [condition],
1629
+ params: newParams
1630
+ });
1631
+ }
1632
+ return this.clone({
1633
+ whereClauses: [...this.state.whereClauses, condition],
1634
+ whereOperators: [...this.state.whereOperators, "AND"],
1635
+ params: newParams
1636
+ });
1637
+ }
1638
+ /**
1639
+ * Add LIMIT clause
1640
+ *
1641
+ * @param value - Maximum number of results
1642
+ */
1643
+ limit(value) {
1644
+ if (value < 0) {
1645
+ throw new Error("LIMIT must be non-negative");
1646
+ }
1647
+ return this.clone({ limitValue: value });
1648
+ }
1649
+ /**
1650
+ * Add OFFSET clause
1651
+ *
1652
+ * @param value - Number of results to skip
1653
+ */
1654
+ offset(value) {
1655
+ if (value < 0) {
1656
+ throw new Error("OFFSET must be non-negative");
1657
+ }
1658
+ return this.clone({ offsetValue: value });
1659
+ }
1660
+ /**
1661
+ * Add ORDER BY clause
1662
+ *
1663
+ * @param field - Field to order by
1664
+ * @param direction - Sort direction (ASC or DESC)
1665
+ */
1666
+ orderBy(field, direction) {
1667
+ const orderClause = direction ? `${field} ${direction}` : field;
1668
+ return this.clone({ orderByClause: orderClause });
1669
+ }
1670
+ /**
1671
+ * Add RETURN clause with specific fields
1672
+ *
1673
+ * @param fields - Fields to return (array or object with aliases)
1674
+ */
1675
+ return(fields) {
1676
+ let returnClause;
1677
+ if (Array.isArray(fields)) {
1678
+ returnClause = fields.join(", ");
1679
+ } else {
1680
+ returnClause = Object.entries(fields).map(([field, alias]) => `${field} AS ${alias}`).join(", ");
1681
+ }
1682
+ return this.clone({ returnClause });
1683
+ }
1684
+ /**
1685
+ * Add RETURN * clause
1686
+ */
1687
+ returnAll() {
1688
+ return this.clone({ returnClause: "*" });
1689
+ }
1690
+ /**
1691
+ * Set fusion strategy for hybrid queries
1692
+ *
1693
+ * @param strategy - Fusion strategy
1694
+ * @param options - Fusion parameters
1695
+ */
1696
+ fusion(strategy, options) {
1697
+ return this.clone({
1698
+ fusionOptions: {
1699
+ strategy,
1700
+ ...options
1701
+ }
1702
+ });
1703
+ }
1704
+ /**
1705
+ * Get the fusion options
1706
+ */
1707
+ getFusionOptions() {
1708
+ return this.state.fusionOptions;
1709
+ }
1710
+ /**
1711
+ * Get all parameters
1712
+ */
1713
+ getParams() {
1714
+ return { ...this.state.params };
1715
+ }
1716
+ /**
1717
+ * Build the VelesQL query string
1718
+ */
1719
+ toVelesQL() {
1720
+ if (this.state.matchClauses.length === 0) {
1721
+ throw new Error("Query must have at least one MATCH clause");
1722
+ }
1723
+ const parts = [];
1724
+ parts.push(`MATCH ${this.state.matchClauses.join(", ")}`);
1725
+ if (this.state.whereClauses.length > 0) {
1726
+ const whereStr = this.buildWhereClause();
1727
+ parts.push(`WHERE ${whereStr}`);
1728
+ }
1729
+ if (this.state.orderByClause) {
1730
+ parts.push(`ORDER BY ${this.state.orderByClause}`);
1731
+ }
1732
+ if (this.state.limitValue !== void 0) {
1733
+ parts.push(`LIMIT ${this.state.limitValue}`);
1734
+ }
1735
+ if (this.state.offsetValue !== void 0) {
1736
+ parts.push(`OFFSET ${this.state.offsetValue}`);
1737
+ }
1738
+ if (this.state.returnClause) {
1739
+ parts.push(`RETURN ${this.state.returnClause}`);
1740
+ }
1741
+ if (this.state.fusionOptions) {
1742
+ parts.push(`/* FUSION ${this.state.fusionOptions.strategy} */`);
1743
+ }
1744
+ return parts.join(" ");
1745
+ }
1746
+ formatLabel(label) {
1747
+ if (!label) return "";
1748
+ if (Array.isArray(label)) {
1749
+ return label.map((l) => `:${l}`).join("");
1750
+ }
1751
+ return `:${label}`;
1752
+ }
1753
+ formatRelationship(type, alias, options) {
1754
+ const aliasStr = alias ? alias : "";
1755
+ const hopsStr = this.formatHops(options);
1756
+ if (alias) {
1757
+ return `[${aliasStr}:${type}${hopsStr}]`;
1758
+ }
1759
+ return `[:${type}${hopsStr}]`;
1760
+ }
1761
+ formatHops(options) {
1762
+ if (!options?.minHops && !options?.maxHops) return "";
1763
+ const min = options.minHops ?? 1;
1764
+ const max = options.maxHops ?? "";
1765
+ return `*${min}..${max}`;
1766
+ }
1767
+ buildWhereClause() {
1768
+ if (this.state.whereClauses.length === 0) return "";
1769
+ const first = this.state.whereClauses[0];
1770
+ if (!first) return "";
1771
+ let result = first;
1772
+ for (let i = 1; i < this.state.whereClauses.length; i++) {
1773
+ const operator = this.state.whereOperators[i - 1] ?? "AND";
1774
+ const clause = this.state.whereClauses[i];
1775
+ if (clause) {
1776
+ result += ` ${operator} ${clause}`;
1777
+ }
1778
+ }
1779
+ return result;
1780
+ }
1010
1781
  };
1782
+ function velesql() {
1783
+ return new VelesQLBuilder();
1784
+ }
1011
1785
  // Annotate the CommonJS export names for ESM import in node:
1012
1786
  0 && (module.exports = {
1013
1787
  ConnectionError,
@@ -1016,5 +1790,7 @@ var VelesDB = class {
1016
1790
  ValidationError,
1017
1791
  VelesDB,
1018
1792
  VelesDBError,
1019
- WasmBackend
1793
+ VelesQLBuilder,
1794
+ WasmBackend,
1795
+ velesql
1020
1796
  });