@convex-dev/agent 0.6.3 → 0.6.4

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.
Files changed (71) hide show
  1. package/dist/UIMessages.d.ts.map +1 -1
  2. package/dist/UIMessages.js.map +1 -1
  3. package/dist/client/definePlaygroundAPI.d.ts +69 -64
  4. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  5. package/dist/client/definePlaygroundAPI.js +8 -5
  6. package/dist/client/definePlaygroundAPI.js.map +1 -1
  7. package/dist/client/index.d.ts +12 -1
  8. package/dist/client/index.d.ts.map +1 -1
  9. package/dist/client/index.js +11 -2
  10. package/dist/client/index.js.map +1 -1
  11. package/dist/client/saveInputMessages.d.ts.map +1 -1
  12. package/dist/client/saveInputMessages.js.map +1 -1
  13. package/dist/client/types.d.ts.map +1 -1
  14. package/dist/component/apiKeys.js +5 -5
  15. package/dist/component/apiKeys.js.map +1 -1
  16. package/dist/component/files.d.ts.map +1 -1
  17. package/dist/component/files.js +13 -11
  18. package/dist/component/files.js.map +1 -1
  19. package/dist/component/messages.d.ts.map +1 -1
  20. package/dist/component/messages.js +37 -27
  21. package/dist/component/messages.js.map +1 -1
  22. package/dist/component/streams.d.ts.map +1 -1
  23. package/dist/component/streams.js +22 -17
  24. package/dist/component/streams.js.map +1 -1
  25. package/dist/component/threads.js +7 -7
  26. package/dist/component/threads.js.map +1 -1
  27. package/dist/component/users.js +2 -2
  28. package/dist/component/users.js.map +1 -1
  29. package/dist/component/vector/index.d.ts.map +1 -1
  30. package/dist/component/vector/index.js +14 -8
  31. package/dist/component/vector/index.js.map +1 -1
  32. package/dist/deltas.d.ts +16 -27
  33. package/dist/deltas.d.ts.map +1 -1
  34. package/dist/deltas.js +269 -286
  35. package/dist/deltas.js.map +1 -1
  36. package/dist/mapping.d.ts +9 -3
  37. package/dist/mapping.d.ts.map +1 -1
  38. package/dist/mapping.js +16 -14
  39. package/dist/mapping.js.map +1 -1
  40. package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
  41. package/dist/react/useStreamingUIMessages.js +42 -26
  42. package/dist/react/useStreamingUIMessages.js.map +1 -1
  43. package/dist/react/useUIMessages.d.ts +1 -0
  44. package/dist/react/useUIMessages.d.ts.map +1 -1
  45. package/dist/react/useUIMessages.js +7 -3
  46. package/dist/react/useUIMessages.js.map +1 -1
  47. package/package.json +2 -1
  48. package/src/UIMessages.ts +1 -2
  49. package/src/client/approval.test.ts +25 -6
  50. package/src/client/createTool.ts +1 -1
  51. package/src/client/definePlaygroundAPI.ts +33 -17
  52. package/src/client/index.test.ts +91 -0
  53. package/src/client/index.ts +25 -1
  54. package/src/client/saveInputMessages.ts +4 -1
  55. package/src/client/streaming.integration.test.ts +39 -117
  56. package/src/client/types.ts +4 -17
  57. package/src/component/apiKeys.ts +5 -5
  58. package/src/component/files.test.ts +1 -1
  59. package/src/component/files.ts +14 -12
  60. package/src/component/messages.ts +40 -28
  61. package/src/component/streams.ts +33 -17
  62. package/src/component/threads.ts +7 -7
  63. package/src/component/users.ts +2 -2
  64. package/src/component/vector/index.ts +14 -7
  65. package/src/deltas.test.ts +373 -392
  66. package/src/deltas.ts +339 -378
  67. package/src/mapping.test.ts +296 -18
  68. package/src/mapping.ts +17 -11
  69. package/src/react/useStreamingUIMessages.ts +62 -34
  70. package/src/react/useUIMessages.test.ts +80 -1
  71. package/src/react/useUIMessages.ts +11 -3
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
2
2
  import {
3
3
  Agent,
4
4
  createThread,
5
+ createTool,
5
6
  filterOutOrphanedToolMessages,
6
7
  type MessageDoc,
7
8
  } from "./index.js";
@@ -62,6 +63,74 @@ export const createThreadManually = mutation({
62
63
  },
63
64
  });
