@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.
@@ -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
- filter: (filter: Record<string, any>): FunctionStageConfig => ({
746
- type: "Filter",
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
- sort: (sort: SortFieldConfig[]): FunctionStageConfig => ({
751
- type: "Sort",
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
- limit: (limit: number): FunctionStageConfig => ({
756
- type: "Limit",
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
- skip: (skip: number): FunctionStageConfig => ({
761
- type: "Skip",
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
@@ -88,6 +88,8 @@ export type {
88
88
  UpdateSessionRequest,
89
89
  MergeSessionsRequest,
90
90
  ChatModels,
91
+ ChatProviderState,
92
+ ChatProviderStatus,
91
93
  CompactChatRequest,
92
94
  CompactChatResponse,
93
95
  EmbedRequest,
package/src/search.ts CHANGED
@@ -90,7 +90,7 @@ export interface SearchResponse {
90
90
  /** Total number of results found */
91
91
  total: number;
92
92
  /** Query execution time in milliseconds */
93
- took_ms?: number;
93
+ execution_time_ms?: number;
94
94
  }
95
95
 
96
96
  /**
@@ -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": "commonjs",
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": "node",
13
+ "moduleResolution": "nodenext",
14
14
  "resolveJsonModule": true,
15
15
  "noUnusedLocals": true,
16
16
  "noUnusedParameters": true