@ekodb/ekodb-client 0.26.0 → 0.26.1
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 +32 -5
- package/dist/client.js +36 -13
- package/dist/client.test.js +50 -2
- package/dist/functions.d.ts +19 -16
- package/dist/functions.js +27 -8
- package/dist/functions.test.js +42 -0
- package/dist/search.d.ts +1 -1
- package/package.json +2 -2
- package/src/client.test.ts +62 -2
- package/src/client.ts +49 -23
- package/src/functions.test.ts +54 -0
- package/src/functions.ts +30 -12
- package/src/search.ts +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1246,9 +1246,21 @@ export declare class EkoDBClient {
|
|
|
1246
1246
|
agentsByDeployment(deploymentId: string): Promise<Record>;
|
|
1247
1247
|
/** Get documents linked to a KV key */
|
|
1248
1248
|
kvGetLinks(key: string): Promise<Record>;
|
|
1249
|
-
/**
|
|
1250
|
-
|
|
1251
|
-
|
|
1249
|
+
/**
|
|
1250
|
+
* Link a document to a KV key.
|
|
1251
|
+
*
|
|
1252
|
+
* The identifying triple goes in the path; the body carries the optional
|
|
1253
|
+
* link payload (`keys`, `field_path`, `metadata`), and an empty object means
|
|
1254
|
+
* "no extra link data".
|
|
1255
|
+
*/
|
|
1256
|
+
kvLink(key: string, collection: string, documentId: string, linkData?: {
|
|
1257
|
+
keys?: string[];
|
|
1258
|
+
field_path?: string;
|
|
1259
|
+
metadata?: {
|
|
1260
|
+
[key: string]: string;
|
|
1261
|
+
};
|
|
1262
|
+
}): Promise<Record>;
|
|
1263
|
+
/** Unlink a document from a KV key. DELETE, with the triple in the path. */
|
|
1252
1264
|
kvUnlink(key: string, collection: string, documentId: string): Promise<Record>;
|
|
1253
1265
|
/** Create a new schedule */
|
|
1254
1266
|
createSchedule(data: Record): Promise<Record>;
|
|
@@ -1260,10 +1272,25 @@ export declare class EkoDBClient {
|
|
|
1260
1272
|
updateSchedule(id: string, data: Record): Promise<Record>;
|
|
1261
1273
|
/** Delete a schedule */
|
|
1262
1274
|
deleteSchedule(id: string): Promise<void>;
|
|
1263
|
-
/**
|
|
1275
|
+
/**
|
|
1276
|
+
* Pause a schedule.
|
|
1277
|
+
*
|
|
1278
|
+
* There is no `/pause` endpoint — pausing is a partial update of the
|
|
1279
|
+
* schedule's `enabled` flag. This previously POSTed to
|
|
1280
|
+
* `/api/schedules/{id}/pause`, which has never existed and always 404'd.
|
|
1281
|
+
*/
|
|
1264
1282
|
pauseSchedule(id: string): Promise<Record>;
|
|
1265
|
-
/**
|
|
1283
|
+
/**
|
|
1284
|
+
* Resume a paused schedule. See {@link pauseSchedule} for why this is an
|
|
1285
|
+
* update rather than its own endpoint.
|
|
1286
|
+
*/
|
|
1266
1287
|
resumeSchedule(id: string): Promise<Record>;
|
|
1288
|
+
/**
|
|
1289
|
+
* Shared implementation for pause/resume: a partial update carrying only
|
|
1290
|
+
* `enabled`. The server recomputes the next execution time when `enabled`
|
|
1291
|
+
* changes, so nothing else needs sending.
|
|
1292
|
+
*/
|
|
1293
|
+
private setScheduleEnabled;
|
|
1267
1294
|
/**
|
|
1268
1295
|
* Check if a collection exists
|
|
1269
1296
|
* @param collection - Collection name to check
|
package/dist/client.js
CHANGED
|
@@ -327,11 +327,11 @@ class EkoDBClient {
|
|
|
327
327
|
// ONLY these operations support MessagePack
|
|
328
328
|
const msgpackPaths = [
|
|
329
329
|
"/api/insert/",
|
|
330
|
-
"/api/
|
|
330
|
+
"/api/batch/insert/",
|
|
331
331
|
"/api/update/",
|
|
332
|
-
"/api/
|
|
332
|
+
"/api/batch/update/",
|
|
333
333
|
"/api/delete/",
|
|
334
|
-
"/api/
|
|
334
|
+
"/api/batch/delete/",
|
|
335
335
|
];
|
|
336
336
|
// Check if path starts with any MessagePack-supported operation
|
|
337
337
|
for (const prefix of msgpackPaths) {
|
|
@@ -1910,15 +1910,21 @@ class EkoDBClient {
|
|
|
1910
1910
|
// ========================================================================
|
|
1911
1911
|
/** Get documents linked to a KV key */
|
|
1912
1912
|
async kvGetLinks(key) {
|
|
1913
|
-
return this.makeRequest("GET", `/api/kv
|
|
1913
|
+
return this.makeRequest("GET", `/api/kv/${encodeURIComponent(key)}/links`, undefined, 0, true);
|
|
1914
1914
|
}
|
|
1915
|
-
/**
|
|
1916
|
-
|
|
1917
|
-
|
|
1915
|
+
/**
|
|
1916
|
+
* Link a document to a KV key.
|
|
1917
|
+
*
|
|
1918
|
+
* The identifying triple goes in the path; the body carries the optional
|
|
1919
|
+
* link payload (`keys`, `field_path`, `metadata`), and an empty object means
|
|
1920
|
+
* "no extra link data".
|
|
1921
|
+
*/
|
|
1922
|
+
async kvLink(key, collection, documentId, linkData = {}) {
|
|
1923
|
+
return this.makeRequest("POST", `/api/kv/${encodeURIComponent(key)}/links/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}`, linkData, 0, true);
|
|
1918
1924
|
}
|
|
1919
|
-
/** Unlink a document from a KV key */
|
|
1925
|
+
/** Unlink a document from a KV key. DELETE, with the triple in the path. */
|
|
1920
1926
|
async kvUnlink(key, collection, documentId) {
|
|
1921
|
-
return this.makeRequest("
|
|
1927
|
+
return this.makeRequest("DELETE", `/api/kv/${encodeURIComponent(key)}/links/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}`, undefined, 0, true);
|
|
1922
1928
|
}
|
|
1923
1929
|
// ========================================================================
|
|
1924
1930
|
// SCHEDULE MANAGEMENT
|
|
@@ -1943,13 +1949,30 @@ class EkoDBClient {
|
|
|
1943
1949
|
async deleteSchedule(id) {
|
|
1944
1950
|
await this.makeRequest("DELETE", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
1945
1951
|
}
|
|
1946
|
-
/**
|
|
1952
|
+
/**
|
|
1953
|
+
* Pause a schedule.
|
|
1954
|
+
*
|
|
1955
|
+
* There is no `/pause` endpoint — pausing is a partial update of the
|
|
1956
|
+
* schedule's `enabled` flag. This previously POSTed to
|
|
1957
|
+
* `/api/schedules/{id}/pause`, which has never existed and always 404'd.
|
|
1958
|
+
*/
|
|
1947
1959
|
async pauseSchedule(id) {
|
|
1948
|
-
return this.
|
|
1960
|
+
return this.setScheduleEnabled(id, false);
|
|
1949
1961
|
}
|
|
1950
|
-
/**
|
|
1962
|
+
/**
|
|
1963
|
+
* Resume a paused schedule. See {@link pauseSchedule} for why this is an
|
|
1964
|
+
* update rather than its own endpoint.
|
|
1965
|
+
*/
|
|
1951
1966
|
async resumeSchedule(id) {
|
|
1952
|
-
return this.
|
|
1967
|
+
return this.setScheduleEnabled(id, true);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Shared implementation for pause/resume: a partial update carrying only
|
|
1971
|
+
* `enabled`. The server recomputes the next execution time when `enabled`
|
|
1972
|
+
* changes, so nothing else needs sending.
|
|
1973
|
+
*/
|
|
1974
|
+
async setScheduleEnabled(id, enabled) {
|
|
1975
|
+
return this.makeRequest("PUT", `/api/schedules/${encodeURIComponent(id)}`, { enabled }, 0, true);
|
|
1953
1976
|
}
|
|
1954
1977
|
// ========================================================================
|
|
1955
1978
|
// COLLECTION UTILITIES
|
package/dist/client.test.js
CHANGED
|
@@ -2277,6 +2277,15 @@ function mockErrorResponse(status, message) {
|
|
|
2277
2277
|
mockJsonResponse({ id: "sched_1", status: "paused" });
|
|
2278
2278
|
const result = await client.pauseSchedule("sched_1");
|
|
2279
2279
|
(0, vitest_1.expect)(result).toHaveProperty("status", "paused");
|
|
2280
|
+
// Assert the REQUEST. There is no /pause route; pausing is a partial
|
|
2281
|
+
// update of `enabled`. A response-only assertion passed for as long as
|
|
2282
|
+
// this method POSTed to a route that does not exist.
|
|
2283
|
+
const calls = global.fetch.mock.calls;
|
|
2284
|
+
const dataCall = calls[1]; // calls[0] is the token exchange
|
|
2285
|
+
(0, vitest_1.expect)(dataCall[0]).toContain("/api/schedules/sched_1");
|
|
2286
|
+
(0, vitest_1.expect)(dataCall[0]).not.toContain("/pause");
|
|
2287
|
+
(0, vitest_1.expect)(dataCall[1]?.method).toBe("PUT");
|
|
2288
|
+
(0, vitest_1.expect)(JSON.parse(dataCall[1]?.body)).toEqual({ enabled: false });
|
|
2280
2289
|
});
|
|
2281
2290
|
(0, vitest_1.it)("resumes a schedule", async () => {
|
|
2282
2291
|
const client = createTestClient();
|
|
@@ -2284,6 +2293,12 @@ function mockErrorResponse(status, message) {
|
|
|
2284
2293
|
mockJsonResponse({ id: "sched_1", status: "active" });
|
|
2285
2294
|
const result = await client.resumeSchedule("sched_1");
|
|
2286
2295
|
(0, vitest_1.expect)(result).toHaveProperty("status", "active");
|
|
2296
|
+
const calls = global.fetch.mock.calls;
|
|
2297
|
+
const dataCall = calls[1];
|
|
2298
|
+
(0, vitest_1.expect)(dataCall[0]).toContain("/api/schedules/sched_1");
|
|
2299
|
+
(0, vitest_1.expect)(dataCall[0]).not.toContain("/resume");
|
|
2300
|
+
(0, vitest_1.expect)(dataCall[1]?.method).toBe("PUT");
|
|
2301
|
+
(0, vitest_1.expect)(JSON.parse(dataCall[1]?.body)).toEqual({ enabled: true });
|
|
2287
2302
|
});
|
|
2288
2303
|
});
|
|
2289
2304
|
// ============================================================================
|
|
@@ -2301,6 +2316,14 @@ function mockErrorResponse(status, message) {
|
|
|
2301
2316
|
});
|
|
2302
2317
|
const result = await client.kvGetLinks("session:user123");
|
|
2303
2318
|
(0, vitest_1.expect)(result).toHaveProperty("links");
|
|
2319
|
+
// Assert the REQUEST, not just the mocked response. These three methods
|
|
2320
|
+
// shipped pointing at routes that do not exist, and every one of these
|
|
2321
|
+
// tests passed the whole time, because a mocked response says nothing
|
|
2322
|
+
// about the URL the client actually asked for.
|
|
2323
|
+
const calls = global.fetch.mock.calls;
|
|
2324
|
+
const dataCall = calls[1]; // calls[0] is the token exchange
|
|
2325
|
+
(0, vitest_1.expect)(dataCall[0]).toContain("/api/kv/session%3Auser123/links");
|
|
2326
|
+
(0, vitest_1.expect)(dataCall[1]?.method).toBe("GET");
|
|
2304
2327
|
});
|
|
2305
2328
|
(0, vitest_1.it)("links a document to a KV key", async () => {
|
|
2306
2329
|
const client = createTestClient();
|
|
@@ -2308,6 +2331,26 @@ function mockErrorResponse(status, message) {
|
|
|
2308
2331
|
mockJsonResponse({ status: "linked" });
|
|
2309
2332
|
const result = await client.kvLink("session:user123", "users", "user_1");
|
|
2310
2333
|
(0, vitest_1.expect)(result).toHaveProperty("status", "linked");
|
|
2334
|
+
const calls = global.fetch.mock.calls;
|
|
2335
|
+
const dataCall = calls[1];
|
|
2336
|
+
// The identifying triple belongs in the PATH, not the body.
|
|
2337
|
+
(0, vitest_1.expect)(dataCall[0]).toContain("/api/kv/session%3Auser123/links/users/user_1");
|
|
2338
|
+
(0, vitest_1.expect)(dataCall[1]?.method).toBe("POST");
|
|
2339
|
+
});
|
|
2340
|
+
(0, vitest_1.it)("passes optional link data in the body", async () => {
|
|
2341
|
+
const client = createTestClient();
|
|
2342
|
+
mockTokenResponse();
|
|
2343
|
+
mockJsonResponse({ status: "linked" });
|
|
2344
|
+
await client.kvLink("session:user123", "users", "user_1", {
|
|
2345
|
+
field_path: "profile.avatar",
|
|
2346
|
+
metadata: { source: "signup" },
|
|
2347
|
+
});
|
|
2348
|
+
const calls = global.fetch.mock.calls;
|
|
2349
|
+
const body = JSON.parse(calls[1][1]?.body);
|
|
2350
|
+
(0, vitest_1.expect)(body).toEqual({
|
|
2351
|
+
field_path: "profile.avatar",
|
|
2352
|
+
metadata: { source: "signup" },
|
|
2353
|
+
});
|
|
2311
2354
|
});
|
|
2312
2355
|
(0, vitest_1.it)("unlinks a document from a KV key", async () => {
|
|
2313
2356
|
const client = createTestClient();
|
|
@@ -2315,6 +2358,11 @@ function mockErrorResponse(status, message) {
|
|
|
2315
2358
|
mockJsonResponse({ status: "unlinked" });
|
|
2316
2359
|
const result = await client.kvUnlink("session:user123", "users", "user_1");
|
|
2317
2360
|
(0, vitest_1.expect)(result).toHaveProperty("status", "unlinked");
|
|
2361
|
+
const calls = global.fetch.mock.calls;
|
|
2362
|
+
const dataCall = calls[1];
|
|
2363
|
+
(0, vitest_1.expect)(dataCall[0]).toContain("/api/kv/session%3Auser123/links/users/user_1");
|
|
2364
|
+
// DELETE, not POST — the previous implementation used POST and 404'd.
|
|
2365
|
+
(0, vitest_1.expect)(dataCall[1]?.method).toBe("DELETE");
|
|
2318
2366
|
});
|
|
2319
2367
|
});
|
|
2320
2368
|
// ============================================================================
|
|
@@ -2338,7 +2386,7 @@ function mockErrorResponse(status, message) {
|
|
|
2338
2386
|
},
|
|
2339
2387
|
],
|
|
2340
2388
|
total: 2,
|
|
2341
|
-
|
|
2389
|
+
execution_time_ms: 12,
|
|
2342
2390
|
});
|
|
2343
2391
|
const result = await client.textSearch("documents", "ownership", {
|
|
2344
2392
|
limit: 10,
|
|
@@ -2360,7 +2408,7 @@ function mockErrorResponse(status, message) {
|
|
|
2360
2408
|
},
|
|
2361
2409
|
],
|
|
2362
2410
|
total: 1,
|
|
2363
|
-
|
|
2411
|
+
execution_time_ms: 25,
|
|
2364
2412
|
});
|
|
2365
2413
|
const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
|
|
2366
2414
|
const result = await client.hybridSearch("documents", "machine learning", queryVector, 5);
|
package/dist/functions.d.ts
CHANGED
|
@@ -56,18 +56,6 @@ export type FunctionStageConfig = {
|
|
|
56
56
|
} | {
|
|
57
57
|
type: "Count";
|
|
58
58
|
output_field: string;
|
|
59
|
-
} | {
|
|
60
|
-
type: "Filter";
|
|
61
|
-
filter: Record<string, any>;
|
|
62
|
-
} | {
|
|
63
|
-
type: "Sort";
|
|
64
|
-
sort: SortFieldConfig[];
|
|
65
|
-
} | {
|
|
66
|
-
type: "Limit";
|
|
67
|
-
limit: number;
|
|
68
|
-
} | {
|
|
69
|
-
type: "Skip";
|
|
70
|
-
skip: number;
|
|
71
59
|
} | {
|
|
72
60
|
type: "Insert";
|
|
73
61
|
collection: string;
|
|
@@ -581,10 +569,25 @@ export declare const Stage: {
|
|
|
581
569
|
deleteById: (collection: string, record_id: string, bypassRipple?: boolean) => FunctionStageConfig;
|
|
582
570
|
batchInsert: (collection: string, records: Record<string, any>[], bypassRipple?: boolean) => FunctionStageConfig;
|
|
583
571
|
batchDelete: (collection: string, record_ids: string[], bypassRipple?: boolean) => FunctionStageConfig;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Filter a collection.
|
|
574
|
+
*
|
|
575
|
+
* Shorthand for a `Query` stage carrying only `filter`. There is no separate
|
|
576
|
+
* `Filter` stage server-side — filtering, sorting, limiting and skipping are
|
|
577
|
+
* all fields on `Query`. This previously emitted `{ type: "Filter" }`, which
|
|
578
|
+
* the server has no variant for; because a function's stage array
|
|
579
|
+
* deserializes as a unit, one such stage rejected the ENTIRE function.
|
|
580
|
+
*
|
|
581
|
+
* Use {@link Stage.query} when you need more than one of these at once — it
|
|
582
|
+
* takes them together and produces a single stage.
|
|
583
|
+
*/
|
|
584
|
+
filter: (collection: string, filter: Record<string, any>) => FunctionStageConfig;
|
|
585
|
+
/** Sort a collection. Shorthand for a `Query` carrying only `sort`. */
|
|
586
|
+
sort: (collection: string, sort: SortFieldConfig[]) => FunctionStageConfig;
|
|
587
|
+
/** Limit a collection read. Shorthand for a `Query` carrying only `limit`. */
|
|
588
|
+
limit: (collection: string, limit: number) => FunctionStageConfig;
|
|
589
|
+
/** Skip rows of a collection read. Shorthand for a `Query` with only `skip`. */
|
|
590
|
+
skip: (collection: string, skip: number) => FunctionStageConfig;
|
|
588
591
|
httpRequest: (url: string, method?: string, headers?: Record<string, string>, body?: any) => FunctionStageConfig;
|
|
589
592
|
vectorSearch: (collection: string, query_vector: number[], limit?: number, threshold?: number) => FunctionStageConfig;
|
|
590
593
|
textSearch: (collection: string, query_text: string, options?: {
|
package/dist/functions.js
CHANGED
|
@@ -103,20 +103,39 @@ exports.Stage = {
|
|
|
103
103
|
record_ids,
|
|
104
104
|
bypass_ripple: bypassRipple,
|
|
105
105
|
}),
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Filter a collection.
|
|
108
|
+
*
|
|
109
|
+
* Shorthand for a `Query` stage carrying only `filter`. There is no separate
|
|
110
|
+
* `Filter` stage server-side — filtering, sorting, limiting and skipping are
|
|
111
|
+
* all fields on `Query`. This previously emitted `{ type: "Filter" }`, which
|
|
112
|
+
* the server has no variant for; because a function's stage array
|
|
113
|
+
* deserializes as a unit, one such stage rejected the ENTIRE function.
|
|
114
|
+
*
|
|
115
|
+
* Use {@link Stage.query} when you need more than one of these at once — it
|
|
116
|
+
* takes them together and produces a single stage.
|
|
117
|
+
*/
|
|
118
|
+
filter: (collection, filter) => ({
|
|
119
|
+
type: "Query",
|
|
120
|
+
collection,
|
|
108
121
|
filter,
|
|
109
122
|
}),
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
/** Sort a collection. Shorthand for a `Query` carrying only `sort`. */
|
|
124
|
+
sort: (collection, sort) => ({
|
|
125
|
+
type: "Query",
|
|
126
|
+
collection,
|
|
112
127
|
sort,
|
|
113
128
|
}),
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
/** Limit a collection read. Shorthand for a `Query` carrying only `limit`. */
|
|
130
|
+
limit: (collection, limit) => ({
|
|
131
|
+
type: "Query",
|
|
132
|
+
collection,
|
|
116
133
|
limit,
|
|
117
134
|
}),
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
/** Skip rows of a collection read. Shorthand for a `Query` with only `skip`. */
|
|
136
|
+
skip: (collection, skip) => ({
|
|
137
|
+
type: "Query",
|
|
138
|
+
collection,
|
|
120
139
|
skip,
|
|
121
140
|
}),
|
|
122
141
|
httpRequest: (url, method = "GET", headers, body) => ({
|
package/dist/functions.test.js
CHANGED
|
@@ -538,4 +538,46 @@ const functions_1 = require("./functions");
|
|
|
538
538
|
(0, vitest_1.expect)(wire.type).toBe(s.type);
|
|
539
539
|
}
|
|
540
540
|
});
|
|
541
|
+
(0, vitest_1.describe)("filter/sort/limit/skip emit Query stages", () => {
|
|
542
|
+
// These four used to emit { type: "Filter" | "Sort" | "Limit" | "Skip" },
|
|
543
|
+
// none of which the server has a variant for. Because a function's stage
|
|
544
|
+
// array deserializes as a unit, a single one of them rejected the ENTIRE
|
|
545
|
+
// function. They are shorthands for a Query carrying that one field.
|
|
546
|
+
(0, vitest_1.it)("filter emits a Query with only the filter set", () => {
|
|
547
|
+
const wire = JSON.parse(JSON.stringify(functions_1.Stage.filter("users", { status: "active" })));
|
|
548
|
+
(0, vitest_1.expect)(wire.type).toBe("Query");
|
|
549
|
+
(0, vitest_1.expect)(wire.collection).toBe("users");
|
|
550
|
+
(0, vitest_1.expect)(wire.filter).toEqual({ status: "active" });
|
|
551
|
+
});
|
|
552
|
+
(0, vitest_1.it)("sort emits a Query with only the sort set", () => {
|
|
553
|
+
const wire = JSON.parse(JSON.stringify(functions_1.Stage.sort("users", [{ field: "created_at", ascending: false }])));
|
|
554
|
+
(0, vitest_1.expect)(wire.type).toBe("Query");
|
|
555
|
+
(0, vitest_1.expect)(wire.collection).toBe("users");
|
|
556
|
+
(0, vitest_1.expect)(wire.sort).toEqual([{ field: "created_at", ascending: false }]);
|
|
557
|
+
});
|
|
558
|
+
(0, vitest_1.it)("limit emits a Query with only the limit set", () => {
|
|
559
|
+
const wire = JSON.parse(JSON.stringify(functions_1.Stage.limit("users", 10)));
|
|
560
|
+
(0, vitest_1.expect)(wire.type).toBe("Query");
|
|
561
|
+
(0, vitest_1.expect)(wire.collection).toBe("users");
|
|
562
|
+
(0, vitest_1.expect)(wire.limit).toBe(10);
|
|
563
|
+
});
|
|
564
|
+
(0, vitest_1.it)("skip emits a Query with only the skip set", () => {
|
|
565
|
+
const wire = JSON.parse(JSON.stringify(functions_1.Stage.skip("users", 5)));
|
|
566
|
+
(0, vitest_1.expect)(wire.type).toBe("Query");
|
|
567
|
+
(0, vitest_1.expect)(wire.collection).toBe("users");
|
|
568
|
+
(0, vitest_1.expect)(wire.skip).toBe(5);
|
|
569
|
+
});
|
|
570
|
+
(0, vitest_1.it)("never emits a stage type the server has no variant for", () => {
|
|
571
|
+
const wire = [
|
|
572
|
+
functions_1.Stage.filter("users", {}),
|
|
573
|
+
functions_1.Stage.sort("users", []),
|
|
574
|
+
functions_1.Stage.limit("users", 1),
|
|
575
|
+
functions_1.Stage.skip("users", 1),
|
|
576
|
+
].map((s) => JSON.parse(JSON.stringify(s)).type);
|
|
577
|
+
(0, vitest_1.expect)(wire).not.toContain("Filter");
|
|
578
|
+
(0, vitest_1.expect)(wire).not.toContain("Sort");
|
|
579
|
+
(0, vitest_1.expect)(wire).not.toContain("Limit");
|
|
580
|
+
(0, vitest_1.expect)(wire).not.toContain("Skip");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
541
583
|
});
|
package/dist/search.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export interface SearchResponse {
|
|
|
79
79
|
/** Total number of results found */
|
|
80
80
|
total: number;
|
|
81
81
|
/** Query execution time in milliseconds */
|
|
82
|
-
|
|
82
|
+
execution_time_ms?: number;
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Builder for constructing search queries with fluent API
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekodb/ekodb-client",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Official TypeScript/JavaScript client for ekoDB",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "ekoDB",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
23
|
+
"@types/node": "^26.4.0",
|
|
24
24
|
"@types/ws": "^8.18.1",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.0.18"
|
package/src/client.test.ts
CHANGED
|
@@ -3021,6 +3021,16 @@ describe("EkoDBClient schedules", () => {
|
|
|
3021
3021
|
|
|
3022
3022
|
const result = await client.pauseSchedule("sched_1");
|
|
3023
3023
|
expect(result).toHaveProperty("status", "paused");
|
|
3024
|
+
|
|
3025
|
+
// Assert the REQUEST. There is no /pause route; pausing is a partial
|
|
3026
|
+
// update of `enabled`. A response-only assertion passed for as long as
|
|
3027
|
+
// this method POSTed to a route that does not exist.
|
|
3028
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
3029
|
+
const dataCall = calls[1]; // calls[0] is the token exchange
|
|
3030
|
+
expect(dataCall[0]).toContain("/api/schedules/sched_1");
|
|
3031
|
+
expect(dataCall[0]).not.toContain("/pause");
|
|
3032
|
+
expect(dataCall[1]?.method).toBe("PUT");
|
|
3033
|
+
expect(JSON.parse(dataCall[1]?.body as string)).toEqual({ enabled: false });
|
|
3024
3034
|
});
|
|
3025
3035
|
|
|
3026
3036
|
it("resumes a schedule", async () => {
|
|
@@ -3030,6 +3040,13 @@ describe("EkoDBClient schedules", () => {
|
|
|
3030
3040
|
|
|
3031
3041
|
const result = await client.resumeSchedule("sched_1");
|
|
3032
3042
|
expect(result).toHaveProperty("status", "active");
|
|
3043
|
+
|
|
3044
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
3045
|
+
const dataCall = calls[1];
|
|
3046
|
+
expect(dataCall[0]).toContain("/api/schedules/sched_1");
|
|
3047
|
+
expect(dataCall[0]).not.toContain("/resume");
|
|
3048
|
+
expect(dataCall[1]?.method).toBe("PUT");
|
|
3049
|
+
expect(JSON.parse(dataCall[1]?.body as string)).toEqual({ enabled: true });
|
|
3033
3050
|
});
|
|
3034
3051
|
});
|
|
3035
3052
|
|
|
@@ -3050,6 +3067,15 @@ describe("EkoDBClient kv links", () => {
|
|
|
3050
3067
|
|
|
3051
3068
|
const result = await client.kvGetLinks("session:user123");
|
|
3052
3069
|
expect(result).toHaveProperty("links");
|
|
3070
|
+
|
|
3071
|
+
// Assert the REQUEST, not just the mocked response. These three methods
|
|
3072
|
+
// shipped pointing at routes that do not exist, and every one of these
|
|
3073
|
+
// tests passed the whole time, because a mocked response says nothing
|
|
3074
|
+
// about the URL the client actually asked for.
|
|
3075
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
3076
|
+
const dataCall = calls[1]; // calls[0] is the token exchange
|
|
3077
|
+
expect(dataCall[0]).toContain("/api/kv/session%3Auser123/links");
|
|
3078
|
+
expect(dataCall[1]?.method).toBe("GET");
|
|
3053
3079
|
});
|
|
3054
3080
|
|
|
3055
3081
|
it("links a document to a KV key", async () => {
|
|
@@ -3059,6 +3085,32 @@ describe("EkoDBClient kv links", () => {
|
|
|
3059
3085
|
|
|
3060
3086
|
const result = await client.kvLink("session:user123", "users", "user_1");
|
|
3061
3087
|
expect(result).toHaveProperty("status", "linked");
|
|
3088
|
+
|
|
3089
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
3090
|
+
const dataCall = calls[1];
|
|
3091
|
+
// The identifying triple belongs in the PATH, not the body.
|
|
3092
|
+
expect(dataCall[0]).toContain(
|
|
3093
|
+
"/api/kv/session%3Auser123/links/users/user_1",
|
|
3094
|
+
);
|
|
3095
|
+
expect(dataCall[1]?.method).toBe("POST");
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
it("passes optional link data in the body", async () => {
|
|
3099
|
+
const client = createTestClient();
|
|
3100
|
+
mockTokenResponse();
|
|
3101
|
+
mockJsonResponse({ status: "linked" });
|
|
3102
|
+
|
|
3103
|
+
await client.kvLink("session:user123", "users", "user_1", {
|
|
3104
|
+
field_path: "profile.avatar",
|
|
3105
|
+
metadata: { source: "signup" },
|
|
3106
|
+
});
|
|
3107
|
+
|
|
3108
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
3109
|
+
const body = JSON.parse(calls[1][1]?.body as string);
|
|
3110
|
+
expect(body).toEqual({
|
|
3111
|
+
field_path: "profile.avatar",
|
|
3112
|
+
metadata: { source: "signup" },
|
|
3113
|
+
});
|
|
3062
3114
|
});
|
|
3063
3115
|
|
|
3064
3116
|
it("unlinks a document from a KV key", async () => {
|
|
@@ -3068,6 +3120,14 @@ describe("EkoDBClient kv links", () => {
|
|
|
3068
3120
|
|
|
3069
3121
|
const result = await client.kvUnlink("session:user123", "users", "user_1");
|
|
3070
3122
|
expect(result).toHaveProperty("status", "unlinked");
|
|
3123
|
+
|
|
3124
|
+
const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
|
3125
|
+
const dataCall = calls[1];
|
|
3126
|
+
expect(dataCall[0]).toContain(
|
|
3127
|
+
"/api/kv/session%3Auser123/links/users/user_1",
|
|
3128
|
+
);
|
|
3129
|
+
// DELETE, not POST — the previous implementation used POST and 404'd.
|
|
3130
|
+
expect(dataCall[1]?.method).toBe("DELETE");
|
|
3071
3131
|
});
|
|
3072
3132
|
});
|
|
3073
3133
|
|
|
@@ -3093,7 +3153,7 @@ describe("EkoDBClient text and hybrid search", () => {
|
|
|
3093
3153
|
},
|
|
3094
3154
|
],
|
|
3095
3155
|
total: 2,
|
|
3096
|
-
|
|
3156
|
+
execution_time_ms: 12,
|
|
3097
3157
|
});
|
|
3098
3158
|
|
|
3099
3159
|
const result = await client.textSearch("documents", "ownership", {
|
|
@@ -3118,7 +3178,7 @@ describe("EkoDBClient text and hybrid search", () => {
|
|
|
3118
3178
|
},
|
|
3119
3179
|
],
|
|
3120
3180
|
total: 1,
|
|
3121
|
-
|
|
3181
|
+
execution_time_ms: 25,
|
|
3122
3182
|
});
|
|
3123
3183
|
|
|
3124
3184
|
const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
|
package/src/client.ts
CHANGED
|
@@ -782,11 +782,11 @@ export class EkoDBClient {
|
|
|
782
782
|
// ONLY these operations support MessagePack
|
|
783
783
|
const msgpackPaths = [
|
|
784
784
|
"/api/insert/",
|
|
785
|
-
"/api/
|
|
785
|
+
"/api/batch/insert/",
|
|
786
786
|
"/api/update/",
|
|
787
|
-
"/api/
|
|
787
|
+
"/api/batch/update/",
|
|
788
788
|
"/api/delete/",
|
|
789
|
-
"/api/
|
|
789
|
+
"/api/batch/delete/",
|
|
790
790
|
];
|
|
791
791
|
|
|
792
792
|
// Check if path starts with any MessagePack-supported operation
|
|
@@ -3231,38 +3231,49 @@ export class EkoDBClient {
|
|
|
3231
3231
|
async kvGetLinks(key: string): Promise<Record> {
|
|
3232
3232
|
return this.makeRequest<Record>(
|
|
3233
3233
|
"GET",
|
|
3234
|
-
`/api/kv
|
|
3234
|
+
`/api/kv/${encodeURIComponent(key)}/links`,
|
|
3235
3235
|
undefined,
|
|
3236
3236
|
0,
|
|
3237
3237
|
true,
|
|
3238
3238
|
);
|
|
3239
3239
|
}
|
|
3240
3240
|
|
|
3241
|
-
/**
|
|
3241
|
+
/**
|
|
3242
|
+
* Link a document to a KV key.
|
|
3243
|
+
*
|
|
3244
|
+
* The identifying triple goes in the path; the body carries the optional
|
|
3245
|
+
* link payload (`keys`, `field_path`, `metadata`), and an empty object means
|
|
3246
|
+
* "no extra link data".
|
|
3247
|
+
*/
|
|
3242
3248
|
async kvLink(
|
|
3243
3249
|
key: string,
|
|
3244
3250
|
collection: string,
|
|
3245
3251
|
documentId: string,
|
|
3252
|
+
linkData: {
|
|
3253
|
+
keys?: string[];
|
|
3254
|
+
field_path?: string;
|
|
3255
|
+
metadata?: { [key: string]: string };
|
|
3256
|
+
} = {},
|
|
3246
3257
|
): Promise<Record> {
|
|
3247
3258
|
return this.makeRequest<Record>(
|
|
3248
3259
|
"POST",
|
|
3249
|
-
`/api/kv/
|
|
3250
|
-
|
|
3260
|
+
`/api/kv/${encodeURIComponent(key)}/links/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}`,
|
|
3261
|
+
linkData,
|
|
3251
3262
|
0,
|
|
3252
3263
|
true,
|
|
3253
3264
|
);
|
|
3254
3265
|
}
|
|
3255
3266
|
|
|
3256
|
-
/** Unlink a document from a KV key */
|
|
3267
|
+
/** Unlink a document from a KV key. DELETE, with the triple in the path. */
|
|
3257
3268
|
async kvUnlink(
|
|
3258
3269
|
key: string,
|
|
3259
3270
|
collection: string,
|
|
3260
3271
|
documentId: string,
|
|
3261
3272
|
): Promise<Record> {
|
|
3262
3273
|
return this.makeRequest<Record>(
|
|
3263
|
-
"
|
|
3264
|
-
`/api/kv/
|
|
3265
|
-
|
|
3274
|
+
"DELETE",
|
|
3275
|
+
`/api/kv/${encodeURIComponent(key)}/links/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}`,
|
|
3276
|
+
undefined,
|
|
3266
3277
|
0,
|
|
3267
3278
|
true,
|
|
3268
3279
|
);
|
|
@@ -3321,23 +3332,38 @@ export class EkoDBClient {
|
|
|
3321
3332
|
);
|
|
3322
3333
|
}
|
|
3323
3334
|
|
|
3324
|
-
/**
|
|
3335
|
+
/**
|
|
3336
|
+
* Pause a schedule.
|
|
3337
|
+
*
|
|
3338
|
+
* There is no `/pause` endpoint — pausing is a partial update of the
|
|
3339
|
+
* schedule's `enabled` flag. This previously POSTed to
|
|
3340
|
+
* `/api/schedules/{id}/pause`, which has never existed and always 404'd.
|
|
3341
|
+
*/
|
|
3325
3342
|
async pauseSchedule(id: string): Promise<Record> {
|
|
3326
|
-
return this.
|
|
3327
|
-
"POST",
|
|
3328
|
-
`/api/schedules/${encodeURIComponent(id)}/pause`,
|
|
3329
|
-
undefined,
|
|
3330
|
-
0,
|
|
3331
|
-
true,
|
|
3332
|
-
);
|
|
3343
|
+
return this.setScheduleEnabled(id, false);
|
|
3333
3344
|
}
|
|
3334
3345
|
|
|
3335
|
-
/**
|
|
3346
|
+
/**
|
|
3347
|
+
* Resume a paused schedule. See {@link pauseSchedule} for why this is an
|
|
3348
|
+
* update rather than its own endpoint.
|
|
3349
|
+
*/
|
|
3336
3350
|
async resumeSchedule(id: string): Promise<Record> {
|
|
3351
|
+
return this.setScheduleEnabled(id, true);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
/**
|
|
3355
|
+
* Shared implementation for pause/resume: a partial update carrying only
|
|
3356
|
+
* `enabled`. The server recomputes the next execution time when `enabled`
|
|
3357
|
+
* changes, so nothing else needs sending.
|
|
3358
|
+
*/
|
|
3359
|
+
private async setScheduleEnabled(
|
|
3360
|
+
id: string,
|
|
3361
|
+
enabled: boolean,
|
|
3362
|
+
): Promise<Record> {
|
|
3337
3363
|
return this.makeRequest<Record>(
|
|
3338
|
-
"
|
|
3339
|
-
`/api/schedules/${encodeURIComponent(id)}
|
|
3340
|
-
|
|
3364
|
+
"PUT",
|
|
3365
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
3366
|
+
{ enabled },
|
|
3341
3367
|
0,
|
|
3342
3368
|
true,
|
|
3343
3369
|
);
|
package/src/functions.test.ts
CHANGED
|
@@ -722,4 +722,58 @@ describe("Crypto and concurrency stages", () => {
|
|
|
722
722
|
expect(wire.type).toBe(s.type);
|
|
723
723
|
}
|
|
724
724
|
});
|
|
725
|
+
|
|
726
|
+
describe("filter/sort/limit/skip emit Query stages", () => {
|
|
727
|
+
// These four used to emit { type: "Filter" | "Sort" | "Limit" | "Skip" },
|
|
728
|
+
// none of which the server has a variant for. Because a function's stage
|
|
729
|
+
// array deserializes as a unit, a single one of them rejected the ENTIRE
|
|
730
|
+
// function. They are shorthands for a Query carrying that one field.
|
|
731
|
+
|
|
732
|
+
it("filter emits a Query with only the filter set", () => {
|
|
733
|
+
const wire = JSON.parse(
|
|
734
|
+
JSON.stringify(Stage.filter("users", { status: "active" })),
|
|
735
|
+
);
|
|
736
|
+
expect(wire.type).toBe("Query");
|
|
737
|
+
expect(wire.collection).toBe("users");
|
|
738
|
+
expect(wire.filter).toEqual({ status: "active" });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("sort emits a Query with only the sort set", () => {
|
|
742
|
+
const wire = JSON.parse(
|
|
743
|
+
JSON.stringify(
|
|
744
|
+
Stage.sort("users", [{ field: "created_at", ascending: false }]),
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
expect(wire.type).toBe("Query");
|
|
748
|
+
expect(wire.collection).toBe("users");
|
|
749
|
+
expect(wire.sort).toEqual([{ field: "created_at", ascending: false }]);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it("limit emits a Query with only the limit set", () => {
|
|
753
|
+
const wire = JSON.parse(JSON.stringify(Stage.limit("users", 10)));
|
|
754
|
+
expect(wire.type).toBe("Query");
|
|
755
|
+
expect(wire.collection).toBe("users");
|
|
756
|
+
expect(wire.limit).toBe(10);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("skip emits a Query with only the skip set", () => {
|
|
760
|
+
const wire = JSON.parse(JSON.stringify(Stage.skip("users", 5)));
|
|
761
|
+
expect(wire.type).toBe("Query");
|
|
762
|
+
expect(wire.collection).toBe("users");
|
|
763
|
+
expect(wire.skip).toBe(5);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("never emits a stage type the server has no variant for", () => {
|
|
767
|
+
const wire = [
|
|
768
|
+
Stage.filter("users", {}),
|
|
769
|
+
Stage.sort("users", []),
|
|
770
|
+
Stage.limit("users", 1),
|
|
771
|
+
Stage.skip("users", 1),
|
|
772
|
+
].map((s) => JSON.parse(JSON.stringify(s)).type);
|
|
773
|
+
expect(wire).not.toContain("Filter");
|
|
774
|
+
expect(wire).not.toContain("Sort");
|
|
775
|
+
expect(wire).not.toContain("Limit");
|
|
776
|
+
expect(wire).not.toContain("Skip");
|
|
777
|
+
});
|
|
778
|
+
});
|
|
725
779
|
});
|
package/src/functions.ts
CHANGED
|
@@ -55,10 +55,6 @@ export type FunctionStageConfig =
|
|
|
55
55
|
functions: GroupFunctionConfig[];
|
|
56
56
|
}
|
|
57
57
|
| { type: "Count"; output_field: string }
|
|
58
|
-
| { type: "Filter"; filter: Record<string, any> }
|
|
59
|
-
| { type: "Sort"; sort: SortFieldConfig[] }
|
|
60
|
-
| { type: "Limit"; limit: number }
|
|
61
|
-
| { type: "Skip"; skip: number }
|
|
62
58
|
| {
|
|
63
59
|
type: "Insert";
|
|
64
60
|
collection: string;
|
|
@@ -742,23 +738,45 @@ export const Stage = {
|
|
|
742
738
|
bypass_ripple: bypassRipple,
|
|
743
739
|
}),
|
|
744
740
|
|
|
745
|
-
|
|
746
|
-
|
|
741
|
+
/**
|
|
742
|
+
* Filter a collection.
|
|
743
|
+
*
|
|
744
|
+
* Shorthand for a `Query` stage carrying only `filter`. There is no separate
|
|
745
|
+
* `Filter` stage server-side — filtering, sorting, limiting and skipping are
|
|
746
|
+
* all fields on `Query`. This previously emitted `{ type: "Filter" }`, which
|
|
747
|
+
* the server has no variant for; because a function's stage array
|
|
748
|
+
* deserializes as a unit, one such stage rejected the ENTIRE function.
|
|
749
|
+
*
|
|
750
|
+
* Use {@link Stage.query} when you need more than one of these at once — it
|
|
751
|
+
* takes them together and produces a single stage.
|
|
752
|
+
*/
|
|
753
|
+
filter: (
|
|
754
|
+
collection: string,
|
|
755
|
+
filter: Record<string, any>,
|
|
756
|
+
): FunctionStageConfig => ({
|
|
757
|
+
type: "Query",
|
|
758
|
+
collection,
|
|
747
759
|
filter,
|
|
748
760
|
}),
|
|
749
761
|
|
|
750
|
-
|
|
751
|
-
|
|
762
|
+
/** Sort a collection. Shorthand for a `Query` carrying only `sort`. */
|
|
763
|
+
sort: (collection: string, sort: SortFieldConfig[]): FunctionStageConfig => ({
|
|
764
|
+
type: "Query",
|
|
765
|
+
collection,
|
|
752
766
|
sort,
|
|
753
767
|
}),
|
|
754
768
|
|
|
755
|
-
|
|
756
|
-
|
|
769
|
+
/** Limit a collection read. Shorthand for a `Query` carrying only `limit`. */
|
|
770
|
+
limit: (collection: string, limit: number): FunctionStageConfig => ({
|
|
771
|
+
type: "Query",
|
|
772
|
+
collection,
|
|
757
773
|
limit,
|
|
758
774
|
}),
|
|
759
775
|
|
|
760
|
-
|
|
761
|
-
|
|
776
|
+
/** Skip rows of a collection read. Shorthand for a `Query` with only `skip`. */
|
|
777
|
+
skip: (collection: string, skip: number): FunctionStageConfig => ({
|
|
778
|
+
type: "Query",
|
|
779
|
+
collection,
|
|
762
780
|
skip,
|
|
763
781
|
}),
|
|
764
782
|
|