@ekodb/ekodb-client 0.22.0 → 0.23.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 +17 -3
- package/dist/client.js +53 -8
- package/dist/client.test.js +63 -0
- package/package.json +1 -1
- package/src/client.test.ts +83 -0
- package/src/client.ts +58 -13
package/dist/client.d.ts
CHANGED
|
@@ -508,16 +508,30 @@ export declare class EkoDBClient {
|
|
|
508
508
|
batchInsert(collection: string, records: Record[], options?: BatchInsertOptions): Promise<BatchOperationResult>;
|
|
509
509
|
/**
|
|
510
510
|
* Batch update multiple documents
|
|
511
|
+
* @param collection - Collection name
|
|
512
|
+
* @param updates - Array of updates ({ id, data, bypassRipple? })
|
|
513
|
+
* @param options - Optional parameters (bypassRipple, transactionId). When
|
|
514
|
+
* `transactionId` is set, the batch is staged into that MVCC transaction
|
|
515
|
+
* (sent as the `transaction_id` query param) instead of committed
|
|
516
|
+
* immediately — mirrors the single-record update.
|
|
511
517
|
*/
|
|
512
518
|
batchUpdate(collection: string, updates: Array<{
|
|
513
519
|
id: string;
|
|
514
520
|
data: Record;
|
|
515
521
|
bypassRipple?: boolean;
|
|
516
|
-
}
|
|
522
|
+
}>, options?: BatchUpdateOptions): Promise<BatchOperationResult>;
|
|
517
523
|
/**
|
|
518
524
|
* Batch delete multiple documents
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
* @param collection - Collection name
|
|
526
|
+
* @param ids - Document IDs to delete
|
|
527
|
+
* @param bypassRipple - Optional flag to bypass ripple propagation (legacy
|
|
528
|
+
* positional form, kept for back-compat)
|
|
529
|
+
* @param options - Optional parameters (bypassRipple, transactionId). When
|
|
530
|
+
* `transactionId` is set, the batch is staged into that MVCC transaction
|
|
531
|
+
* (sent as the `transaction_id` query param) instead of committed
|
|
532
|
+
* immediately — mirrors the single-record delete.
|
|
533
|
+
*/
|
|
534
|
+
batchDelete(collection: string, ids: string[], bypassRipple?: boolean, options?: BatchDeleteOptions): Promise<BatchOperationResult>;
|
|
521
535
|
/**
|
|
522
536
|
* Set a key-value pair with optional TTL
|
|
523
537
|
* @param key - The key to set
|
package/dist/client.js
CHANGED
|
@@ -599,24 +599,61 @@ class EkoDBClient {
|
|
|
599
599
|
}
|
|
600
600
|
/**
|
|
601
601
|
* Batch update multiple documents
|
|
602
|
+
* @param collection - Collection name
|
|
603
|
+
* @param updates - Array of updates ({ id, data, bypassRipple? })
|
|
604
|
+
* @param options - Optional parameters (bypassRipple, transactionId). When
|
|
605
|
+
* `transactionId` is set, the batch is staged into that MVCC transaction
|
|
606
|
+
* (sent as the `transaction_id` query param) instead of committed
|
|
607
|
+
* immediately — mirrors the single-record update.
|
|
602
608
|
*/
|
|
603
|
-
async batchUpdate(collection, updates) {
|
|
609
|
+
async batchUpdate(collection, updates, options) {
|
|
610
|
+
const params = new URLSearchParams();
|
|
611
|
+
if (options?.bypassRipple !== undefined) {
|
|
612
|
+
params.append("bypass_ripple", String(options.bypassRipple));
|
|
613
|
+
}
|
|
614
|
+
if (options?.transactionId) {
|
|
615
|
+
params.append("transaction_id", options.transactionId);
|
|
616
|
+
}
|
|
604
617
|
const formattedUpdates = updates.map((u) => ({
|
|
605
618
|
id: u.id,
|
|
606
619
|
data: u.data,
|
|
607
620
|
bypass_ripple: u.bypassRipple,
|
|
608
621
|
}));
|
|
609
|
-
|
|
622
|
+
const url = params.toString()
|
|
623
|
+
? `/api/batch/update/${encodeURIComponent(collection)}?${params.toString()}`
|
|
624
|
+
: `/api/batch/update/${encodeURIComponent(collection)}`;
|
|
625
|
+
return this.makeRequest("PUT", url, {
|
|
626
|
+
updates: formattedUpdates,
|
|
627
|
+
});
|
|
610
628
|
}
|
|
611
629
|
/**
|
|
612
630
|
* Batch delete multiple documents
|
|
613
|
-
|
|
614
|
-
|
|
631
|
+
* @param collection - Collection name
|
|
632
|
+
* @param ids - Document IDs to delete
|
|
633
|
+
* @param bypassRipple - Optional flag to bypass ripple propagation (legacy
|
|
634
|
+
* positional form, kept for back-compat)
|
|
635
|
+
* @param options - Optional parameters (bypassRipple, transactionId). When
|
|
636
|
+
* `transactionId` is set, the batch is staged into that MVCC transaction
|
|
637
|
+
* (sent as the `transaction_id` query param) instead of committed
|
|
638
|
+
* immediately — mirrors the single-record delete.
|
|
639
|
+
*/
|
|
640
|
+
async batchDelete(collection, ids, bypassRipple, options) {
|
|
641
|
+
// bypass_ripple is sent per-item in the body (existing behavior). The
|
|
642
|
+
// effective value prefers the explicit positional arg, falling back to the
|
|
643
|
+
// options object so callers can use either form.
|
|
644
|
+
const effectiveBypassRipple = bypassRipple !== undefined ? bypassRipple : options?.bypassRipple;
|
|
645
|
+
const params = new URLSearchParams();
|
|
646
|
+
if (options?.transactionId) {
|
|
647
|
+
params.append("transaction_id", options.transactionId);
|
|
648
|
+
}
|
|
615
649
|
const deletes = ids.map((id) => ({
|
|
616
650
|
id: id,
|
|
617
|
-
bypass_ripple:
|
|
651
|
+
bypass_ripple: effectiveBypassRipple,
|
|
618
652
|
}));
|
|
619
|
-
|
|
653
|
+
const url = params.toString()
|
|
654
|
+
? `/api/batch/delete/${encodeURIComponent(collection)}?${params.toString()}`
|
|
655
|
+
: `/api/batch/delete/${encodeURIComponent(collection)}`;
|
|
656
|
+
return this.makeRequest("DELETE", url, { deletes });
|
|
620
657
|
}
|
|
621
658
|
/**
|
|
622
659
|
* Set a key-value pair with optional TTL
|
|
@@ -1533,7 +1570,11 @@ class EkoDBClient {
|
|
|
1533
1570
|
* List all functions, optionally filtered by tags
|
|
1534
1571
|
*/
|
|
1535
1572
|
async listFunctions(tags) {
|
|
1536
|
-
|
|
1573
|
+
// URLSearchParams percent-encodes the value (`&`/`=`/`,`), so a tag
|
|
1574
|
+
// containing query-reserved characters can't smuggle extra params.
|
|
1575
|
+
const params = tags
|
|
1576
|
+
? `?${new URLSearchParams({ tags: tags.join(",") }).toString()}`
|
|
1577
|
+
: "";
|
|
1537
1578
|
return this.makeRequest("GET", `/api/functions${params}`);
|
|
1538
1579
|
}
|
|
1539
1580
|
/**
|
|
@@ -1580,7 +1621,11 @@ class EkoDBClient {
|
|
|
1580
1621
|
* @returns Array of user functions
|
|
1581
1622
|
*/
|
|
1582
1623
|
async listUserFunctions(tags) {
|
|
1583
|
-
|
|
1624
|
+
// URLSearchParams percent-encodes the value (`&`/`=`/`,`), so a tag
|
|
1625
|
+
// containing query-reserved characters can't smuggle extra params.
|
|
1626
|
+
const params = tags
|
|
1627
|
+
? `?${new URLSearchParams({ tags: tags.join(",") }).toString()}`
|
|
1628
|
+
: "";
|
|
1584
1629
|
return this.makeRequest("GET", `/api/functions${params}`, undefined, 0, true);
|
|
1585
1630
|
}
|
|
1586
1631
|
/**
|
package/dist/client.test.js
CHANGED
|
@@ -227,6 +227,56 @@ function mockErrorResponse(status, message) {
|
|
|
227
227
|
const result = await client.batchDelete("users", ["id_1", "id_2"]);
|
|
228
228
|
(0, vitest_1.expect)(result.successful).toHaveLength(2);
|
|
229
229
|
});
|
|
230
|
+
(0, vitest_1.it)("batchInsert sends transaction_id as a query param", async () => {
|
|
231
|
+
const client = createTestClient();
|
|
232
|
+
mockTokenResponse();
|
|
233
|
+
mockJsonResponse({ successful: ["id_1"], failed: [] });
|
|
234
|
+
await client.batchInsert("users", [{ name: "A" }], {
|
|
235
|
+
transactionId: "tx_123",
|
|
236
|
+
});
|
|
237
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
238
|
+
(0, vitest_1.expect)(init.method).toBe("POST");
|
|
239
|
+
const parsed = new URL(url);
|
|
240
|
+
(0, vitest_1.expect)(parsed.pathname).toBe("/api/batch/insert/users");
|
|
241
|
+
(0, vitest_1.expect)(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
242
|
+
});
|
|
243
|
+
(0, vitest_1.it)("batchUpdate sends transaction_id as a query param", async () => {
|
|
244
|
+
const client = createTestClient();
|
|
245
|
+
mockTokenResponse();
|
|
246
|
+
mockJsonResponse({ successful: ["id_1"], failed: [] });
|
|
247
|
+
await client.batchUpdate("users", [{ id: "id_1", data: { name: "B" } }], {
|
|
248
|
+
transactionId: "tx_123",
|
|
249
|
+
});
|
|
250
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
251
|
+
(0, vitest_1.expect)(init.method).toBe("PUT");
|
|
252
|
+
const parsed = new URL(url);
|
|
253
|
+
(0, vitest_1.expect)(parsed.pathname).toBe("/api/batch/update/users");
|
|
254
|
+
(0, vitest_1.expect)(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
255
|
+
});
|
|
256
|
+
(0, vitest_1.it)("batchDelete sends transaction_id as a query param", async () => {
|
|
257
|
+
const client = createTestClient();
|
|
258
|
+
mockTokenResponse();
|
|
259
|
+
mockJsonResponse({ successful: ["id_1", "id_2"], failed: [] });
|
|
260
|
+
await client.batchDelete("users", ["id_1", "id_2"], undefined, {
|
|
261
|
+
transactionId: "tx_123",
|
|
262
|
+
});
|
|
263
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
264
|
+
(0, vitest_1.expect)(init.method).toBe("DELETE");
|
|
265
|
+
const parsed = new URL(url);
|
|
266
|
+
(0, vitest_1.expect)(parsed.pathname).toBe("/api/batch/delete/users");
|
|
267
|
+
(0, vitest_1.expect)(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
268
|
+
});
|
|
269
|
+
(0, vitest_1.it)("batch ops without transaction_id send no such query param (additive)", async () => {
|
|
270
|
+
const client = createTestClient();
|
|
271
|
+
mockTokenResponse();
|
|
272
|
+
mockJsonResponse({ successful: ["id_1"], failed: [] });
|
|
273
|
+
// Legacy call sites: no options object at all.
|
|
274
|
+
await client.batchUpdate("users", [{ id: "id_1", data: { name: "B" } }]);
|
|
275
|
+
const [url] = mockFetch.mock.calls[1];
|
|
276
|
+
const parsed = new URL(url);
|
|
277
|
+
(0, vitest_1.expect)(parsed.searchParams.get("transaction_id")).toBeNull();
|
|
278
|
+
(0, vitest_1.expect)(parsed.search).toBe("");
|
|
279
|
+
});
|
|
230
280
|
});
|
|
231
281
|
// ============================================================================
|
|
232
282
|
// KV Store Tests
|
|
@@ -2503,6 +2553,19 @@ function mockErrorResponse(status, message) {
|
|
|
2503
2553
|
// URLSearchParams, so they are out of scope here.
|
|
2504
2554
|
// ============================================================================
|
|
2505
2555
|
(0, vitest_1.describe)("EkoDBClient URL path segment encoding", () => {
|
|
2556
|
+
(0, vitest_1.it)("listUserFunctions percent-encodes reserved chars in the tags query param", async () => {
|
|
2557
|
+
// A tag with query-reserved characters must be percent-encoded, not
|
|
2558
|
+
// concatenated raw into `?tags=...`. Without encoding, `a&injected=1`
|
|
2559
|
+
// splits into tags="a" plus a smuggled `injected=1` query param.
|
|
2560
|
+
const client = createTestClient();
|
|
2561
|
+
mockTokenResponse();
|
|
2562
|
+
mockJsonResponse([]);
|
|
2563
|
+
await client.listUserFunctions(["a&injected=1", "b"]);
|
|
2564
|
+
const [url] = mockFetch.mock.calls[1];
|
|
2565
|
+
const parsed = new URL(url);
|
|
2566
|
+
(0, vitest_1.expect)(parsed.searchParams.get("tags")).toBe("a&injected=1,b");
|
|
2567
|
+
(0, vitest_1.expect)(parsed.searchParams.has("injected")).toBe(false);
|
|
2568
|
+
});
|
|
2506
2569
|
(0, vitest_1.it)("findById encodes a reserved-char id (a/b -> a%2Fb)", async () => {
|
|
2507
2570
|
const client = createTestClient();
|
|
2508
2571
|
mockTokenResponse();
|
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -313,6 +313,72 @@ describe("EkoDBClient batch operations", () => {
|
|
|
313
313
|
|
|
314
314
|
expect(result.successful).toHaveLength(2);
|
|
315
315
|
});
|
|
316
|
+
|
|
317
|
+
it("batchInsert sends transaction_id as a query param", async () => {
|
|
318
|
+
const client = createTestClient();
|
|
319
|
+
|
|
320
|
+
mockTokenResponse();
|
|
321
|
+
mockJsonResponse({ successful: ["id_1"], failed: [] });
|
|
322
|
+
|
|
323
|
+
await client.batchInsert("users", [{ name: "A" }], {
|
|
324
|
+
transactionId: "tx_123",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
328
|
+
expect(init.method).toBe("POST");
|
|
329
|
+
const parsed = new URL(url as string);
|
|
330
|
+
expect(parsed.pathname).toBe("/api/batch/insert/users");
|
|
331
|
+
expect(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("batchUpdate sends transaction_id as a query param", async () => {
|
|
335
|
+
const client = createTestClient();
|
|
336
|
+
|
|
337
|
+
mockTokenResponse();
|
|
338
|
+
mockJsonResponse({ successful: ["id_1"], failed: [] });
|
|
339
|
+
|
|
340
|
+
await client.batchUpdate("users", [{ id: "id_1", data: { name: "B" } }], {
|
|
341
|
+
transactionId: "tx_123",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
345
|
+
expect(init.method).toBe("PUT");
|
|
346
|
+
const parsed = new URL(url as string);
|
|
347
|
+
expect(parsed.pathname).toBe("/api/batch/update/users");
|
|
348
|
+
expect(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("batchDelete sends transaction_id as a query param", async () => {
|
|
352
|
+
const client = createTestClient();
|
|
353
|
+
|
|
354
|
+
mockTokenResponse();
|
|
355
|
+
mockJsonResponse({ successful: ["id_1", "id_2"], failed: [] });
|
|
356
|
+
|
|
357
|
+
await client.batchDelete("users", ["id_1", "id_2"], undefined, {
|
|
358
|
+
transactionId: "tx_123",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const [url, init] = mockFetch.mock.calls[1];
|
|
362
|
+
expect(init.method).toBe("DELETE");
|
|
363
|
+
const parsed = new URL(url as string);
|
|
364
|
+
expect(parsed.pathname).toBe("/api/batch/delete/users");
|
|
365
|
+
expect(parsed.searchParams.get("transaction_id")).toBe("tx_123");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("batch ops without transaction_id send no such query param (additive)", async () => {
|
|
369
|
+
const client = createTestClient();
|
|
370
|
+
|
|
371
|
+
mockTokenResponse();
|
|
372
|
+
mockJsonResponse({ successful: ["id_1"], failed: [] });
|
|
373
|
+
|
|
374
|
+
// Legacy call sites: no options object at all.
|
|
375
|
+
await client.batchUpdate("users", [{ id: "id_1", data: { name: "B" } }]);
|
|
376
|
+
|
|
377
|
+
const [url] = mockFetch.mock.calls[1];
|
|
378
|
+
const parsed = new URL(url as string);
|
|
379
|
+
expect(parsed.searchParams.get("transaction_id")).toBeNull();
|
|
380
|
+
expect(parsed.search).toBe("");
|
|
381
|
+
});
|
|
316
382
|
});
|
|
317
383
|
|
|
318
384
|
// ============================================================================
|
|
@@ -3281,6 +3347,23 @@ describe("extractRecordId", () => {
|
|
|
3281
3347
|
// ============================================================================
|
|
3282
3348
|
|
|
3283
3349
|
describe("EkoDBClient URL path segment encoding", () => {
|
|
3350
|
+
it("listUserFunctions percent-encodes reserved chars in the tags query param", async () => {
|
|
3351
|
+
// A tag with query-reserved characters must be percent-encoded, not
|
|
3352
|
+
// concatenated raw into `?tags=...`. Without encoding, `a&injected=1`
|
|
3353
|
+
// splits into tags="a" plus a smuggled `injected=1` query param.
|
|
3354
|
+
const client = createTestClient();
|
|
3355
|
+
|
|
3356
|
+
mockTokenResponse();
|
|
3357
|
+
mockJsonResponse([]);
|
|
3358
|
+
|
|
3359
|
+
await client.listUserFunctions(["a&injected=1", "b"]);
|
|
3360
|
+
|
|
3361
|
+
const [url] = mockFetch.mock.calls[1];
|
|
3362
|
+
const parsed = new URL(url as string);
|
|
3363
|
+
expect(parsed.searchParams.get("tags")).toBe("a&injected=1,b");
|
|
3364
|
+
expect(parsed.searchParams.has("injected")).toBe(false);
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3284
3367
|
it("findById encodes a reserved-char id (a/b -> a%2Fb)", async () => {
|
|
3285
3368
|
const client = createTestClient();
|
|
3286
3369
|
|
package/src/client.ts
CHANGED
|
@@ -1051,40 +1051,77 @@ export class EkoDBClient {
|
|
|
1051
1051
|
|
|
1052
1052
|
/**
|
|
1053
1053
|
* Batch update multiple documents
|
|
1054
|
+
* @param collection - Collection name
|
|
1055
|
+
* @param updates - Array of updates ({ id, data, bypassRipple? })
|
|
1056
|
+
* @param options - Optional parameters (bypassRipple, transactionId). When
|
|
1057
|
+
* `transactionId` is set, the batch is staged into that MVCC transaction
|
|
1058
|
+
* (sent as the `transaction_id` query param) instead of committed
|
|
1059
|
+
* immediately — mirrors the single-record update.
|
|
1054
1060
|
*/
|
|
1055
1061
|
async batchUpdate(
|
|
1056
1062
|
collection: string,
|
|
1057
1063
|
updates: Array<{ id: string; data: Record; bypassRipple?: boolean }>,
|
|
1064
|
+
options?: BatchUpdateOptions,
|
|
1058
1065
|
): Promise<BatchOperationResult> {
|
|
1066
|
+
const params = new URLSearchParams();
|
|
1067
|
+
if (options?.bypassRipple !== undefined) {
|
|
1068
|
+
params.append("bypass_ripple", String(options.bypassRipple));
|
|
1069
|
+
}
|
|
1070
|
+
if (options?.transactionId) {
|
|
1071
|
+
params.append("transaction_id", options.transactionId);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1059
1074
|
const formattedUpdates = updates.map((u) => ({
|
|
1060
1075
|
id: u.id,
|
|
1061
1076
|
data: u.data,
|
|
1062
1077
|
bypass_ripple: u.bypassRipple,
|
|
1063
1078
|
}));
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
`/api/batch/update/${encodeURIComponent(collection)}
|
|
1067
|
-
|
|
1068
|
-
|
|
1079
|
+
const url = params.toString()
|
|
1080
|
+
? `/api/batch/update/${encodeURIComponent(collection)}?${params.toString()}`
|
|
1081
|
+
: `/api/batch/update/${encodeURIComponent(collection)}`;
|
|
1082
|
+
|
|
1083
|
+
return this.makeRequest<BatchOperationResult>("PUT", url, {
|
|
1084
|
+
updates: formattedUpdates,
|
|
1085
|
+
});
|
|
1069
1086
|
}
|
|
1070
1087
|
|
|
1071
1088
|
/**
|
|
1072
1089
|
* Batch delete multiple documents
|
|
1090
|
+
* @param collection - Collection name
|
|
1091
|
+
* @param ids - Document IDs to delete
|
|
1092
|
+
* @param bypassRipple - Optional flag to bypass ripple propagation (legacy
|
|
1093
|
+
* positional form, kept for back-compat)
|
|
1094
|
+
* @param options - Optional parameters (bypassRipple, transactionId). When
|
|
1095
|
+
* `transactionId` is set, the batch is staged into that MVCC transaction
|
|
1096
|
+
* (sent as the `transaction_id` query param) instead of committed
|
|
1097
|
+
* immediately — mirrors the single-record delete.
|
|
1073
1098
|
*/
|
|
1074
1099
|
async batchDelete(
|
|
1075
1100
|
collection: string,
|
|
1076
1101
|
ids: string[],
|
|
1077
1102
|
bypassRipple?: boolean,
|
|
1103
|
+
options?: BatchDeleteOptions,
|
|
1078
1104
|
): Promise<BatchOperationResult> {
|
|
1105
|
+
// bypass_ripple is sent per-item in the body (existing behavior). The
|
|
1106
|
+
// effective value prefers the explicit positional arg, falling back to the
|
|
1107
|
+
// options object so callers can use either form.
|
|
1108
|
+
const effectiveBypassRipple =
|
|
1109
|
+
bypassRipple !== undefined ? bypassRipple : options?.bypassRipple;
|
|
1110
|
+
|
|
1111
|
+
const params = new URLSearchParams();
|
|
1112
|
+
if (options?.transactionId) {
|
|
1113
|
+
params.append("transaction_id", options.transactionId);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1079
1116
|
const deletes = ids.map((id) => ({
|
|
1080
1117
|
id: id,
|
|
1081
|
-
bypass_ripple:
|
|
1118
|
+
bypass_ripple: effectiveBypassRipple,
|
|
1082
1119
|
}));
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
`/api/batch/delete/${encodeURIComponent(collection)}
|
|
1086
|
-
|
|
1087
|
-
);
|
|
1120
|
+
const url = params.toString()
|
|
1121
|
+
? `/api/batch/delete/${encodeURIComponent(collection)}?${params.toString()}`
|
|
1122
|
+
: `/api/batch/delete/${encodeURIComponent(collection)}`;
|
|
1123
|
+
|
|
1124
|
+
return this.makeRequest<BatchOperationResult>("DELETE", url, { deletes });
|
|
1088
1125
|
}
|
|
1089
1126
|
|
|
1090
1127
|
/**
|
|
@@ -2449,7 +2486,11 @@ export class EkoDBClient {
|
|
|
2449
2486
|
* List all functions, optionally filtered by tags
|
|
2450
2487
|
*/
|
|
2451
2488
|
async listFunctions(tags?: string[]): Promise<UserFunction[]> {
|
|
2452
|
-
|
|
2489
|
+
// URLSearchParams percent-encodes the value (`&`/`=`/`,`), so a tag
|
|
2490
|
+
// containing query-reserved characters can't smuggle extra params.
|
|
2491
|
+
const params = tags
|
|
2492
|
+
? `?${new URLSearchParams({ tags: tags.join(",") }).toString()}`
|
|
2493
|
+
: "";
|
|
2453
2494
|
return this.makeRequest<UserFunction[]>("GET", `/api/functions${params}`);
|
|
2454
2495
|
}
|
|
2455
2496
|
|
|
@@ -2529,7 +2570,11 @@ export class EkoDBClient {
|
|
|
2529
2570
|
* @returns Array of user functions
|
|
2530
2571
|
*/
|
|
2531
2572
|
async listUserFunctions(tags?: string[]): Promise<UserFunction[]> {
|
|
2532
|
-
|
|
2573
|
+
// URLSearchParams percent-encodes the value (`&`/`=`/`,`), so a tag
|
|
2574
|
+
// containing query-reserved characters can't smuggle extra params.
|
|
2575
|
+
const params = tags
|
|
2576
|
+
? `?${new URLSearchParams({ tags: tags.join(",") }).toString()}`
|
|
2577
|
+
: "";
|
|
2533
2578
|
return this.makeRequest<UserFunction[]>(
|
|
2534
2579
|
"GET",
|
|
2535
2580
|
`/api/functions${params}`,
|