@ekodb/ekodb-client 0.12.0 → 0.13.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.
@@ -85,7 +85,6 @@ describe("EkoDBClient configuration", () => {
85
85
  const client = new EkoDBClient({
86
86
  baseURL: "http://localhost:8080",
87
87
  apiKey: "test-key",
88
- timeout: 60000,
89
88
  maxRetries: 5,
90
89
  shouldRetry: true,
91
90
  format: SerializationFormat.Json,
@@ -190,6 +189,80 @@ describe("EkoDBClient update", () => {
190
189
  });
191
190
  });
192
191
 
192
+ // ============================================================================
193
+ // Atomic Field Action Tests
194
+ // ============================================================================
195
+
196
+ describe("EkoDBClient updateWithAction", () => {
197
+ it("increments a field", async () => {
198
+ const client = createTestClient();
199
+
200
+ mockTokenResponse();
201
+ mockJsonResponse({ id: "rec_1", views: 42 });
202
+
203
+ const result = await client.updateWithAction(
204
+ "counters",
205
+ "rec_1",
206
+ "increment",
207
+ "views",
208
+ 1,
209
+ );
210
+
211
+ expect(result).toHaveProperty("views", 42);
212
+ });
213
+
214
+ it("pushes to an array field", async () => {
215
+ const client = createTestClient();
216
+
217
+ mockTokenResponse();
218
+ mockJsonResponse({ id: "rec_2", tags: ["rust", "new-tag"] });
219
+
220
+ const result = await client.updateWithAction(
221
+ "lists",
222
+ "rec_2",
223
+ "push",
224
+ "tags",
225
+ "new-tag",
226
+ );
227
+
228
+ expect(result.tags).toContain("new-tag");
229
+ });
230
+
231
+ it("clears a field without value", async () => {
232
+ const client = createTestClient();
233
+
234
+ mockTokenResponse();
235
+ mockJsonResponse({ id: "rec_3", temp: 0 });
236
+
237
+ const result = await client.updateWithAction(
238
+ "data",
239
+ "rec_3",
240
+ "clear",
241
+ "temp",
242
+ );
243
+
244
+ expect(result).toHaveProperty("temp", 0);
245
+ });
246
+ });
247
+
248
+ describe("EkoDBClient updateWithActionSequence", () => {
249
+ it("applies multiple actions atomically", async () => {
250
+ const client = createTestClient();
251
+
252
+ mockTokenResponse();
253
+ mockJsonResponse({ id: "player_1", score: 110, lives: 2 });
254
+
255
+ const result = await client.updateWithActionSequence("game", "player_1", [
256
+ ["increment", "score", 10],
257
+ ["decrement", "lives", 1],
258
+ ["push", "log", "hit"],
259
+ ]);
260
+
261
+ expect(result).toHaveProperty("score", 110);
262
+ expect(result).toHaveProperty("lives", 2);
263
+ });
264
+ });
265
+
193
266
  // ============================================================================
194
267
  // Delete Tests
195
268
  // ============================================================================
@@ -1243,3 +1316,270 @@ describe("EkoDBClient collection utilities", () => {
1243
1316
  expect(result).toBe(0);
1244
1317
  });
1245
1318
  });
