@ekodb/ekodb-client 0.25.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/README.md +5 -2
- package/dist/client.d.ts +79 -6
- package/dist/client.js +104 -55
- package/dist/client.test.js +227 -2
- package/dist/functions.d.ts +19 -16
- package/dist/functions.js +27 -8
- package/dist/functions.test.js +42 -0
- package/dist/index.d.ts +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/websocket.test.js +66 -0
- package/package.json +3 -3
- package/src/client.test.ts +281 -2
- package/src/client.ts +192 -32
- package/src/functions.test.ts +54 -0
- package/src/functions.ts +30 -12
- package/src/index.ts +2 -0
- package/src/search.ts +1 -1
- package/src/websocket.test.ts +90 -0
- package/tsconfig.json +2 -2
package/dist/client.test.js
CHANGED
|
@@ -1045,6 +1045,47 @@ function mockErrorResponse(status, message) {
|
|
|
1045
1045
|
(0, vitest_1.expect)(result.anthropic).toHaveLength(2);
|
|
1046
1046
|
(0, vitest_1.expect)(result.perplexity).toHaveLength(1);
|
|
1047
1047
|
});
|
|
1048
|
+
(0, vitest_1.it)("carries gemini and the per-provider status from the server", async () => {
|
|
1049
|
+
const client = createTestClient();
|
|
1050
|
+
mockTokenResponse();
|
|
1051
|
+
mockJsonResponse({
|
|
1052
|
+
openai: [],
|
|
1053
|
+
anthropic: ["claude-sonnet-4-5"],
|
|
1054
|
+
perplexity: ["sonar"],
|
|
1055
|
+
gemini: ["gemini-2.5-flash"],
|
|
1056
|
+
providers: {
|
|
1057
|
+
anthropic: { status: "ok", verified: true, model_count: 1 },
|
|
1058
|
+
gemini: { status: "ok", verified: true, model_count: 1 },
|
|
1059
|
+
openai: {
|
|
1060
|
+
status: "auth_failed",
|
|
1061
|
+
verified: true,
|
|
1062
|
+
http_status: 401,
|
|
1063
|
+
message: "Failed to fetch OpenAI models: 401 Unauthorized",
|
|
1064
|
+
},
|
|
1065
|
+
perplexity: {
|
|
1066
|
+
status: "ok",
|
|
1067
|
+
verified: false,
|
|
1068
|
+
message: "static model list; key not verified",
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
const result = await client.getChatModels();
|
|
1073
|
+
(0, vitest_1.expect)(result.gemini).toEqual(["gemini-2.5-flash"]);
|
|
1074
|
+
(0, vitest_1.expect)(result.providers?.openai.status).toBe("auth_failed");
|
|
1075
|
+
(0, vitest_1.expect)(result.providers?.openai.http_status).toBe(401);
|
|
1076
|
+
(0, vitest_1.expect)(result.providers?.openai.verified).toBe(true);
|
|
1077
|
+
(0, vitest_1.expect)(result.providers?.perplexity.verified).toBe(false);
|
|
1078
|
+
(0, vitest_1.expect)(result.providers?.anthropic.model_count).toBe(1);
|
|
1079
|
+
});
|
|
1080
|
+
(0, vitest_1.it)("tolerates a server that predates gemini and providers", async () => {
|
|
1081
|
+
const client = createTestClient();
|
|
1082
|
+
mockTokenResponse();
|
|
1083
|
+
mockJsonResponse({ openai: ["gpt-4o"], anthropic: [], perplexity: [] });
|
|
1084
|
+
const result = await client.getChatModels();
|
|
1085
|
+
(0, vitest_1.expect)(result.openai).toEqual(["gpt-4o"]);
|
|
1086
|
+
(0, vitest_1.expect)(result.gemini).toBeUndefined();
|
|
1087
|
+
(0, vitest_1.expect)(result.providers).toBeUndefined();
|
|
1088
|
+
});
|
|
1048
1089
|
(0, vitest_1.it)("gets models for specific provider", async () => {
|
|
1049
1090
|
const client = createTestClient();
|
|
1050
1091
|
mockTokenResponse();
|
|
@@ -1909,6 +1950,112 @@ function mockErrorResponse(status, message) {
|
|
|
1909
1950
|
(0, vitest_1.expect)(events[2].messageId).toBe("msg_1");
|
|
1910
1951
|
(0, vitest_1.expect)(events[2].executionTimeMs).toBe(42);
|
|
1911
1952
|
});
|
|
1953
|
+
(0, vitest_1.it)("treats a frame named error as an error even when its payload says message", async () => {
|
|
1954
|
+
const client = createTestClient();
|
|
1955
|
+
mockTokenResponse();
|
|
1956
|
+
const sseBody = 'event: token\ndata: {"token":"Hel"}\n\nevent: error\ndata: {"message":"boom"}\n\n';
|
|
1957
|
+
mockFetch.mockResolvedValueOnce({
|
|
1958
|
+
ok: true,
|
|
1959
|
+
status: 200,
|
|
1960
|
+
text: async () => sseBody,
|
|
1961
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
1962
|
+
});
|
|
1963
|
+
const events = [];
|
|
1964
|
+
const stream = client.chatMessageStream("chat_123", {
|
|
1965
|
+
message: "Hello",
|
|
1966
|
+
});
|
|
1967
|
+
stream.on("event", (evt) => events.push(evt));
|
|
1968
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1969
|
+
(0, vitest_1.expect)(events).toEqual([
|
|
1970
|
+
{ type: "chunk", content: "Hel" },
|
|
1971
|
+
{ type: "error", error: "boom" },
|
|
1972
|
+
]);
|
|
1973
|
+
});
|
|
1974
|
+
(0, vitest_1.it)("stops reading after an error frame and emits nothing that follows it", async () => {
|
|
1975
|
+
const client = createTestClient();
|
|
1976
|
+
mockTokenResponse();
|
|
1977
|
+
const sseBody = 'event: error\ndata: {"message":"boom"}\n\nevent: token\ndata: {"token":"late"}\n\n';
|
|
1978
|
+
mockFetch.mockResolvedValueOnce({
|
|
1979
|
+
ok: true,
|
|
1980
|
+
status: 200,
|
|
1981
|
+
text: async () => sseBody,
|
|
1982
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
1983
|
+
});
|
|
1984
|
+
const events = [];
|
|
1985
|
+
const stream = client.chatMessageStream("chat_123", {
|
|
1986
|
+
message: "Hello",
|
|
1987
|
+
});
|
|
1988
|
+
stream.on("event", (evt) => events.push(evt));
|
|
1989
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1990
|
+
(0, vitest_1.expect)(events).toEqual([{ type: "error", error: "boom" }]);
|
|
1991
|
+
});
|
|
1992
|
+
(0, vitest_1.it)("cancels the body reader after an error frame instead of waiting for the server to close", async () => {
|
|
1993
|
+
const client = createTestClient();
|
|
1994
|
+
mockTokenResponse();
|
|
1995
|
+
const encoder = new TextEncoder();
|
|
1996
|
+
const chunks = [
|
|
1997
|
+
encoder.encode('event: error\ndata: {"message":"boom"}\n\n'),
|
|
1998
|
+
encoder.encode('event: token\ndata: {"token":"late"}\n\n'),
|
|
1999
|
+
];
|
|
2000
|
+
const cancel = vitest_1.vi.fn(async () => { });
|
|
2001
|
+
let reads = 0;
|
|
2002
|
+
const reader = {
|
|
2003
|
+
read: vitest_1.vi.fn(async () => {
|
|
2004
|
+
// A server (or proxy) that does not close after the error frame: it
|
|
2005
|
+
// keeps sending frames. Bounded so a client that never stops fails
|
|
2006
|
+
// the assertions below instead of looping forever.
|
|
2007
|
+
if (reads >= 50)
|
|
2008
|
+
return { done: true, value: undefined };
|
|
2009
|
+
const value = chunks[Math.min(reads, chunks.length - 1)];
|
|
2010
|
+
reads += 1;
|
|
2011
|
+
return { done: false, value };
|
|
2012
|
+
}),
|
|
2013
|
+
cancel,
|
|
2014
|
+
};
|
|
2015
|
+
mockFetch.mockResolvedValueOnce({
|
|
2016
|
+
ok: true,
|
|
2017
|
+
status: 200,
|
|
2018
|
+
body: { getReader: () => reader },
|
|
2019
|
+
text: async () => "",
|
|
2020
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2021
|
+
});
|
|
2022
|
+
const events = [];
|
|
2023
|
+
const stream = client.chatMessageStream("chat_123", {
|
|
2024
|
+
message: "Hello",
|
|
2025
|
+
});
|
|
2026
|
+
stream.on("event", (evt) => events.push(evt));
|
|
2027
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2028
|
+
(0, vitest_1.expect)(events).toEqual([{ type: "error", error: "boom" }]);
|
|
2029
|
+
// One read delivered the error frame; the reader was cancelled rather
|
|
2030
|
+
// than read until the server closed.
|
|
2031
|
+
(0, vitest_1.expect)(reader.read).toHaveBeenCalledTimes(1);
|
|
2032
|
+
(0, vitest_1.expect)(cancel).toHaveBeenCalledTimes(1);
|
|
2033
|
+
});
|
|
2034
|
+
(0, vitest_1.it)("keeps the error text a string when the server sends a structured error", async () => {
|
|
2035
|
+
const client = createTestClient();
|
|
2036
|
+
mockTokenResponse();
|
|
2037
|
+
const sseBody = 'event: error\ndata: {"error":{"code":"upstream_down","status":503},"error_kind":"provider_unavailable","provider":"openai"}\n\n';
|
|
2038
|
+
mockFetch.mockResolvedValueOnce({
|
|
2039
|
+
ok: true,
|
|
2040
|
+
status: 200,
|
|
2041
|
+
text: async () => sseBody,
|
|
2042
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2043
|
+
});
|
|
2044
|
+
const events = [];
|
|
2045
|
+
const stream = client.chatMessageStream("chat_123", {
|
|
2046
|
+
message: "Hello",
|
|
2047
|
+
});
|
|
2048
|
+
stream.on("event", (evt) => events.push(evt));
|
|
2049
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2050
|
+
(0, vitest_1.expect)(events).toEqual([
|
|
2051
|
+
{
|
|
2052
|
+
type: "error",
|
|
2053
|
+
error: "Unknown error",
|
|
2054
|
+
errorKind: "provider_unavailable",
|
|
2055
|
+
provider: "openai",
|
|
2056
|
+
},
|
|
2057
|
+
]);
|
|
2058
|
+
});
|
|
1912
2059
|
(0, vitest_1.it)("emits error event on SSE error", async () => {
|
|
1913
2060
|
const client = createTestClient();
|
|
1914
2061
|
mockTokenResponse();
|
|
@@ -1928,6 +2075,36 @@ function mockErrorResponse(status, message) {
|
|
|
1928
2075
|
(0, vitest_1.expect)(events).toHaveLength(1);
|
|
1929
2076
|
(0, vitest_1.expect)(events[0]).toEqual({ type: "error", error: "LLM timeout" });
|
|
1930
2077
|
});
|
|
2078
|
+
(0, vitest_1.it)("carries the provider failure classification on an error event", async () => {
|
|
2079
|
+
// The deployment classifies a provider failure (`error_kind`, `provider`,
|
|
2080
|
+
// `provider_status`, `retry_after_secs` on the wire); the event carries
|
|
2081
|
+
// every one of them, in this shape's camelCase, so a consumer can act on
|
|
2082
|
+
// it without string-matching.
|
|
2083
|
+
const client = createTestClient();
|
|
2084
|
+
mockTokenResponse();
|
|
2085
|
+
const sseBody = 'data: {"error":"OpenAI API error: Incorrect API key provided","error_kind":"provider_auth_failed","provider":"openai","provider_status":401}\n';
|
|
2086
|
+
mockFetch.mockResolvedValueOnce({
|
|
2087
|
+
ok: true,
|
|
2088
|
+
status: 200,
|
|
2089
|
+
text: async () => sseBody,
|
|
2090
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2091
|
+
});
|
|
2092
|
+
const events = [];
|
|
2093
|
+
const stream = client.chatMessageStream("chat_123", {
|
|
2094
|
+
message: "Hello",
|
|
2095
|
+
});
|
|
2096
|
+
stream.on("event", (evt) => events.push(evt));
|
|
2097
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2098
|
+
(0, vitest_1.expect)(events).toEqual([
|
|
2099
|
+
{
|
|
2100
|
+
type: "error",
|
|
2101
|
+
error: "OpenAI API error: Incorrect API key provided",
|
|
2102
|
+
errorKind: "provider_auth_failed",
|
|
2103
|
+
provider: "openai",
|
|
2104
|
+
providerStatus: 401,
|
|
2105
|
+
},
|
|
2106
|
+
]);
|
|
2107
|
+
});
|
|
1931
2108
|
(0, vitest_1.it)("emits error event on non-200 HTTP response", async () => {
|
|
1932
2109
|
const client = createTestClient();
|
|
1933
2110
|
mockTokenResponse();
|
|
@@ -2100,6 +2277,15 @@ function mockErrorResponse(status, message) {
|
|
|
2100
2277
|
mockJsonResponse({ id: "sched_1", status: "paused" });
|
|
2101
2278
|
const result = await client.pauseSchedule("sched_1");
|
|
2102
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 });
|
|
2103
2289
|
});
|
|
2104
2290
|
(0, vitest_1.it)("resumes a schedule", async () => {
|
|
2105
2291
|
const client = createTestClient();
|
|
@@ -2107,6 +2293,12 @@ function mockErrorResponse(status, message) {
|
|
|
2107
2293
|
mockJsonResponse({ id: "sched_1", status: "active" });
|
|
2108
2294
|
const result = await client.resumeSchedule("sched_1");
|
|
2109
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 });
|
|
2110
2302
|
});
|
|
2111
2303
|
});
|
|
2112
2304
|
// ============================================================================
|
|
@@ -2124,6 +2316,14 @@ function mockErrorResponse(status, message) {
|
|
|
2124
2316
|
});
|
|
2125
2317
|
const result = await client.kvGetLinks("session:user123");
|
|
2126
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");
|
|
2127
2327
|
});
|
|
2128
2328
|
(0, vitest_1.it)("links a document to a KV key", async () => {
|
|
2129
2329
|
const client = createTestClient();
|
|
@@ -2131,6 +2331,26 @@ function mockErrorResponse(status, message) {
|
|
|
2131
2331
|
mockJsonResponse({ status: "linked" });
|
|
2132
2332
|
const result = await client.kvLink("session:user123", "users", "user_1");
|
|
2133
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
|
+
});
|
|
2134
2354
|
});
|
|
2135
2355
|
(0, vitest_1.it)("unlinks a document from a KV key", async () => {
|
|
2136
2356
|
const client = createTestClient();
|
|
@@ -2138,6 +2358,11 @@ function mockErrorResponse(status, message) {
|
|
|
2138
2358
|
mockJsonResponse({ status: "unlinked" });
|
|
2139
2359
|
const result = await client.kvUnlink("session:user123", "users", "user_1");
|
|
2140
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");
|
|
2141
2366
|
});
|
|
2142
2367
|
});
|
|
2143
2368
|
// ============================================================================
|
|
@@ -2161,7 +2386,7 @@ function mockErrorResponse(status, message) {
|
|
|
2161
2386
|
},
|
|
2162
2387
|
],
|
|
2163
2388
|
total: 2,
|
|
2164
|
-
|
|
2389
|
+
execution_time_ms: 12,
|
|
2165
2390
|
});
|
|
2166
2391
|
const result = await client.textSearch("documents", "ownership", {
|
|
2167
2392
|
limit: 10,
|
|
@@ -2183,7 +2408,7 @@ function mockErrorResponse(status, message) {
|
|
|
2183
2408
|
},
|
|
2184
2409
|
],
|
|
2185
2410
|
total: 1,
|
|
2186
|
-
|
|
2411
|
+
execution_time_ms: 25,
|
|
2187
2412
|
});
|
|
2188
2413
|
const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
|
|
2189
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/index.d.ts
CHANGED
|
@@ -13,4 +13,4 @@ export type { Schema, FieldTypeSchema, IndexConfig, CollectionMetadata, } from "
|
|
|
13
13
|
export type { JoinConfig } from "./join";
|
|
14
14
|
export type { UserFunction, ParameterDefinition, FunctionStageConfig, GroupFunctionConfig, SortFieldConfig, FunctionResult, FunctionStats, StageStats, } from "./functions";
|
|
15
15
|
export type { MutationNotification, ChatStreamEvent, ClientToolDefinition, ChatSendOptions, SubscribeOptions, } from "./client";
|
|
16
|
-
export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, CompactChatRequest, CompactChatResponse, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
|
|
16
|
+
export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, ChatProviderState, ChatProviderStatus, CompactChatRequest, CompactChatResponse, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
|
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/dist/websocket.test.js
CHANGED
|
@@ -300,6 +300,72 @@ function waitForMessage(ws) {
|
|
|
300
300
|
(0, vitest_1.expect)(stream.closed).toBe(true);
|
|
301
301
|
client.close();
|
|
302
302
|
});
|
|
303
|
+
// The WebSocket route carries the provider-failure classification like
|
|
304
|
+
// the SSE route does, in the event's camelCase shape.
|
|
305
|
+
(0, vitest_1.it)("carries the provider failure classification on a chat stream error", async () => {
|
|
306
|
+
const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token");
|
|
307
|
+
const streamPromise = client.chatSend("chat-4", "test");
|
|
308
|
+
await new Promise((r) => wss.once("connection", r));
|
|
309
|
+
const ws = getLastConnection();
|
|
310
|
+
await waitForMessage(ws);
|
|
311
|
+
const stream = await streamPromise;
|
|
312
|
+
const events = [];
|
|
313
|
+
stream.on("event", (e) => events.push(e));
|
|
314
|
+
ws.send(JSON.stringify({
|
|
315
|
+
type: "ChatStreamError",
|
|
316
|
+
payload: {
|
|
317
|
+
chat_id: "chat-4",
|
|
318
|
+
error: "OpenAI API error 429 Too Many Requests",
|
|
319
|
+
error_kind: "provider_rate_limited",
|
|
320
|
+
provider: "openai",
|
|
321
|
+
provider_status: 429,
|
|
322
|
+
retry_after_secs: 7,
|
|
323
|
+
},
|
|
324
|
+
}));
|
|
325
|
+
await new Promise((r) => stream.on("close", r));
|
|
326
|
+
(0, vitest_1.expect)(events).toEqual([
|
|
327
|
+
{
|
|
328
|
+
type: "error",
|
|
329
|
+
error: "OpenAI API error 429 Too Many Requests",
|
|
330
|
+
errorKind: "provider_rate_limited",
|
|
331
|
+
provider: "openai",
|
|
332
|
+
providerStatus: 429,
|
|
333
|
+
retryAfterSecs: 7,
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
client.close();
|
|
337
|
+
});
|
|
338
|
+
// A structured `error` value is still an error, with string text — the
|
|
339
|
+
// WebSocket route guards the shape exactly as the SSE route does.
|
|
340
|
+
(0, vitest_1.it)("keeps the error text a string when the payload's error is an object", async () => {
|
|
341
|
+
const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token");
|
|
342
|
+
const streamPromise = client.chatSend("chat-5", "test");
|
|
343
|
+
await new Promise((r) => wss.once("connection", r));
|
|
344
|
+
const ws = getLastConnection();
|
|
345
|
+
await waitForMessage(ws);
|
|
346
|
+
const stream = await streamPromise;
|
|
347
|
+
const events = [];
|
|
348
|
+
stream.on("event", (e) => events.push(e));
|
|
349
|
+
ws.send(JSON.stringify({
|
|
350
|
+
type: "ChatStreamError",
|
|
351
|
+
payload: {
|
|
352
|
+
chat_id: "chat-5",
|
|
353
|
+
error: { code: "upstream_down", status: 503 },
|
|
354
|
+
error_kind: "provider_unavailable",
|
|
355
|
+
provider: "gemini",
|
|
356
|
+
},
|
|
357
|
+
}));
|
|
358
|
+
await new Promise((r) => stream.on("close", r));
|
|
359
|
+
(0, vitest_1.expect)(events).toEqual([
|
|
360
|
+
{
|
|
361
|
+
type: "error",
|
|
362
|
+
error: "Unknown error",
|
|
363
|
+
errorKind: "provider_unavailable",
|
|
364
|
+
provider: "gemini",
|
|
365
|
+
},
|
|
366
|
+
]);
|
|
367
|
+
client.close();
|
|
368
|
+
});
|
|
303
369
|
(0, vitest_1.it)("sends options with ChatSend", async () => {
|
|
304
370
|
const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token");
|
|
305
371
|
const streamPromise = client.chatSend("chat-3", "Hello", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekodb/ekodb-client",
|
|
3
|
-
"version": "0.
|
|
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,9 +20,9 @@
|
|
|
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
|
-
"typescript": "^
|
|
25
|
+
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.0.18"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|