@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.
- package/dist/client.d.ts +213 -9
- package/dist/client.js +424 -25
- package/dist/client.test.js +246 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/websocket.test.d.ts +6 -0
- package/dist/websocket.test.js +407 -0
- package/package.json +2 -1
- package/src/client.test.ts +341 -1
- package/src/client.ts +612 -30
- package/src/index.ts +10 -0
- package/src/websocket.test.ts +575 -0
- package/tsconfig.json +3 -1
package/src/client.test.ts
CHANGED
|
@@ -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
|
+
});
|