1319
+
1320
+ // ============================================================================
1321
+ // Distinct Values Tests
1322
+ // ============================================================================
1323
+
1324
+ describe("EkoDBClient distinctValues", () => {
1325
+ it("returns distinct values for a field", async () => {
1326
+ const client = createTestClient();
1327
+
1328
+ mockTokenResponse();
1329
+ mockJsonResponse({
1330
+ collection: "products",
1331
+ field: "category",
1332
+ values: ["books", "electronics", "food"],
1333
+ count: 3,
1334
+ });
1335
+
1336
+ const result = await client.distinctValues("products", "category");
1337
+
1338
+ expect(result.collection).toBe("products");
1339
+ expect(result.field).toBe("category");
1340
+ expect(result.count).toBe(3);
1341
+ expect(result.values).toHaveLength(3);
1342
+ expect(result.values).toContain("books");
1343
+ });
1344
+
1345
+ it("sends filter in request body", async () => {
1346
+ const client = createTestClient();
1347
+
1348
+ mockTokenResponse();
1349
+ mockJsonResponse({
1350
+ collection: "orders",
1351
+ field: "status",
1352
+ values: ["active", "pending"],
1353
+ count: 2,
1354
+ });
1355
+
1356
+ const filter = {
1357
+ type: "Condition",
1358
+ content: { field: "region", operator: "Eq", value: "us" },
1359
+ };
1360
+ const result = await client.distinctValues("orders", "status", { filter });
1361
+
1362
+ expect(result.count).toBe(2);
1363
+ expect(result.values).toContain("active");
1364
+ });
1365
+
1366
+ it("returns empty values for collection with no matching records", async () => {
1367
+ const client = createTestClient();
1368
+
1369
+ mockTokenResponse();
1370
+ mockJsonResponse({
1371
+ collection: "empty",
1372
+ field: "tag",
1373
+ values: [],
1374
+ count: 0,
1375
+ });
1376
+
1377
+ const result = await client.distinctValues("empty", "tag");
1378
+
1379
+ expect(result.count).toBe(0);
1380
+ expect(result.values).toHaveLength(0);
1381
+ });
1382
+
1383
+ it("calls correct endpoint", async () => {
1384
+ const client = createTestClient();
1385
+
1386
+ mockTokenResponse();
1387
+ mockJsonResponse({
1388
+ collection: "users",
1389
+ field: "role",
1390
+ values: ["admin", "user"],
1391
+ count: 2,
1392
+ });
1393
+
1394
+ await client.distinctValues("users", "role");
1395
+
1396
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1397
+ const dataCall = calls[1]; // calls[0] is token
1398
+ expect(dataCall[0]).toContain("/api/distinct/users/role");
1399
+ expect(dataCall[1]?.method).toBe("POST");
1400
+ });
1401
+ });
1402
+
1403
+ // ============================================================================
1404
+ // Raw Completion Tests
1405
+ // ============================================================================
1406
+
1407
+ describe("EkoDBClient rawCompletion", () => {
1408
+ it("returns content from LLM response", async () => {
1409
+ const client = createTestClient();
1410
+
1411
+ mockTokenResponse();
1412
+ mockJsonResponse({ content: "The answer is 42." });
1413
+
1414
+ const result = await client.rawCompletion({
1415
+ system_prompt: "You are a helpful assistant.",
1416
+ message: "What is the answer?",
1417
+ });
1418
+
1419
+ expect(result.content).toBe("The answer is 42.");
1420
+ });
1421
+
1422
+ it("sends all fields in request body", async () => {
1423
+ const client = createTestClient();
1424
+
1425
+ mockTokenResponse();
1426
+ mockJsonResponse({ content: "Response text." });
1427
+
1428
+ await client.rawCompletion({
1429
+ system_prompt: "System.",
1430
+ message: "User.",
1431
+ provider: "openai",
1432
+ model: "gpt-4o",
1433
+ max_tokens: 512,
1434
+ });
1435
+
1436
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1437
+ const dataCall = calls[1]; // calls[0] is token
1438
+ const body = JSON.parse(dataCall[1]?.body as string);
1439
+ expect(body.system_prompt).toBe("System.");
1440
+ expect(body.message).toBe("User.");
1441
+ expect(body.provider).toBe("openai");
1442
+ expect(body.model).toBe("gpt-4o");
1443
+ expect(body.max_tokens).toBe(512);
1444
+ });
1445
+
1446
+ it("omits optional fields when not provided", async () => {
1447
+ const client = createTestClient();
1448
+
1449
+ mockTokenResponse();
1450
+ mockJsonResponse({ content: "Response." });
1451
+
1452
+ await client.rawCompletion({
1453
+ system_prompt: "System.",
1454
+ message: "User.",
1455
+ });
1456
+
1457
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1458
+ const dataCall = calls[1];
1459
+ const body = JSON.parse(dataCall[1]?.body as string);
1460
+ expect(body.provider).toBeUndefined();
1461
+ expect(body.model).toBeUndefined();
1462
+ expect(body.max_tokens).toBeUndefined();
1463
+ });
1464
+
1465
+ it("calls correct endpoint with POST method", async () => {
1466
+ const client = createTestClient();
1467
+
1468
+ mockTokenResponse();
1469
+ mockJsonResponse({ content: "Response." });
1470
+
1471
+ await client.rawCompletion({
1472
+ system_prompt: "System.",
1473
+ message: "User.",
1474
+ });
1475
+
1476
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1477
+ const dataCall = calls[1];
1478
+ expect(dataCall[0]).toContain("/api/chat/complete");
1479
+ expect(dataCall[1]?.method).toBe("POST");
1480
+ });
1481
+ });
1482
+
1483
+ // ============================================================================
1484
+ // Token Management Tests
1485
+ // ============================================================================
1486
+
1487
+ describe("refreshToken", () => {
1488
+ it("fetches a new token", async () => {
1489
+ const client = createTestClient();
1490
+
1491
+ // First token fetch (init)
1492
+ mockTokenResponse();
1493
+ mockJsonResponse({ status: "ok" });
1494
+ await client.health();
1495
+
1496
+ // Refresh token
1497
+ mockFetch.mockResolvedValueOnce({
1498
+ ok: true,
1499
+ status: 200,
1500
+ json: async () => ({ token: "new-jwt-token" }),
1501
+ headers: new Headers(),
1502
+ });
1503
+ await client.refreshToken();
1504
+
1505
+ // Verify it called the token endpoint again
1506
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1507
+ const tokenCalls = calls.filter((c: unknown[]) =>
1508
+ (c[0] as string).includes("/api/auth/token"),
1509
+ );
1510
+ expect(tokenCalls.length).toBe(2);
1511
+ });
1512
+ });
1513
+
1514
+ describe("clearTokenCache", () => {
1515
+ it("clears the cached token", async () => {
1516
+ const client = createTestClient();
1517
+
1518
+ // Init with token
1519
+ mockTokenResponse();
1520
+ mockJsonResponse({ status: "ok" });
1521
+ await client.health();
1522
+
1523
+ // Clear cache
1524
+ client.clearTokenCache();
1525
+
1526
+ // Next request should fetch a new token
1527
+ mockTokenResponse();
1528
+ mockJsonResponse({ status: "ok" });
1529
+ await client.health();
1530
+
1531
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1532
+ const tokenCalls = calls.filter((c: unknown[]) =>
1533
+ (c[0] as string).includes("/api/auth/token"),
1534
+ );
1535
+ expect(tokenCalls.length).toBe(2);
1536
+ });
1537
+ });
1538
+
1539
+ // ============================================================================
1540
+ // findByIdWithProjection Tests
1541
+ // ============================================================================
1542
+
1543
+ describe("findByIdWithProjection", () => {
1544
+ it("calls correct endpoint with select_fields", async () => {
1545
+ const client = createTestClient();
1546
+
1547
+ mockTokenResponse();
1548
+ mockJsonResponse({ id: "123", name: "Alice" });
1549
+
1550
+ await client.findByIdWithProjection("users", "123", ["name", "email"]);
1551
+
1552
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1553
+ const dataCall = calls[1];
1554
+ expect(dataCall[0]).toContain("/api/find/users/123");
1555
+ expect(dataCall[0]).toContain("select_fields=name%2Cemail");
1556
+ });
1557
+
1558
+ it("calls correct endpoint with exclude_fields", async () => {
1559
+ const client = createTestClient();
1560
+
1561
+ mockTokenResponse();
1562
+ mockJsonResponse({ id: "123", name: "Alice" });
1563
+
1564
+ await client.findByIdWithProjection("users", "123", undefined, [
1565
+ "password",
1566
+ ]);
1567
+
1568
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1569
+ const dataCall = calls[1];
1570
+ expect(dataCall[0]).toContain("exclude_fields=password");
1571
+ });
1572
+
1573
+ it("calls without params when no projection", async () => {
1574
+ const client = createTestClient();
1575
+
1576
+ mockTokenResponse();
1577
+ mockJsonResponse({ id: "123", name: "Alice" });
1578
+
1579
+ await client.findByIdWithProjection("users", "123");
1580
+
1581
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1582
+ const dataCall = calls[1];
1583
+ expect(dataCall[0]).toBe("http://localhost:8080/api/find/users/123");
1584
+ });
1585
+ });