@ekodb/ekodb-client 0.20.0 → 0.21.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/README.md +28 -0
- package/dist/client.d.ts +90 -12
- package/dist/client.js +253 -56
- package/dist/client.test.js +170 -0
- package/dist/websocket.test.js +159 -5
- package/package.json +1 -1
- package/src/client.test.ts +244 -0
- package/src/client.ts +344 -65
- package/src/websocket.test.ts +225 -5
package/src/client.test.ts
CHANGED
|
@@ -350,6 +350,15 @@ describe("EkoDBClient KV store", () => {
|
|
|
350
350
|
await expect(client.kvDelete("my_key")).resolves.not.toThrow();
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
+
it("clears KV store", async () => {
|
|
354
|
+
const client = createTestClient();
|
|
355
|
+
|
|
356
|
+
mockTokenResponse();
|
|
357
|
+
mockJsonResponse({ message: "success" });
|
|
358
|
+
|
|
359
|
+
await expect(client.kvClear()).resolves.not.toThrow();
|
|
360
|
+
});
|
|
361
|
+
|
|
353
362
|
it("checks KV exists", async () => {
|
|
354
363
|
const client = createTestClient();
|
|
355
364
|
|
|
@@ -414,6 +423,17 @@ describe("EkoDBClient collections", () => {
|
|
|
414
423
|
expect(result).toHaveLength(3);
|
|
415
424
|
});
|
|
416
425
|
|
|
426
|
+
it("lists user collections (excludes internal)", async () => {
|
|
427
|
+
const client = createTestClient();
|
|
428
|
+
|
|
429
|
+
mockTokenResponse();
|
|
430
|
+
mockJsonResponse({ collections: ["users", "posts"] });
|
|
431
|
+
|
|
432
|
+
const result = await client.listUserCollections();
|
|
433
|
+
|
|
434
|
+
expect(result).toEqual(["users", "posts"]);
|
|
435
|
+
});
|
|
436
|
+
|
|
417
437
|
it("deletes collection", async () => {
|
|
418
438
|
const client = createTestClient();
|
|
419
439
|
|
|
@@ -994,6 +1014,84 @@ describe("EkoDBClient transaction status", () => {
|
|
|
994
1014
|
});
|
|
995
1015
|
});
|
|
996
1016
|
|
|
1017
|
+
describe("bypass_ripple on the transactional read path", () => {
|
|
1018
|
+
it("findById sends bypass_ripple AND transaction_id together (query params)", async () => {
|
|
1019
|
+
const client = createTestClient();
|
|
1020
|
+
|
|
1021
|
+
mockTokenResponse();
|
|
1022
|
+
mockJsonResponse({ id: "user_123", name: "Alice" });
|
|
1023
|
+
|
|
1024
|
+
await client.findById("users", "user_123", {
|
|
1025
|
+
bypassRipple: true,
|
|
1026
|
+
transactionId: "tx_123",
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
1030
|
+
expect(init.method).toBe("GET");
|
|
1031
|
+
const parsed = new URL(url as string);
|
|
1032
|
+
expect(parsed.searchParams.get("bypass_ripple")).toBe("true");
|
|
1033
|
+
expect(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it("findById sends bypass_ripple=false explicitly alongside transaction_id", async () => {
|
|
1037
|
+
const client = createTestClient();
|
|
1038
|
+
|
|
1039
|
+
mockTokenResponse();
|
|
1040
|
+
mockJsonResponse({ id: "user_123", name: "Alice" });
|
|
1041
|
+
|
|
1042
|
+
await client.findById("users", "user_123", {
|
|
1043
|
+
bypassRipple: false,
|
|
1044
|
+
transactionId: "tx_123",
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const [url] = mockFetch.mock.calls[1];
|
|
1048
|
+
const parsed = new URL(url as string);
|
|
1049
|
+
expect(parsed.searchParams.get("bypass_ripple")).toBe("false");
|
|
1050
|
+
expect(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("find sends bypass_ripple AND transaction_id together as query params", async () => {
|
|
1054
|
+
const client = createTestClient();
|
|
1055
|
+
|
|
1056
|
+
mockTokenResponse();
|
|
1057
|
+
mockJsonResponse([{ id: "user_1", name: "Alice" }]);
|
|
1058
|
+
|
|
1059
|
+
await client.find(
|
|
1060
|
+
"users",
|
|
1061
|
+
{ limit: 10 },
|
|
1062
|
+
{ bypassRipple: true, transactionId: "tx_123" },
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
1066
|
+
expect(init.method).toBe("POST");
|
|
1067
|
+
const parsed = new URL(url as string);
|
|
1068
|
+
// bypass_ripple is a query param (like every other method), not in the body.
|
|
1069
|
+
expect(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
1070
|
+
expect(parsed.searchParams.get("bypass_ripple")).toBe("true");
|
|
1071
|
+
const body = JSON.parse(init.body as string);
|
|
1072
|
+
expect(body.bypass_ripple).toBeUndefined();
|
|
1073
|
+
expect(body.limit).toBe(10);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it("find hoists bypass_ripple from the query object into the query string", async () => {
|
|
1077
|
+
const client = createTestClient();
|
|
1078
|
+
|
|
1079
|
+
mockTokenResponse();
|
|
1080
|
+
mockJsonResponse([]);
|
|
1081
|
+
|
|
1082
|
+
// A query object carrying bypass_ripple, as QueryBuilder.bypassRipple() builds.
|
|
1083
|
+
await client.find("users", { limit: 5, bypass_ripple: true } as any);
|
|
1084
|
+
|
|
1085
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
1086
|
+
const parsed = new URL(url as string);
|
|
1087
|
+
// Hoisted to the query string, removed from the body.
|
|
1088
|
+
expect(parsed.searchParams.get("bypass_ripple")).toBe("true");
|
|
1089
|
+
const body = JSON.parse(init.body as string);
|
|
1090
|
+
expect(body.bypass_ripple).toBeUndefined();
|
|
1091
|
+
expect(body.limit).toBe(5);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
997
1095
|
// ============================================================================
|
|
998
1096
|
// Convenience Methods Tests
|
|
999
1097
|
// ============================================================================
|
|
@@ -1821,6 +1919,26 @@ describe("findByIdWithProjection", () => {
|
|
|
1821
1919
|
const dataCall = calls[1];
|
|
1822
1920
|
expect(dataCall[0]).toBe("http://localhost:8080/api/find/users/123");
|
|
1823
1921
|
});
|
|
1922
|
+
|
|
1923
|
+
it("appends transaction_id when a transactionId is given", async () => {
|
|
1924
|
+
const client = createTestClient();
|
|
1925
|
+
|
|
1926
|
+
mockTokenResponse();
|
|
1927
|
+
mockJsonResponse({ id: "123", name: "Alice" });
|
|
1928
|
+
|
|
1929
|
+
await client.findByIdWithProjection(
|
|
1930
|
+
"users",
|
|
1931
|
+
"123",
|
|
1932
|
+
["name"],
|
|
1933
|
+
undefined,
|
|
1934
|
+
"txn-abc",
|
|
1935
|
+
);
|
|
1936
|
+
|
|
1937
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
1938
|
+
const dataCall = calls[1];
|
|
1939
|
+
expect(dataCall[0]).toContain("transaction_id=txn-abc");
|
|
1940
|
+
expect(dataCall[0]).toContain("select_fields=name");
|
|
1941
|
+
});
|
|
1824
1942
|
});
|
|
1825
1943
|
|
|
1826
1944
|
// ============================================================================
|
|
@@ -3108,3 +3226,129 @@ describe("extractRecordId", () => {
|
|
|
3108
3226
|
expect(extractRecordId({ _id: "underscore" })).toBe("underscore");
|
|
3109
3227
|
});
|
|
3110
3228
|
});
|
|
3229
|
+
|
|
3230
|
+
// ============================================================================
|
|
3231
|
+
// URL Path Segment Encoding Tests
|
|
3232
|
+
//
|
|
3233
|
+
// Every caller-supplied path segment (collection, id, function label, chat
|
|
3234
|
+
// model/provider, session/message ids, etc.) must be percent-encoded so a
|
|
3235
|
+
// reserved char (`/`, space, `#`, `?`) can't break the URL. This matches the
|
|
3236
|
+
// Rust and Go clients. Query parameters are NOT path segments and go through
|
|
3237
|
+
// URLSearchParams, so they are out of scope here.
|
|
3238
|
+
// ============================================================================
|
|
3239
|
+
|
|
3240
|
+
describe("EkoDBClient URL path segment encoding", () => {
|
|
3241
|
+
it("findById encodes a reserved-char id (a/b -> a%2Fb)", async () => {
|
|
3242
|
+
const client = createTestClient();
|
|
3243
|
+
|
|
3244
|
+
mockTokenResponse();
|
|
3245
|
+
mockJsonResponse({ id: "a/b", name: "Alice" });
|
|
3246
|
+
|
|
3247
|
+
await client.findById("users", "a/b");
|
|
3248
|
+
|
|
3249
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3250
|
+
expect(url as string).toContain("/api/find/users/a%2Fb");
|
|
3251
|
+
expect(url as string).not.toContain("/api/find/users/a/b");
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
it("findById encodes a reserved-char collection", async () => {
|
|
3255
|
+
const client = createTestClient();
|
|
3256
|
+
|
|
3257
|
+
mockTokenResponse();
|
|
3258
|
+
mockJsonResponse({ id: "user_123" });
|
|
3259
|
+
|
|
3260
|
+
await client.findById("my coll", "user_123");
|
|
3261
|
+
|
|
3262
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3263
|
+
expect(url as string).toContain("/api/find/my%20coll/user_123");
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
it("findById leaves a normal id unchanged (/api/find/users/123)", async () => {
|
|
3267
|
+
const client = createTestClient();
|
|
3268
|
+
|
|
3269
|
+
mockTokenResponse();
|
|
3270
|
+
mockJsonResponse({ id: "123", name: "Alice" });
|
|
3271
|
+
|
|
3272
|
+
await client.findById("users", "123");
|
|
3273
|
+
|
|
3274
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3275
|
+
expect(url as string).toContain("/api/find/users/123");
|
|
3276
|
+
expect(url as string).not.toContain("%2F");
|
|
3277
|
+
expect(url as string).not.toContain("%20");
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
it("callFunction encodes a label containing a slash (anthropic/claude)", async () => {
|
|
3281
|
+
const client = createTestClient();
|
|
3282
|
+
|
|
3283
|
+
mockTokenResponse();
|
|
3284
|
+
mockJsonResponse({ result: { ok: true } });
|
|
3285
|
+
|
|
3286
|
+
await client.callFunction("anthropic/claude", {});
|
|
3287
|
+
|
|
3288
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3289
|
+
expect(url as string).toContain("/api/functions/anthropic%2Fclaude");
|
|
3290
|
+
expect(url as string).not.toContain("/api/functions/anthropic/claude");
|
|
3291
|
+
});
|
|
3292
|
+
|
|
3293
|
+
it("getUserFunction encodes a label containing reserved chars", async () => {
|
|
3294
|
+
const client = createTestClient();
|
|
3295
|
+
|
|
3296
|
+
mockTokenResponse();
|
|
3297
|
+
mockJsonResponse({ label: "items get/by id" });
|
|
3298
|
+
|
|
3299
|
+
await client.getUserFunction("items get/by id");
|
|
3300
|
+
|
|
3301
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3302
|
+
expect(url as string).toContain("/api/functions/items%20get%2Fby%20id");
|
|
3303
|
+
});
|
|
3304
|
+
|
|
3305
|
+
it("getChatModel encodes a provider containing a slash (anthropic/claude)", async () => {
|
|
3306
|
+
const client = createTestClient();
|
|
3307
|
+
|
|
3308
|
+
mockTokenResponse();
|
|
3309
|
+
mockJsonResponse(["claude-3"]);
|
|
3310
|
+
|
|
3311
|
+
await client.getChatModel("anthropic/claude");
|
|
3312
|
+
|
|
3313
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3314
|
+
expect(url as string).toContain("/api/chat_models/anthropic%2Fclaude");
|
|
3315
|
+
expect(url as string).not.toContain("/api/chat_models/anthropic/claude");
|
|
3316
|
+
});
|
|
3317
|
+
|
|
3318
|
+
it("deleteCollection encodes a reserved-char collection", async () => {
|
|
3319
|
+
const client = createTestClient();
|
|
3320
|
+
|
|
3321
|
+
mockTokenResponse();
|
|
3322
|
+
mockJsonResponse({ status: "deleted" });
|
|
3323
|
+
|
|
3324
|
+
await client.deleteCollection("a/b");
|
|
3325
|
+
|
|
3326
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3327
|
+
expect(url as string).toContain("/api/collections/a%2Fb");
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
it("getChatSessionMessages encodes the session id path segment", async () => {
|
|
3331
|
+
const client = createTestClient();
|
|
3332
|
+
|
|
3333
|
+
mockTokenResponse();
|
|
3334
|
+
mockJsonResponse({ messages: [], total: 0 });
|
|
3335
|
+
|
|
3336
|
+
await client.getChatSessionMessages("sess/1");
|
|
3337
|
+
|
|
3338
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3339
|
+
expect(url as string).toContain("/api/chat/sess%2F1/messages");
|
|
3340
|
+
expect(url as string).not.toContain("/api/chat/sess/1/messages");
|
|
3341
|
+
});
|
|
3342
|
+
|
|
3343
|
+
it("chatMessage encodes the session id and leaves a normal one unchanged", async () => {
|
|
3344
|
+
const client = createTestClient();
|
|
3345
|
+
|
|
3346
|
+
mockTokenResponse();
|
|
3347
|
+
mockJsonResponse({ message_id: "m1", content: "hi" });
|
|
3348
|
+
|
|
3349
|
+
await client.chatMessage("sess#1", { message: "hi" });
|
|
3350
|
+
|
|
3351
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3352
|
+
expect(url as string).toContain("/api/chat/sess%231/messages");
|
|
3353
|
+
});
|
|
3354
|
+
});
|