64
65
 
66
+ const saveStepAgent = new Agent(components.agent, {
67
+ name: "save-step-test",
68
+ instructions: "test",
69
+ tools: {
70
+ echo: createTool({
71
+ description: "Echo a value",
72
+ inputSchema: z.object({ value: z.string() }),
73
+ execute: async (_ctx, input) => `echo:${input.value}`,
74
+ }),
75
+ },
76
+ languageModel: mockModel({
77
+ contentSteps: [
78
+ [
79
+ {
80
+ type: "tool-call",
81
+ toolCallId: "ss-1",
82
+ toolName: "echo",
83
+ input: JSON.stringify({ value: "hi" }),
84
+ },
85
+ ],
86
+ [{ type: "text", text: "done" }],
87
+ ],
88
+ }),
89
+ stopWhen: stepCountIs(5),
90
+ });
91
+
92
+ export const replayStepsViaSaveStep = action({
93
+ args: { withWatermark: v.boolean() },
94
+ handler: async (ctx, args) => {
95
+ const { thread } = await saveStepAgent.createThread(ctx, {
96
+ userId: "ss-gen",
97
+ });
98
+ const genResult = await thread.generateText({ prompt: "echo hi" });
99
+ const steps = genResult.steps;
100
+
101
+ const { threadId } = await saveStepAgent.createThread(ctx, {
102
+ userId: "ss-replay",
103
+ });
104
+ const { messageId: promptMessageId } = await saveStepAgent.saveMessage(ctx, {
105
+ threadId,
106
+ message: { role: "user", content: "echo hi" },
107
+ skipEmbeddings: true,
108
+ });
109
+ let previousStep: (typeof steps)[number] | undefined;
110
+ for (const step of steps) {
111
+ await saveStepAgent.saveStep(ctx, {
112
+ threadId,
113
+ promptMessageId,
114
+ step,
115
+ previousStep: args.withWatermark ? previousStep : undefined,
116
+ });
117
+ previousStep = step;
118
+ }
119
+
120
+ const replayed = await saveStepAgent.listMessages(ctx, {
121
+ threadId,
122
+ paginationOpts: { cursor: null, numItems: 50 },
123
+ statuses: ["success", "pending", "failed"],
124
+ });
125
+ const contentTypes = replayed.page.flatMap((m) =>
126
+ Array.isArray(m.message?.content)
127
+ ? m.message!.content.map((c: { type?: string }) => c.type ?? "text")
128
+ : ["text"],
129
+ );
130
+ return { stepCount: steps.length, contentTypes };
131
+ },
132
+ });
133
+
65
134
  export const createThreadMutation = agent.createThreadMutation();
66
135
  export const generateObjectAction = agent.asObjectAction({
67
136
  schema: z.object({ hello: z.string().describe("A string for testing") }),
@@ -162,6 +231,7 @@ const testApi: ApiFromModules<{
162
231
  generateTextAction: typeof generateTextAction;
163
232
  generateObjectAction: typeof generateObjectAction;
164
233
  saveMessageMutation: typeof saveMessageMutation;
234
+ replayStepsViaSaveStep: typeof replayStepsViaSaveStep;
165
235
  };
166
236
  }>["fns"] = anyApi["index.test"] as any;
167
237
 
@@ -177,6 +247,27 @@ describe("Agent thick client", () => {
177
247
  expect(result).toBeDefined();
178
248
  expect(result).toMatch(TEST_TEXT);
179
249
  });
250
+ test("saveStep with previousStep saves each step's new messages exactly once", async () => {
251
+ const t = initConvexTest(schema);
252
+ const res = await t.action(testApi.replayStepsViaSaveStep, {
253
+ withWatermark: true,
254
+ });
255
+ expect(res.stepCount).toBe(2);
256
+ const toolCalls = res.contentTypes.filter((t) => t === "tool-call").length;
257
+ const toolResults = res.contentTypes.filter(
258
+ (t) => t === "tool-result",
259
+ ).length;
260
+ expect(toolCalls).toBe(1);
261
+ expect(toolResults).toBe(1);
262
+ });
263
+ test("saveStep without previousStep duplicates prior messages", async () => {
264
+ const t = initConvexTest(schema);
265
+ const res = await t.action(testApi.replayStepsViaSaveStep, {
266
+ withWatermark: false,
267
+ });
268
+ const toolCalls = res.contentTypes.filter((t) => t === "tool-call").length;
269
+ expect(toolCalls).toBeGreaterThan(1);
270
+ });
180
271
  });
