@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/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
|
|
package/src/index.ts
CHANGED
package/src/search.ts
CHANGED
package/src/websocket.test.ts
CHANGED
|
@@ -432,6 +432,96 @@ describe("WebSocketClient", () => {
|
|
|
432
432
|
client.close();
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
+
// The WebSocket route carries the provider-failure classification like
|
|
436
|
+
// the SSE route does, in the event's camelCase shape.
|
|
437
|
+
it("carries the provider failure classification on a chat stream error", async () => {
|
|
438
|
+
const client = new WebSocketClient(
|
|
439
|
+
`ws://localhost:${port}/api/ws`,
|
|
440
|
+
"test-token",
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const streamPromise = client.chatSend("chat-4", "test");
|
|
444
|
+
|
|
445
|
+
await new Promise((r) => wss.once("connection", r));
|
|
446
|
+
const ws = getLastConnection();
|
|
447
|
+
await waitForMessage(ws);
|
|
448
|
+
|
|
449
|
+
const stream = await streamPromise;
|
|
450
|
+
const events: any[] = [];
|
|
451
|
+
stream.on("event", (e) => events.push(e));
|
|
452
|
+
|
|
453
|
+
ws.send(
|
|
454
|
+
JSON.stringify({
|
|
455
|
+
type: "ChatStreamError",
|
|
456
|
+
payload: {
|
|
457
|
+
chat_id: "chat-4",
|
|
458
|
+
error: "OpenAI API error 429 Too Many Requests",
|
|
459
|
+
error_kind: "provider_rate_limited",
|
|
460
|
+
provider: "openai",
|
|
461
|
+
provider_status: 429,
|
|
462
|
+
retry_after_secs: 7,
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
await new Promise((r) => stream.on("close", r));
|
|
468
|
+
expect(events).toEqual([
|
|
469
|
+
{
|
|
470
|
+
type: "error",
|
|
471
|
+
error: "OpenAI API error 429 Too Many Requests",
|
|
472
|
+
errorKind: "provider_rate_limited",
|
|
473
|
+
provider: "openai",
|
|
474
|
+
providerStatus: 429,
|
|
475
|
+
retryAfterSecs: 7,
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
client.close();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// A structured `error` value is still an error, with string text — the
|
|
483
|
+
// WebSocket route guards the shape exactly as the SSE route does.
|
|
484
|
+
it("keeps the error text a string when the payload's error is an object", async () => {
|
|
485
|
+
const client = new WebSocketClient(
|
|
486
|
+
`ws://localhost:${port}/api/ws`,
|
|
487
|
+
"test-token",
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const streamPromise = client.chatSend("chat-5", "test");
|
|
491
|
+
|
|
492
|
+
await new Promise((r) => wss.once("connection", r));
|
|
493
|
+
const ws = getLastConnection();
|
|
494
|
+
await waitForMessage(ws);
|
|
495
|
+
|
|
496
|
+
const stream = await streamPromise;
|
|
497
|
+
const events: any[] = [];
|
|
498
|
+
stream.on("event", (e) => events.push(e));
|
|
499
|
+
|
|
500
|
+
ws.send(
|
|
501
|
+
JSON.stringify({
|
|
502
|
+
type: "ChatStreamError",
|
|
503
|
+
payload: {
|
|
504
|
+
chat_id: "chat-5",
|
|
505
|
+
error: { code: "upstream_down", status: 503 },
|
|
506
|
+
error_kind: "provider_unavailable",
|
|
507
|
+
provider: "gemini",
|
|
508
|
+
},
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
await new Promise((r) => stream.on("close", r));
|
|
513
|
+
expect(events).toEqual([
|
|
514
|
+
{
|
|
515
|
+
type: "error",
|
|
516
|
+
error: "Unknown error",
|
|
517
|
+
errorKind: "provider_unavailable",
|
|
518
|
+
provider: "gemini",
|
|
519
|
+
},
|
|
520
|
+
]);
|
|
521
|
+
|
|
522
|
+
client.close();
|
|
523
|
+
});
|
|
524
|
+
|
|
435
525
|
it("sends options with ChatSend", async () => {
|
|
436
526
|
const client = new WebSocketClient(
|
|
437
527
|
`ws://localhost:${port}/api/ws`,
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
|
-
"module": "
|
|
4
|
+
"module": "nodenext",
|
|
5
5
|
"lib": ["ES2020"],
|
|
6
6
|
"declaration": true,
|
|
7
7
|
"outDir": "./dist",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"esModuleInterop": true,
|
|
11
11
|
"skipLibCheck": true,
|
|
12
12
|
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"moduleResolution": "
|
|
13
|
+
"moduleResolution": "nodenext",
|
|
14
14
|
"resolveJsonModule": true,
|
|
15
15
|
"noUnusedLocals": true,
|
|
16
16
|
"noUnusedParameters": true
|