@ekodb/ekodb-client 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * ekoDB TypeScript Client
3
3
  */
4
4
  import { QueryBuilder } from "./query-builder";
5
- import { SearchQuery, SearchQueryBuilder, SearchResponse } from "./search";
5
+ import { SearchQuery, SearchResponse } from "./search";
6
6
  import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
7
7
  import { Script, FunctionResult } from "./functions";
8
8
  export interface Record {
@@ -64,6 +64,52 @@ export interface BatchOperationResult {
64
64
  error: string;
65
65
  }>;
66
66
  }
67
+ export interface InsertOptions {
68
+ ttl?: string;
69
+ bypassRipple?: boolean;
70
+ transactionId?: string;
71
+ bypassCache?: boolean;
72
+ }
73
+ export interface UpdateOptions {
74
+ bypassRipple?: boolean;
75
+ transactionId?: string;
76
+ bypassCache?: boolean;
77
+ selectFields?: string[];
78
+ excludeFields?: string[];
79
+ }
80
+ export interface DeleteOptions {
81
+ bypassRipple?: boolean;
82
+ transactionId?: string;
83
+ }
84
+ export interface UpsertOptions {
85
+ ttl?: string;
86
+ bypassRipple?: boolean;
87
+ transactionId?: string;
88
+ bypassCache?: boolean;
89
+ }
90
+ export interface FindOptions {
91
+ filter?: any;
92
+ sort?: any;
93
+ limit?: number;
94
+ skip?: number;
95
+ join?: any;
96
+ bypassRipple?: boolean;
97
+ bypassCache?: boolean;
98
+ selectFields?: string[];
99
+ excludeFields?: string[];
100
+ }
101
+ export interface BatchInsertOptions {
102
+ bypassRipple?: boolean;
103
+ transactionId?: string;
104
+ }
105
+ export interface BatchUpdateOptions {
106
+ bypassRipple?: boolean;
107
+ transactionId?: string;
108
+ }
109
+ export interface BatchDeleteOptions {
110
+ bypassRipple?: boolean;
111
+ transactionId?: string;
112
+ }
67
113
  export interface CollectionConfig {
68
114
  collection_name: string;
69
115
  fields?: string[];
@@ -208,9 +254,9 @@ export declare class EkoDBClient {
208
254
  * Insert a document into a collection
209
255
  * @param collection - Collection name
210
256
  * @param record - Document to insert
211
- * @param ttl - Optional TTL: duration string ("1h", "30m"), seconds ("3600"), or ISO8601 timestamp
257
+ * @param options - Optional parameters (ttl, bypassRipple, transactionId, bypassCache)
212
258
  */
213
- insert(collection: string, record: Record, ttl?: string): Promise<Record>;
259
+ insert(collection: string, record: Record, options?: InsertOptions): Promise<Record>;
214
260
  /**
215
261
  * Find documents in a collection
216
262
  *
@@ -237,19 +283,29 @@ export declare class EkoDBClient {
237
283
  /**
238
284
  * Find a document by ID
239
285
  */
240
- findByID(collection: string, id: string): Promise<Record>;
286
+ findById(collection: string, id: string): Promise<Record>;
241
287
  /**
242
288
  * Update a document
289
+ * @param collection - Collection name
290
+ * @param id - Document ID
291
+ * @param record - Update data
292
+ * @param options - Optional parameters (bypassRipple, transactionId, bypassCache, selectFields, excludeFields)
243
293
  */
244
- update(collection: string, id: string, record: Record): Promise<Record>;
294
+ update(collection: string, id: string, record: Record, options?: UpdateOptions): Promise<Record>;
245
295
  /**
246
296
  * Delete a document
297
+ * @param collection - Collection name
298
+ * @param id - Document ID
299
+ * @param options - Optional parameters (bypassRipple, transactionId)
247
300
  */
248
- delete(collection: string, id: string): Promise<void>;
301
+ delete(collection: string, id: string, options?: DeleteOptions): Promise<void>;
249
302
  /**
250
303
  * Batch insert multiple documents
304
+ * @param collection - Collection name
305
+ * @param records - Array of documents to insert
306
+ * @param options - Optional parameters (bypassRipple, transactionId)
251
307
  */
252
- batchInsert(collection: string, records: Record[], bypassRipple?: boolean): Promise<BatchOperationResult>;
308
+ batchInsert(collection: string, records: Record[], options?: BatchInsertOptions): Promise<BatchOperationResult>;
253
309
  /**
254
310
  * Batch update multiple documents
255
311
  */
@@ -324,6 +380,90 @@ export declare class EkoDBClient {
324
380
  * @param transactionId - The transaction ID to rollback
325
381
  */
326
382
  rollbackTransaction(transactionId: string): Promise<void>;
383
+ /**
384
+ * Insert or update a record (upsert operation)
385
+ *
386
+ * Attempts to update the record first. If the record doesn't exist (404 error),
387
+ * it will be inserted instead. This provides atomic insert-or-update semantics.
388
+ *
389
+ * @param collection - Collection name
390
+ * @param id - Record ID
391
+ * @param record - Record data to insert or update
392
+ * @param bypassRipple - Optional flag to bypass ripple effects
393
+ * @returns The inserted or updated record
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const record = { name: "John Doe", email: "john@example.com" };
398
+ * // Will update if exists, insert if not
399
+ * const result = await client.upsert("users", "user123", record);
400
+ * ```
401
+ */
402
+ /**
403
+ * Upsert a document (insert or update)
404
+ * @param collection - Collection name
405
+ * @param id - Document ID
406
+ * @param record - Document data
407
+ * @param options - Optional parameters (ttl, bypassRipple, transactionId, bypassCache)
408
+ */
409
+ upsert(collection: string, id: string, record: Record, options?: UpsertOptions): Promise<Record>;
410
+ /**
411
+ * Find a single record by field value
412
+ *
413
+ * Convenience method for finding one record matching a specific field value.
414
+ * Returns null if no record matches, or the first matching record.
415
+ *
416
+ * @param collection - Collection name
417
+ * @param field - Field name to search
418
+ * @param value - Value to match
419
+ * @returns The matching record or null if not found
420
+ *
421
+ * @example
422
+ * ```typescript
423
+ * // Find user by email
424
+ * const user = await client.findOne("users", "email", "john@example.com");
425
+ * if (user) {
426
+ * console.log("Found user:", user);
427
+ * }
428
+ * ```
429
+ */
430
+ findOne(collection: string, field: string, value: any): Promise<Record | null>;
431
+ /**
432
+ * Check if a record exists by ID
433
+ *
434
+ * This is more efficient than fetching the record when you only need to check existence.
435
+ *
436
+ * @param collection - Collection name
437
+ * @param id - Record ID to check
438
+ * @returns true if the record exists, false if it doesn't
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * if (await client.exists("users", "user123")) {
443
+ * console.log("User exists");
444
+ * } else {
445
+ * console.log("User not found");
446
+ * }
447
+ * ```
448
+ */
449
+ exists(collection: string, id: string): Promise<boolean>;
450
+ /**
451
+ * Paginate through records
452
+ *
453
+ * Convenience method for pagination with page numbers (1-indexed).
454
+ *
455
+ * @param collection - Collection name
456
+ * @param page - Page number (1-indexed, i.e., first page is 1)
457
+ * @param pageSize - Number of records per page
458
+ * @returns Array of records for the requested page
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * // Get page 2 with 10 records per page
463
+ * const records = await client.paginate("users", 2, 10);
464
+ * ```
465
+ */
466
+ paginate(collection: string, page: number, pageSize: number): Promise<Record[]>;
327
467
  /**
328
468
  * List all collections
329
469
  */
@@ -332,6 +472,25 @@ export declare class EkoDBClient {
332
472
  * Delete a collection
333
473
  */
334
474
  deleteCollection(collection: string): Promise<void>;
475
+ /**
476
+ * Restore a deleted record from trash
477
+ * Records remain in trash for 30 days before permanent deletion
478
+ *
479
+ * @param collection - Collection name
480
+ * @param id - Record ID to restore
481
+ * @returns true if restored successfully
482
+ */
483
+ restoreRecord(collection: string, id: string): Promise<boolean>;
484
+ /**
485
+ * Restore all deleted records in a collection from trash
486
+ * Records remain in trash for 30 days before permanent deletion
487
+ *
488
+ * @param collection - Collection name
489
+ * @returns Number of records restored
490
+ */
491
+ restoreCollection(collection: string): Promise<{
492
+ recordsRestored: number;
493
+ }>;
335
494
  /**
336
495
  * Create a collection with schema
337
496
  *
@@ -396,7 +555,11 @@ export declare class EkoDBClient {
396
555
  * );
397
556
  * ```
398
557
  */
399
- search(collection: string, searchQuery: SearchQuery | SearchQueryBuilder): Promise<SearchResponse>;
558
+ search(collection: string, query: SearchQuery): Promise<SearchResponse>;
559
+ /**
560
+ * Health check - verify the ekoDB server is responding
561
+ */
562
+ health(): Promise<boolean>;
400
563
  /**
401
564
  * Create a new chat session
402
565
  */
@@ -508,20 +671,24 @@ export declare class EkoDBClient {
508
671
  * Simplified text search with full-text matching, fuzzy search, and stemming.
509
672
  *
510
673
  * @param collection - Collection name to search
511
- * @param queryText - Search query text
512
- * @param limit - Maximum number of results to return
513
- * @returns Array of matching records
674
+ * @param query - Search query text
675
+ * @param options - Additional search options
676
+ * @returns Search response with results and metadata
514
677
  *
515
678
  * @example
516
679
  * ```typescript
517
680
  * const results = await client.textSearch(
518
681
  * "documents",
519
682
  * "ownership system",
520
- * 10
683
+ * {
684
+ * limit: 10,
685
+ * select_fields: ["title", "content"],
686
+ * exclude_fields: ["author"]
687
+ * }
521
688
  * );
522
689
  * ```
523
690
  */
524
- textSearch(collection: string, queryText: string, limit: number): Promise<Record[]>;
691
+ textSearch(collection: string, query: string, options?: Partial<SearchQuery>): Promise<SearchResponse>;
525
692
  /**
526
693
  * Perform hybrid search combining text and vector search
527
694
  *
package/dist/client.js CHANGED
@@ -39,7 +39,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.WebSocketClient = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
40
40
  const msgpack_1 = require("@msgpack/msgpack");
41
41
  const query_builder_1 = require("./query-builder");
42
- const search_1 = require("./search");
43
42
  const schema_1 = require("./schema");
44
43
  /**
45
44
  * Serialization format for client-server communication
@@ -121,7 +120,17 @@ class EkoDBClient {
121
120
  body: JSON.stringify({ api_key: this.apiKey }),
122
121
  });
123
122
  if (!response.ok) {
124
- throw new Error(`Auth failed with status: ${response.status}`);
123
+ let errorMsg = `Auth failed with status: ${response.status}`;
124
+ try {
125
+ const errorBody = (await response.json());
126
+ if (errorBody.error) {
127
+ errorMsg = errorBody.error;
128
+ }
129
+ }
130
+ catch {
131
+ // Ignore JSON parse errors, use default message
132
+ }
133
+ throw new Error(errorMsg);
125
134
  }
126
135
  const result = (await response.json());
127
136
  this.token = result.token;
@@ -265,14 +274,24 @@ class EkoDBClient {
265
274
  * Insert a document into a collection
266
275
  * @param collection - Collection name
267
276
  * @param record - Document to insert
268
- * @param ttl - Optional TTL: duration string ("1h", "30m"), seconds ("3600"), or ISO8601 timestamp
277
+ * @param options - Optional parameters (ttl, bypassRipple, transactionId, bypassCache)
269
278
  */
270
- async insert(collection, record, ttl) {
279
+ async insert(collection, record, options) {
271
280
  const data = { ...record };
272
- if (ttl) {
273
- data.ttl = ttl;
281
+ if (options?.ttl) {
282
+ data.ttl = options.ttl;
274
283
  }
275
- return this.makeRequest("POST", `/api/insert/${collection}`, data);
284
+ const params = new URLSearchParams();
285
+ if (options?.bypassRipple !== undefined) {
286
+ params.append("bypass_ripple", String(options.bypassRipple));
287
+ }
288
+ if (options?.transactionId) {
289
+ params.append("transaction_id", options.transactionId);
290
+ }
291
+ const url = params.toString()
292
+ ? `/api/insert/${collection}?${params.toString()}`
293
+ : `/api/insert/${collection}`;
294
+ return this.makeRequest("POST", url, data);
276
295
  }
277
296
  /**
278
297
  * Find documents in a collection
@@ -303,30 +322,67 @@ class EkoDBClient {
303
322
  /**
304
323
  * Find a document by ID
305
324
  */
306
- async findByID(collection, id) {
325
+ async findById(collection, id) {
307
326
  return this.makeRequest("GET", `/api/find/${collection}/${id}`);
308
327
  }
309
328
  /**
310
329
  * Update a document
330
+ * @param collection - Collection name
331
+ * @param id - Document ID
332
+ * @param record - Update data
333
+ * @param options - Optional parameters (bypassRipple, transactionId, bypassCache, selectFields, excludeFields)
311
334
  */
312
- async update(collection, id, record) {
313
- return this.makeRequest("PUT", `/api/update/${collection}/${id}`, record);
335
+ async update(collection, id, record, options) {
336
+ const params = new URLSearchParams();
337
+ if (options?.bypassRipple !== undefined) {
338
+ params.append("bypass_ripple", String(options.bypassRipple));
339
+ }
340
+ if (options?.transactionId) {
341
+ params.append("transaction_id", options.transactionId);
342
+ }
343
+ const url = params.toString()
344
+ ? `/api/update/${collection}/${id}?${params.toString()}`
345
+ : `/api/update/${collection}/${id}`;
346
+ return this.makeRequest("PUT", url, record);
314
347
  }
315
348
  /**
316
349
  * Delete a document
350
+ * @param collection - Collection name
351
+ * @param id - Document ID
352
+ * @param options - Optional parameters (bypassRipple, transactionId)
317
353
  */
318
- async delete(collection, id) {
319
- await this.makeRequest("DELETE", `/api/delete/${collection}/${id}`);
354
+ async delete(collection, id, options) {
355
+ const params = new URLSearchParams();
356
+ if (options?.bypassRipple !== undefined) {
357
+ params.append("bypass_ripple", String(options.bypassRipple));
358
+ }
359
+ if (options?.transactionId) {
360
+ params.append("transaction_id", options.transactionId);
361
+ }
362
+ const url = params.toString()
363
+ ? `/api/delete/${collection}/${id}?${params.toString()}`
364
+ : `/api/delete/${collection}/${id}`;
365
+ await this.makeRequest("DELETE", url);
320
366
  }
321
367
  /**
322
368
  * Batch insert multiple documents
369
+ * @param collection - Collection name
370
+ * @param records - Array of documents to insert
371
+ * @param options - Optional parameters (bypassRipple, transactionId)
323
372
  */
324
- async batchInsert(collection, records, bypassRipple) {
325
- const inserts = records.map((data) => ({
326
- data,
327
- bypass_ripple: bypassRipple,
328
- }));
329
- return this.makeRequest("POST", `/api/batch/insert/${collection}`, { inserts });
373
+ async batchInsert(collection, records, options) {
374
+ const params = new URLSearchParams();
375
+ if (options?.bypassRipple !== undefined) {
376
+ params.append("bypass_ripple", String(options.bypassRipple));
377
+ }
378
+ if (options?.transactionId) {
379
+ params.append("transaction_id", options.transactionId);
380
+ }
381
+ const inserts = records.map((data) => ({ data }));
382
+ const url = params.toString()
383
+ ? `/api/batch/insert/${collection}?${params.toString()}`
384
+ : `/api/batch/insert/${collection}`;
385
+ return this.makeRequest("POST", url, { inserts });
330
386
  }
331
387
  /**
332
388
  * Batch update multiple documents
@@ -438,6 +494,139 @@ class EkoDBClient {
438
494
  async rollbackTransaction(transactionId) {
439
495
  await this.makeRequest("POST", `/api/transactions/${encodeURIComponent(transactionId)}/rollback`, undefined, 0, true);
440
496
  }
497
+ // ============================================================================
498
+ // Convenience Methods
499
+ // ============================================================================
500
+ /**
501
+ * Insert or update a record (upsert operation)
502
+ *
503
+ * Attempts to update the record first. If the record doesn't exist (404 error),
504
+ * it will be inserted instead. This provides atomic insert-or-update semantics.
505
+ *
506
+ * @param collection - Collection name
507
+ * @param id - Record ID
508
+ * @param record - Record data to insert or update
509
+ * @param bypassRipple - Optional flag to bypass ripple effects
510
+ * @returns The inserted or updated record
511
+ *
512
+ * @example
513
+ * ```typescript
514
+ * const record = { name: "John Doe", email: "john@example.com" };
515
+ * // Will update if exists, insert if not
516
+ * const result = await client.upsert("users", "user123", record);
517
+ * ```
518
+ */
519
+ /**
520
+ * Upsert a document (insert or update)
521
+ * @param collection - Collection name
522
+ * @param id - Document ID
523
+ * @param record - Document data
524
+ * @param options - Optional parameters (ttl, bypassRipple, transactionId, bypassCache)
525
+ */
526
+ async upsert(collection, id, record, options) {
527
+ try {
528
+ // Try update first
529
+ return await this.update(collection, id, record, {
530
+ bypassRipple: options?.bypassRipple,
531
+ transactionId: options?.transactionId,
532
+ bypassCache: options?.bypassCache,
533
+ });
534
+ }
535
+ catch (error) {
536
+ // If not found, insert instead
537
+ if (error.message?.includes("404") ||
538
+ error.message?.includes("Not found")) {
539
+ return await this.insert(collection, record, {
540
+ ttl: options?.ttl,
541
+ bypassRipple: options?.bypassRipple,
542
+ transactionId: options?.transactionId,
543
+ bypassCache: options?.bypassCache,
544
+ });
545
+ }
546
+ throw error;
547
+ }
548
+ }
549
+ /**
550
+ * Find a single record by field value
551
+ *
552
+ * Convenience method for finding one record matching a specific field value.
553
+ * Returns null if no record matches, or the first matching record.
554
+ *
555
+ * @param collection - Collection name
556
+ * @param field - Field name to search
557
+ * @param value - Value to match
558
+ * @returns The matching record or null if not found
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * // Find user by email
563
+ * const user = await client.findOne("users", "email", "john@example.com");
564
+ * if (user) {
565
+ * console.log("Found user:", user);
566
+ * }
567
+ * ```
568
+ */
569
+ async findOne(collection, field, value) {
570
+ const query = new query_builder_1.QueryBuilder().eq(field, value).limit(1).build();
571
+ const results = await this.find(collection, query);
572
+ return results.length > 0 ? results[0] : null;
573
+ }
574
+ /**
575
+ * Check if a record exists by ID
576
+ *
577
+ * This is more efficient than fetching the record when you only need to check existence.
578
+ *
579
+ * @param collection - Collection name
580
+ * @param id - Record ID to check
581
+ * @returns true if the record exists, false if it doesn't
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * if (await client.exists("users", "user123")) {
586
+ * console.log("User exists");
587
+ * } else {
588
+ * console.log("User not found");
589
+ * }
590
+ * ```
591
+ */
592
+ async exists(collection, id) {
593
+ try {
594
+ await this.findById(collection, id);
595
+ return true;
596
+ }
597
+ catch (error) {
598
+ if (error.message?.includes("404") ||
599
+ error.message?.includes("Not found")) {
600
+ return false;
601
+ }
602
+ throw error;
603
+ }
604
+ }
605
+ /**
606
+ * Paginate through records
607
+ *
608
+ * Convenience method for pagination with page numbers (1-indexed).
609
+ *
610
+ * @param collection - Collection name
611
+ * @param page - Page number (1-indexed, i.e., first page is 1)
612
+ * @param pageSize - Number of records per page
613
+ * @returns Array of records for the requested page
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * // Get page 2 with 10 records per page
618
+ * const records = await client.paginate("users", 2, 10);
619
+ * ```
620
+ */
621
+ async paginate(collection, page, pageSize) {
622
+ // Page 1 = skip 0, Page 2 = skip pageSize, etc.
623
+ const skip = page > 0 ? (page - 1) * pageSize : 0;
624
+ const query = {
625
+ limit: pageSize,
626
+ skip: skip,
627
+ };
628
+ return this.find(collection, query);
629
+ }
441
630
  /**
442
631
  * List all collections
443
632
  */
@@ -451,6 +640,29 @@ class EkoDBClient {
451
640
  async deleteCollection(collection) {
452
641
  await this.makeRequest("DELETE", `/api/collections/${collection}`, undefined, 0, true);
453
642
  }
643
+ /**
644
+ * Restore a deleted record from trash
645
+ * Records remain in trash for 30 days before permanent deletion
646
+ *
647
+ * @param collection - Collection name
648
+ * @param id - Record ID to restore
649
+ * @returns true if restored successfully
650
+ */
651
+ async restoreRecord(collection, id) {
652
+ const result = await this.makeRequest("POST", `/api/trash/${collection}/${id}`, undefined, 0, true);
653
+ return result.status === "restored";
654
+ }
655
+ /**
656
+ * Restore all deleted records in a collection from trash
657
+ * Records remain in trash for 30 days before permanent deletion
658
+ *
659
+ * @param collection - Collection name
660
+ * @returns Number of records restored
661
+ */
662
+ async restoreCollection(collection) {
663
+ const result = await this.makeRequest("POST", `/api/trash/${collection}`, undefined, 0, true);
664
+ return { recordsRestored: result.records_restored };
665
+ }
454
666
  /**
455
667
  * Create a collection with schema
456
668
  *
@@ -523,11 +735,21 @@ class EkoDBClient {
523
735
  * );
524
736
  * ```
525
737
  */
526
- async search(collection, searchQuery) {
527
- const queryObj = searchQuery instanceof search_1.SearchQueryBuilder
528
- ? searchQuery.build()
529
- : searchQuery;
530
- return this.makeRequest("POST", `/api/search/${collection}`, queryObj, 0, true);
738
+ async search(collection, query) {
739
+ // Ensure all parameters from SearchQuery are sent to server
740
+ return this.makeRequest("POST", `/api/search/${collection}`, query, 0, true);
741
+ }
742
+ /**
743
+ * Health check - verify the ekoDB server is responding
744
+ */
745
+ async health() {
746
+ try {
747
+ const result = await this.makeRequest("GET", "/api/health", undefined, 0, true);
748
+ return result.status === "ok";
749
+ }
750
+ catch {
751
+ return false;
752
+ }
531
753
  }
532
754
  // ========== Chat Methods ==========
533
755
  /**
@@ -752,26 +974,29 @@ class EkoDBClient {
752
974
  * Simplified text search with full-text matching, fuzzy search, and stemming.
753
975
  *
754
976
  * @param collection - Collection name to search
755
- * @param queryText - Search query text
756
- * @param limit - Maximum number of results to return
757
- * @returns Array of matching records
977
+ * @param query - Search query text
978
+ * @param options - Additional search options
979
+ * @returns Search response with results and metadata
758
980
  *
759
981
  * @example
760
982
  * ```typescript
761
983
  * const results = await client.textSearch(
762
984
  * "documents",
763
985
  * "ownership system",
764
- * 10
986
+ * {
987
+ * limit: 10,
988
+ * select_fields: ["title", "content"],
989
+ * exclude_fields: ["author"]
990
+ * }
765
991
  * );
766
992
  * ```
767
993
  */
768
- async textSearch(collection, queryText, limit) {
994
+ async textSearch(collection, query, options) {
769
995
  const searchQuery = {
770
- query: queryText,
771
- limit,
996
+ query,
997
+ ...options,
772
998
  };
773
- const response = await this.search(collection, searchQuery);
774
- return response.results.map((r) => r.record);
999
+ return this.search(collection, searchQuery);
775
1000
  }
776
1001
  /**
777
1002
  * Perform hybrid search combining text and vector search
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Unit tests for ekoDB TypeScript client
3
+ *
4
+ * These tests use vitest and mock fetch to test client methods
5
+ * without requiring a running ekoDB server.
6
+ */
7
+ export {};