@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.
@@ -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
+ });