181
272
 
182
273
  describe("filterOutOrphanedToolMessages", () => {
@@ -1182,7 +1182,9 @@ export class Agent<
1182
1182
  }
1183
1183
 
1184
1184
  /**
1185
- * Explicitly save a "step" created by the AI SDK.
1185
+ * Explicitly save a "step" created by the AI SDK. For multi-step generation
1186
+ * loops, pass `previousStep` so we save only the new response messages —
1187
+ * see the arg JSDoc for why.
1186
1188
  * @param ctx The ctx argument to a mutation or action.
1187
1189
  * @param args The Step generated by the AI SDK.
1188
1190
  */
@@ -1199,6 +1201,15 @@ export class Agent<
1199
1201
  * The step to save, possibly including multiple tool calls.
1200
1202
  */
1201
1203
  step: StepResult<TOOLS>;
1204
+ /**
1205
+ * The previous step in the same generation loop, if any. Pass it so we
1206
+ * can compute how many of `step.response.messages` are already saved.
1207
+ * Omit for the first step. AI SDK v6's `step.response.messages` is
1208
+ * cumulative across steps; without this, multi-step callers duplicate
1209
+ * every prior message on every save — the exact failure mode this fix
1210
+ * addresses, just at the public-API layer.
1211
+ */
1212
+ previousStep?: StepResult<TOOLS>;
1202
1213
  /**
1203
1214
  * The model used to generate the step.
1204
1215
  * Defaults to the chat model for the Agent.
@@ -1211,6 +1222,18 @@ export class Agent<
1211
1222
  provider?: string;
1212
1223
  },
1213
1224
  ): Promise<{ messages: MessageDoc[] }> {
1225
+ const previousResponseMessageCount =
1226
+ args.previousStep?.response.messages.length ?? 0;
1227
+ if (
1228
+ args.previousStep !== undefined &&
1229
+ args.step.response.messages.length < previousResponseMessageCount
1230
+ ) {
1231
+ throw new Error(
1232
+ `saveStep: step.response.messages length (${args.step.response.messages.length}) is less than ` +
1233
+ `previousStep.response.messages length (${previousResponseMessageCount}). ` +
1234
+ `Ensure previousStep is from the immediately preceding step in the same generation loop.`,
1235
+ );
1236
+ }
1214
1237
  const { messages } = await serializeNewMessagesInStep(
1215
1238
  ctx,
1216
1239
  this.component,
@@ -1219,6 +1242,7 @@ export class Agent<
1219
1242
  provider: args.provider ?? getProviderName(this.options.languageModel),
1220
1243
  model: args.model ?? getModelName(this.options.languageModel),
1221
1244
  },
1245
+ previousResponseMessageCount,
1222
1246
  );
1223
1247
  const embeddings = await this.generateEmbeddings(
1224
1248
  ctx,
@@ -31,7 +31,10 @@ export async function saveInputMessages(
31
31
  storageOptions?: {
32
32
  saveMessages?: "all" | "promptAndOutput";
33
33
  };
34
- } & Pick<Config, "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings">,
34
+ } & Pick<
35
+ Config,
36
+ "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings"
37
+ >,
35
38
  ): Promise<{
36
39
  promptMessageId: string | undefined;
37
40
  pendingMessage: MessageDoc;
@@ -9,11 +9,7 @@ import {
9
9
  DeltaStreamer,
10
10
  mergeTransforms,
11
11
  } from "./streaming.js";
12
- import {
13
- getParts,
14
- deriveUIMessagesFromDeltas,
15
- deriveUIMessagesFromTextStreamParts,
16
- } from "../deltas.js";
12
+ import { getParts, deriveUIMessagesFromDeltas } from "../deltas.js";
17
13
  import type { TestConvex } from "convex-test";
18
14
  import type { StreamDelta, StreamMessage } from "../validators.js";
19
15
  import { dedupeMessages } from "../react/useUIMessages.js";
@@ -152,9 +148,7 @@ describe("HTTP Streaming Initiation", () => {
152
148
 
153
149
  // Verify we can reconstruct the text from deltas
154
150
  const { parts } = getParts(deltas);
155
- const textParts = parts.filter(
156
- (p: any) => p.type === "text-delta",
157
- );
151
+ const textParts = parts.filter((p: any) => p.type === "text-delta");
158
152
  expect(textParts.length).toBeGreaterThan(0);
159
153
  });
160
154
  });
@@ -265,10 +259,9 @@ describe("Stream Exclusion Logic", () => {
265
259
  await streamer2.addParts([{ type: "start" }]);
266
260
 
267
261
  // Default list: only streaming
268
- const defaultStreams = await ctx.runQuery(
269
- components.agent.streams.list,
270
- { threadId },
271
- );
262
+ const defaultStreams = await ctx.runQuery(components.agent.streams.list, {
263
+ threadId,
264
+ });
272
265
  expect(defaultStreams).toHaveLength(1);
273
266
  expect(defaultStreams[0].status).toBe("streaming");
274
267
  expect(defaultStreams[0].order).toBe(1);
@@ -325,10 +318,10 @@ describe("Stream Exclusion Logic", () => {
325
318
  expect(finishedStreams[0].status).toBe("finished");
326
319
 
327
320
  // Query for only aborted
328
- const abortedStreams = await ctx.runQuery(
329
- components.agent.streams.list,
330
- { threadId, statuses: ["aborted"] },
331
- );
321
+ const abortedStreams = await ctx.runQuery(components.agent.streams.list, {
322
+ threadId,
323
+ statuses: ["aborted"],
324
+ });
332
325
  expect(abortedStreams).toHaveLength(1);
333
326
  expect(abortedStreams[0].status).toBe("aborted");
334
327
 
@@ -487,10 +480,10 @@ describe("Delta Stream Consumption", () => {
487
480
  expect(laterParts.length).toBeLessThanOrEqual(allParts.length);
488
481
 
489
482
  // Fetching from the end cursor should yield nothing
490
- const noDeltas = await ctx.runQuery(
491
- components.agent.streams.listDeltas,
492
- { threadId, cursors: [{ cursor: endCursor, streamId }] },
493
- );
483
+ const noDeltas = await ctx.runQuery(components.agent.streams.listDeltas, {
484
+ threadId,
485
+ cursors: [{ cursor: endCursor, streamId }],
486
+ });
494
487
  expect(noDeltas).toHaveLength(0);
495
488
  });
496
489
  });
@@ -526,16 +519,13 @@ describe("Delta Stream Consumption", () => {
526
519
  const id2 = streamer2.streamId!;
527
520
 
528
521
  // Fetch deltas for both streams simultaneously
529
- const deltas = await ctx.runQuery(
530
- components.agent.streams.listDeltas,
531
- {
532
- threadId,
533
- cursors: [
534
- { cursor: 0, streamId: id1 },
535
- { cursor: 0, streamId: id2 },
536
- ],
537
- },
538
- );
522
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
523
+ threadId,
524
+ cursors: [
525
+ { cursor: 0, streamId: id1 },
526
+ { cursor: 0, streamId: id2 },
527
+ ],
528
+ });
539
529
 
540
530
  // Should have deltas for both streams
541
531
  const s1Deltas = deltas.filter((d) => d.streamId === id1);
@@ -568,10 +558,10 @@ describe("Delta Stream Consumption", () => {
568
558
  threadId,
569
559
  statuses: ["finished"],
570
560
  });
571
- const deltas = await ctx.runQuery(
572
- components.agent.streams.listDeltas,
573
- { threadId, cursors: [{ cursor: 0, streamId }] },
574
- );
561
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
562
+ threadId,
563
+ cursors: [{ cursor: 0, streamId }],
564
+ });
575
565
 
576
566
  // Derive UI messages
577
567
  const uiMessages = await deriveUIMessagesFromDeltas(
@@ -615,10 +605,10 @@ describe("Delta Stream Consumption", () => {
615
605
  await streamer.consumeStream(result.toUIMessageStream());
616
606
  const streamId = streamer.streamId!;
617
607
 
618
- const deltas = await ctx.runQuery(
619
- components.agent.streams.listDeltas,
620
- { threadId, cursors: [{ cursor: 0, streamId }] },
621
- );
608
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
609
+ threadId,
610
+ cursors: [{ cursor: 0, streamId }],
611
+ });
622
612
  const { parts } = getParts(deltas);
623
613
 
624
614
  // Compressed: all text-deltas for one text section should be merged
@@ -677,77 +667,6 @@ describe("Delta Stream Consumption", () => {
677
667
  expect((parts[0] as { type: string }).type).toBe("new");
678
668
  expect(cursor).toBe(6);
679
669
  });
680
-
681
- test("TextStreamPart format delta reconstruction with tool calls", () => {
682
- const streamId = "s1";
683
- const streamMessage: StreamMessage = {
684
- streamId,
685
- order: 1,
686
- stepOrder: 0,
687
- status: "streaming",
688
- };
689
- const deltas: StreamDelta[] = [
690
- {
691
- streamId,
692
- start: 0,
693
- end: 1,
694
- parts: [{ type: "text-delta", id: "txt-0", text: "Let me call a tool. " }],
695
- },
696
- {
697
- streamId,
698
- start: 1,
699
- end: 2,
700
- parts: [
701
- {
702
- type: "tool-call",
703
- toolCallId: "tc1",
704
- toolName: "search",
705
- input: { query: "hello" },
706
- },
707
- ],
708
- },
709
- {
710
- streamId,
711
- start: 2,
712
- end: 3,
713
- parts: [
714
- {
715
- type: "tool-result",
716
- toolCallId: "tc1",
717
- toolName: "search",
718
- output: "Found 3 results",
719
- },
720
- ],
721
- },
722
- {
723
- streamId,
724
- start: 3,
725
- end: 4,
726
- parts: [
727
- { type: "text-delta", id: "txt-1", text: "Here are the results." },
728
- ],
729
- },
730
- ];
731
-
732
- const [messages, , changed] = deriveUIMessagesFromTextStreamParts(
733
- "thread1",
734
- [streamMessage],
735
- [],
736
- deltas,
737
- );
738
-
739
- expect(messages).toHaveLength(1);
740
- expect(changed).toBe(true);
741
-
742
- const msg = messages[0];
743
- expect(msg.text).toContain("Let me call a tool.");
744
- expect(msg.text).toContain("Here are the results.");
745
-
746
- const toolParts = msg.parts.filter((p: any) =>
747
- p.type.startsWith("tool-"),
748
- );
749
- expect(toolParts.length).toBeGreaterThan(0);
750
- });
751
670
  });
752
671
 
753
672
  // ============================================================================
@@ -879,18 +798,21 @@ describe("Fallback Behavior", () => {
879
798
  order: 0,
880
799
  stepOrder: 0,
881
800
  status: "streaming",
801
+ format: "UIMessageChunk",
882
802
  };
883
803
  const finishedMsg: StreamMessage = {
884
804
  streamId: "s2",
885
805
  order: 1,
886
806
  stepOrder: 0,
887
807
  status: "finished",
808
+ format: "UIMessageChunk",
888
809
  };
889
810
  const abortedMsg: StreamMessage = {
890
811
  streamId: "s3",
891
812
  order: 2,
892
813
  stepOrder: 0,
893
814
  status: "aborted",
815
+ format: "UIMessageChunk",
894
816
  };
895
817
 
896
818
  const msgs = await deriveUIMessagesFromDeltas(
@@ -1001,10 +923,10 @@ describe("Stream Lifecycle Integration", () => {
1001
923
  expect(finished).toHaveLength(1);
1002
924
 
1003
925
  // 4. Derive UI messages from stored deltas
1004
- const deltas = await ctx.runQuery(
1005
- components.agent.streams.listDeltas,
1006
- { threadId, cursors: [{ cursor: 0, streamId }] },
1007
- );
926
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
927
+ threadId,
928
+ cursors: [{ cursor: 0, streamId }],
929
+ });
1008
930
  const uiMessages = await deriveUIMessagesFromDeltas(
1009
931
  threadId,
1010
932
  finished,
@@ -1056,10 +978,10 @@ describe("Stream Lifecycle Integration", () => {
1056
978
  expect(aborted[0].status).toBe("aborted");
1057
979
 
1058
980
  // Even aborted streams have their deltas stored
1059
- const deltas = await ctx.runQuery(
1060
- components.agent.streams.listDeltas,
1061
- { threadId, cursors: [{ cursor: 0, streamId }] },
1062
- );
981
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
982
+ threadId,
983
+ cursors: [{ cursor: 0, streamId }],
984
+ });
1063
985
  expect(deltas.length).toBeGreaterThan(0);
1064
986
  });
1065
987
  });
@@ -354,10 +354,7 @@ export type TextArgs<
354
354
  OUTPUT extends Output<any, any, any> = never,
355
355
  > = Omit<
356
356
  Parameters<
357
- typeof generateText<
358
- TOOLS extends undefined ? AgentTools : TOOLS,
359
- OUTPUT
360
- >
357
+ typeof generateText<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT>
361
358
  >[0],
362
359
  "model" | "prompt" | "messages"
363
360
  > & {
@@ -374,10 +371,7 @@ export type StreamingTextArgs<
374
371
  OUTPUT extends Output<any, any, any> = never,
375
372
  > = Omit<
376
373
  Parameters<
377
- typeof streamText<
378
- TOOLS extends undefined ? AgentTools : TOOLS,
379
- OUTPUT
380
- >
374
+ typeof streamText<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT>
381
375
  >[0],
382
376
  "model" | "prompt" | "messages"
383
377
  > & {
@@ -494,11 +488,7 @@ export interface Thread<DefaultTools extends ToolSet> {
494
488
  OUTPUT extends Output<any, any, any> = never,
495
489
  >(
496
490
  generateTextArgs: AgentPrompt &
497
- TextArgs<
498
- TOOLS extends undefined ? DefaultTools : TOOLS,
499
- TOOLS,
500
- OUTPUT
501
- >,
491
+ TextArgs<TOOLS extends undefined ? DefaultTools : TOOLS, TOOLS, OUTPUT>,
502
492
  options?: Options,
503
493
  ): Promise<
504
494
  GenerateTextResult<TOOLS extends undefined ? DefaultTools : TOOLS, OUTPUT> &
@@ -539,10 +529,7 @@ export interface Thread<DefaultTools extends ToolSet> {
539
529
  saveStreamDeltas?: boolean | StreamingOptions;
540
530
  },
541
531
  ): Promise<
542
- StreamTextResult<
543
- TOOLS extends undefined ? DefaultTools : TOOLS,
544
- OUTPUT
545
- > &
532
+ StreamTextResult<TOOLS extends undefined ? DefaultTools : TOOLS, OUTPUT> &
546
533
  ThreadOutputMetadata
547
534
  >;
548
535
  /**
@@ -13,7 +13,7 @@ export const issue = mutation({
13
13
  .first();
14
14
  if (existingApiKey) {
15
15
  console.warn(`API key ${args.name} already exists, deleting...`);
16
- await ctx.db.delete(existingApiKey._id);
16
+ await ctx.db.delete("apiKeys", existingApiKey._id);
17
17
  }
18
18
  }
19
19
  const apiKey = await ctx.db.insert("apiKeys", args);
@@ -27,7 +27,7 @@ export const validate = query({
27
27
  apiKey: v.id("apiKeys"),
28
28
  },
29
29
  handler: async (ctx, args) => {
30
- const apiKey = await ctx.db.get(args.apiKey);
30
+ const apiKey = await ctx.db.get("apiKeys", args.apiKey);
31
31
  if (!apiKey) {
32
32
  throw new Error("Invalid API key");
33
33
  }
@@ -43,14 +43,14 @@ export const destroy = mutation({
43
43
  }),
44
44
  handler: async (ctx, args) => {
45
45
  if (args.apiKey) {
46
- const apiKey = await ctx.db.get(args.apiKey);
46
+ const apiKey = await ctx.db.get("apiKeys", args.apiKey);
47
47
  if (!apiKey) {
48
48
  return "missing";
49
49
  }
50
50
  if (apiKey.name !== args.name) {
51
51
  return "name mismatch";
52
52
  }
53
- await ctx.db.delete(args.apiKey);
53
+ await ctx.db.delete("apiKeys", args.apiKey);
54
54
  } else if (args.name) {
55
55
  const apiKey = await ctx.db
56
56
  .query("apiKeys")
@@ -59,7 +59,7 @@ export const destroy = mutation({
59
59
  if (!apiKey) {
60
60
  return "missing";
61
61
  }
62
- await ctx.db.delete(apiKey._id);
62
+ await ctx.db.delete("apiKeys", apiKey._id);
63
63
  } else {
64
64
  return "must provide either apiKey or name";
65
65
  }
@@ -96,7 +96,7 @@ describe("files", () => {
96
96
  });
97
97
  // Manually set refcount to 0
98
98
  await t.run(async (ctx) => {
99
- await ctx.db.patch(fileId, { refcount: 0 });
99
+ await ctx.db.patch("files", fileId, { refcount: 0 });
100
100
  });
101
101
  files.push(fileId);
102
102
  }
@@ -29,15 +29,16 @@ export async function addFileHandler(
29
29
  ) {
30
30
  // Support both mediaType (preferred) and mimeType (deprecated)
31
31
  const mediaType = args.mediaType ?? args.mimeType;
32
-
32
+
33
33
  const existingFile = await ctx.db
34
34
  .query("files")
35
35
  .withIndex("hash", (q) => q.eq("hash", args.hash))
36
+ // eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many files with the same hash and different filenames
36
37
  .filter((q) => q.eq(q.field("filename"), args.filename))
37
38
  .first();
38
39
  if (existingFile) {
39
40
  // increment the refcount
40
- await ctx.db.patch(existingFile._id, {
41
+ await ctx.db.patch("files", existingFile._id, {
41
42
  refcount: existingFile.refcount + 1,
42
43
  lastTouchedAt: Date.now(),
43
44
  });
@@ -68,7 +69,7 @@ export const get = query({
68
69
  },
69
70
  returns: v.union(v.null(), v.doc("files")),
70
71
  handler: async (ctx, args) => {
71
- return ctx.db.get(args.fileId);
72
+ return ctx.db.get("files", args.fileId);
72
73
  },
73
74
  });
74
75
 
@@ -87,12 +88,13 @@ export const useExistingFile = mutation({
87
88
  const file = await ctx.db
88
89
  .query("files")
89
90
  .withIndex("hash", (q) => q.eq("hash", args.hash))
91
+ // eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many files with the same hash and different filenames
90
92
  .filter((q) => q.eq(q.field("filename"), args.filename))
91
93
  .first();
92
94
  if (!file) {
93
95
  return null;
94
96
  }
95
- await ctx.db.patch(file._id, {
97
+ await ctx.db.patch("files", file._id, {
96
98
  lastTouchedAt: Date.now(),
97
99
  });
98
100
  return { fileId: file._id, storageId: file.storageId };
@@ -115,9 +117,9 @@ export async function changeRefcount(
115
117
  const nextSet = new Set(next);
116
118
  for (const fileId of prevSet) {
117
119
  if (!nextSet.has(fileId)) {
118
- const file = await ctx.db.get(fileId);
120
+ const file = await ctx.db.get("files", fileId);
119
121
  if (file) {
120
- await ctx.db.patch(fileId, {
122
+ await ctx.db.patch("files", fileId, {
121
123
  refcount: file.refcount - 1,
122
124
  });
123
125
  } else {
@@ -127,9 +129,9 @@ export async function changeRefcount(
127
129
  }
128
130
  for (const fileId of nextSet) {
129
131
  if (!prevSet.has(fileId)) {
130
- const file = await ctx.db.get(fileId);
132
+ const file = await ctx.db.get("files", fileId);
131
133
  if (file) {
132
- await ctx.db.patch(fileId, {
134
+ await ctx.db.patch("files", fileId, {
133
135
  refcount: file.refcount + 1,
134
136
  });
135
137
  } else {
@@ -151,11 +153,11 @@ export async function copyFileHandler(
151
153
  ctx: MutationCtx,
152
154
  args: { fileId: Id<"files"> },
153
155
  ) {
154
- const file = await ctx.db.get(args.fileId);
156
+ const file = await ctx.db.get("files", args.fileId);
155
157
  if (!file) {
156
158
  throw new Error("File not found");
157
159
  }
158
- await ctx.db.patch(args.fileId, {
160
+ await ctx.db.patch("files", args.fileId, {
159
161
  refcount: file.refcount + 1,
160
162
  lastTouchedAt: Date.now(),
161
163
  });
@@ -195,7 +197,7 @@ export const deleteFiles = mutation({
195
197
  handler: async (ctx, args) => {
196
198
  const deletedFileIds = await Promise.all(
197
199
  args.fileIds.map(async (fileId) => {
198
- const file = await ctx.db.get(fileId);
200
+ const file = await ctx.db.get("files", fileId);
199
201
  if (!file) {
200
202
  console.error(`File ${fileId} not found when deleting, skipping...`);
201
203
  return null;
@@ -208,7 +210,7 @@ export const deleteFiles = mutation({
208
210
  return null;
209
211
  }
210
212
  }
211
- await ctx.db.delete(fileId);
213
+ await ctx.db.delete("files", fileId);
212
214
  return fileId;
213
215
  }),
214
216
  );