fluent-convex 0.4.3 → 0.5.2

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.
@@ -1,8 +1,15 @@
1
1
  import { describe, it, assertType } from "vitest";
2
2
  import { v } from "convex/values";
3
3
  import { z } from "zod";
4
- import { defineSchema, defineTable } from "convex/server";
4
+ import {
5
+ defineSchema,
6
+ defineTable,
7
+ type RegisteredQuery,
8
+ type RegisteredMutation,
9
+ } from "convex/server";
5
10
  import { createBuilder } from "./builder";
11
+ import { input, returns } from "./decorators";
12
+ import type { QueryCtx, MutationCtx } from "./types";
6
13
 
7
14
  const schema = defineSchema({
8
15
  numbers: defineTable({
@@ -21,7 +28,8 @@ describe("ConvexBuilder Type Tests", () => {
21
28
  .handler(async ({ input }) => {
22
29
  assertType<{ count: number }>(input);
23
30
  return { success: true };
24
- });
31
+ })
32
+ .public();
25
33
  });
26
34
 
27
35
  it("should accept Convex v.object() validators", () => {
@@ -31,7 +39,8 @@ describe("ConvexBuilder Type Tests", () => {
31
39
  .handler(async ({ input }) => {
32
40
  assertType<{ count: number }>(input);
33
41
  return { success: true };
34
- });
42
+ })
43
+ .public();
35
44
  });
36
45
 
37
46
  it("should accept Zod object schemas", () => {
@@ -41,7 +50,8 @@ describe("ConvexBuilder Type Tests", () => {
41
50
  .handler(async ({ input }) => {
42
51
  assertType<{ count: number }>(input);
43
52
  return { success: true };
44
- });
53
+ })
54
+ .public();
45
55
  });
46
56
 
47
57
  it("should reject Zod primitive schemas (not objects)", () => {
@@ -61,7 +71,8 @@ describe("ConvexBuilder Type Tests", () => {
61
71
  .handler(async ({ input }) => {
62
72
  assertType<{ count: number }>(input);
63
73
  return { success: true };
64
- });
74
+ })
75
+ .public();
65
76
  });
66
77
  });
67
78
 
@@ -73,7 +84,8 @@ describe("ConvexBuilder Type Tests", () => {
73
84
  .returns(v.object({ numbers: v.array(v.number()) }))
74
85
  .handler(async () => {
75
86
  return { numbers: [1, 2, 3] };
76
- });
87
+ })
88
+ .public();
77
89
  });
78
90
 
79
91
  it("should accept Zod return validators", () => {
@@ -83,7 +95,8 @@ describe("ConvexBuilder Type Tests", () => {
83
95
  .returns(z.object({ numbers: z.array(z.number()) }))
84
96
  .handler(async () => {
85
97
  return { numbers: [1, 2, 3] };
86
- });
98
+ })
99
+ .public();
87
100
  });
88
101
 
89
102
  it("should accept Zod primitive return types", () => {
@@ -93,7 +106,205 @@ describe("ConvexBuilder Type Tests", () => {
93
106
  .returns(z.number())
94
107
  .handler(async () => {
95
108
  return 42;
96
- });
109
+ })
110
+ .public();
111
+ });
112
+
113
+ it("should enforce return type when .returns() is specified with Convex validator", () => {
114
+ convex
115
+ .query()
116
+ .input({ count: v.number() })
117
+ .returns(v.object({ numbers: v.array(v.number()) }))
118
+ .handler(async () => {
119
+ // This should work - correct return type
120
+ return { numbers: [1, 2, 3] };
121
+ })
122
+ .public();
123
+ });
124
+
125
+ it("should reject incorrect return type when .returns() is specified with Convex validator", () => {
126
+ convex
127
+ .query()
128
+ .input({ count: v.number() })
129
+ .returns(v.object({ numbers: v.array(v.number()) }))
130
+ // @ts-expect-error Return type mismatch: handler returns { count: number } but .returns() expects { numbers: number[] }
131
+ .handler(async () => {
132
+ return { count: 5 };
133
+ })
134
+ .public();
135
+ });
136
+
137
+ it("should reject return type with missing required property", () => {
138
+ convex
139
+ .query()
140
+ .input({ count: v.number() })
141
+ .returns(v.object({ numbers: v.array(v.number()), total: v.number() }))
142
+ // @ts-expect-error Return type mismatch: missing 'total' property
143
+ .handler(async () => {
144
+ return { numbers: [1, 2, 3] };
145
+ })
146
+ .public();
147
+ });
148
+
149
+ it("should reject return type with wrong property type", () => {
150
+ convex
151
+ .query()
152
+ .input({ count: v.number() })
153
+ .returns(v.object({ numbers: v.array(v.number()) }))
154
+ // @ts-expect-error Return type mismatch: 'numbers' should be number[] but is string[]
155
+ .handler(async () => {
156
+ return { numbers: ["1", "2", "3"] };
157
+ })
158
+ .public();
159
+ });
160
+
161
+ it("should enforce return type with Zod validator", () => {
162
+ convex
163
+ .query()
164
+ .input({ count: v.number() })
165
+ .returns(z.object({ numbers: z.array(z.number()) }))
166
+ .handler(async () => {
167
+ // This should work - correct return type
168
+ return { numbers: [1, 2, 3] };
169
+ })
170
+ .public();
171
+ });
172
+
173
+ it("should reject incorrect return type with Zod validator", () => {
174
+ convex
175
+ .query()
176
+ .input({ count: v.number() })
177
+ .returns(z.object({ numbers: z.array(z.number()) }))
178
+ // @ts-expect-error Return type mismatch: handler returns { count: number } but .returns() expects { numbers: number[] }
179
+ .handler(async () => {
180
+ return { count: 5 };
181
+ })
182
+ .public();
183
+ });
184
+
185
+ it("should enforce primitive return types", () => {
186
+ convex
187
+ .query()
188
+ .input({ count: v.number() })
189
+ .returns(z.string())
190
+ .handler(async () => {
191
+ // This should work - correct return type
192
+ return "hello";
193
+ })
194
+ .public();
195
+ });
196
+
197
+ it("should reject wrong primitive return type", () => {
198
+ convex
199
+ .query()
200
+ .input({ count: v.number() })
201
+ .returns(z.string())
202
+ // @ts-expect-error Return type mismatch: handler returns number but .returns() expects string
203
+ .handler(async () => {
204
+ return 42;
205
+ })
206
+ .public();
207
+ });
208
+
209
+ it("should enforce array return types", () => {
210
+ convex
211
+ .query()
212
+ .input({ count: v.number() })
213
+ .returns(v.array(v.number()))
214
+ .handler(async () => {
215
+ // This should work - correct return type
216
+ return [1, 2, 3];
217
+ })
218
+ .public();
219
+ });
220
+
221
+ it("should reject wrong array return type", () => {
222
+ convex
223
+ .query()
224
+ .input({ count: v.number() })
225
+ .returns(v.array(v.number()))
226
+ // @ts-expect-error Return type mismatch: handler returns string[] but .returns() expects number[]
227
+ .handler(async () => {
228
+ return ["1", "2", "3"];
229
+ })
230
+ .public();
231
+ });
232
+
233
+ it("should allow any return type when .returns() is not specified", () => {
234
+ convex
235
+ .query()
236
+ .input({ count: v.number() })
237
+ .handler(async () => {
238
+ // Should allow any return type
239
+ return { numbers: [1, 2, 3] };
240
+ })
241
+ .public();
242
+
243
+ convex
244
+ .query()
245
+ .input({ count: v.number() })
246
+ .handler(async () => {
247
+ // Should allow different return types
248
+ return { count: 5 };
249
+ })
250
+ .public();
251
+
252
+ convex
253
+ .query()
254
+ .input({ count: v.number() })
255
+ .handler(async () => {
256
+ // Should allow primitive return types
257
+ return 42;
258
+ })
259
+ .public();
260
+ });
261
+
262
+ it("should enforce return type for mutations", () => {
263
+ convex
264
+ .mutation()
265
+ .input({ value: v.number() })
266
+ .returns(v.id("numbers"))
267
+ .handler(async ({ context, input }) => {
268
+ // This should work - correct return type
269
+ return await context.db.insert("numbers", { value: input.value });
270
+ })
271
+ .public();
272
+ });
273
+
274
+ it("should reject incorrect return type for mutations", () => {
275
+ convex
276
+ .mutation()
277
+ .input({ value: v.number() })
278
+ .returns(v.id("numbers"))
279
+ // @ts-expect-error Return type mismatch: handler returns string but .returns() expects Id<"numbers">
280
+ .handler(async () => {
281
+ return "wrong-type";
282
+ })
283
+ .public();
284
+ });
285
+
286
+ it("should enforce return type for actions", () => {
287
+ convex
288
+ .action()
289
+ .input({ count: v.number() })
290
+ .returns(v.array(v.number()))
291
+ .handler(async () => {
292
+ // This should work - correct return type
293
+ return [1, 2, 3];
294
+ })
295
+ .public();
296
+ });
297
+
298
+ it("should reject incorrect return type for actions", () => {
299
+ convex
300
+ .action()
301
+ .input({ count: v.number() })
302
+ .returns(v.array(v.number()))
303
+ // @ts-expect-error Return type mismatch: handler returns string but .returns() expects number[]
304
+ .handler(async () => {
305
+ return "wrong";
306
+ })
307
+ .public();
97
308
  });
98
309
  });
99
310
 
@@ -123,7 +334,8 @@ describe("ConvexBuilder Type Tests", () => {
123
334
  assertType(context.auth);
124
335
 
125
336
  return { userId: context.user.id };
126
- });
337
+ })
338
+ .public();
127
339
  });
128
340
 
129
341
  it("should chain multiple middleware", () => {
@@ -160,7 +372,8 @@ describe("ConvexBuilder Type Tests", () => {
160
372
  assertType<string>(context.requestId);
161
373
 
162
374
  return { success: true };
163
- });
375
+ })
376
+ .public();
164
377
  });
165
378
 
166
379
  it("should work with mutations", () => {
@@ -184,7 +397,8 @@ describe("ConvexBuilder Type Tests", () => {
184
397
  assertType(context.db);
185
398
  assertType<string>(input.name);
186
399
  return null;
187
- });
400
+ })
401
+ .public();
188
402
  });
189
403
 
190
404
  it("should work with actions", () => {
@@ -207,7 +421,8 @@ describe("ConvexBuilder Type Tests", () => {
207
421
  assertType<string>(context.token);
208
422
  assertType<string>(input.url);
209
423
  return { success: true };
210
- });
424
+ })
425
+ .public();
211
426
  });
212
427
 
213
428
  it("should respect middleware application order", () => {
@@ -239,7 +454,8 @@ describe("ConvexBuilder Type Tests", () => {
239
454
  assertType<string>(context.first);
240
455
  assertType<string>(context.second);
241
456
  return { success: true };
242
- });
457
+ })
458
+ .public();
243
459
  });
244
460
  });
245
461
 
@@ -251,7 +467,8 @@ describe("ConvexBuilder Type Tests", () => {
251
467
  .handler(async ({ input }) => {
252
468
  assertType<string>(input.id);
253
469
  return { id: input.id };
254
- });
470
+ })
471
+ .public();
255
472
  });
256
473
 
257
474
  it("should create mutations", () => {
@@ -261,7 +478,8 @@ describe("ConvexBuilder Type Tests", () => {
261
478
  .handler(async ({ input }) => {
262
479
  assertType<string>(input.name);
263
480
  return { name: input.name };
264
- });
481
+ })
482
+ .public();
265
483
  });
266
484
 
267
485
  it("should create actions", () => {
@@ -271,50 +489,54 @@ describe("ConvexBuilder Type Tests", () => {
271
489
  .handler(async ({ input }) => {
272
490
  assertType<string>(input.url);
273
491
  return { url: input.url };
274
- });
492
+ })
493
+ .public();
275
494
  });
276
495
 
277
496
  it("should create internal queries", () => {
278
497
  convex
279
498
  .query()
280
- .internal()
281
499
  .input({ id: v.string() })
282
500
  .handler(async ({ input }) => {
283
501
  assertType<string>(input.id);
284
502
  return { id: input.id };
285
- });
503
+ })
504
+ .internal();
286
505
  });
287
506
 
288
507
  it("should create internal mutations", () => {
289
508
  convex
290
509
  .mutation()
291
- .internal()
292
510
  .input({ value: v.number() })
293
511
  .handler(async ({ context, input }) => {
294
512
  assertType(context.db);
295
513
  assertType<number>(input.value);
296
514
  return { value: input.value };
297
- });
515
+ })
516
+ .internal();
298
517
  });
299
518
 
300
519
  it("should create internal actions", () => {
301
520
  convex
302
521
  .action()
303
- .internal()
304
522
  .input({ url: v.string() })
305
523
  .handler(async ({ input }) => {
306
524
  assertType<string>(input.url);
307
525
  return { url: input.url };
308
- });
526
+ })
527
+ .internal();
309
528
  });
310
529
  });
311
530
 
312
531
  describe("optional input", () => {
313
532
  it("should work without input validation", () => {
314
- convex.query().handler(async ({ input }) => {
315
- assertType<Record<never, never>>(input);
316
- return { success: true };
317
- });
533
+ convex
534
+ .query()
535
+ .handler(async ({ input }) => {
536
+ assertType<Record<never, never>>(input);
537
+ return { success: true };
538
+ })
539
+ .public();
318
540
  });
319
541
 
320
542
  it("should infer optional fields as T | undefined", () => {
@@ -330,7 +552,8 @@ describe("ConvexBuilder Type Tests", () => {
330
552
  assertType<string | undefined>(input.name);
331
553
  assertType<number | undefined>(input.value);
332
554
  return null;
333
- });
555
+ })
556
+ .public();
334
557
  });
335
558
 
336
559
  it("should handle many optional fields with complex types", () => {
@@ -357,7 +580,8 @@ describe("ConvexBuilder Type Tests", () => {
357
580
  assertType<string | null | undefined>(input.status);
358
581
  assertType<Array<{ x: number; y: number }> | undefined>(input.items);
359
582
  return null;
360
- });
583
+ })
584
+ .public();
361
585
  });
362
586
 
363
587
  it("should allow all fields to be optional", () => {
@@ -373,7 +597,8 @@ describe("ConvexBuilder Type Tests", () => {
373
597
  assertType<number | undefined>(input.minValue);
374
598
  assertType<number | undefined>(input.maxValue);
375
599
  return [];
376
- });
600
+ })
601
+ .public();
377
602
  });
378
603
 
379
604
  it("should make all properties required but allow undefined for optional fields", () => {
@@ -413,7 +638,8 @@ describe("ConvexBuilder Type Tests", () => {
413
638
  };
414
639
 
415
640
  return { id: input.id };
416
- });
641
+ })
642
+ .public();
417
643
  });
418
644
 
419
645
  it("should work with Partial for truly optional calling patterns", () => {
@@ -448,7 +674,8 @@ describe("ConvexBuilder Type Tests", () => {
448
674
  };
449
675
 
450
676
  return null;
451
- });
677
+ })
678
+ .public();
452
679
  });
453
680
 
454
681
  it("should allow empty object when all fields are optional", () => {
@@ -476,7 +703,8 @@ describe("ConvexBuilder Type Tests", () => {
476
703
  };
477
704
 
478
705
  return [];
479
- });
706
+ })
707
+ .public();
480
708
  });
481
709
  });
482
710
 
@@ -499,7 +727,8 @@ describe("ConvexBuilder Type Tests", () => {
499
727
  assertType<string[]>(input.tags);
500
728
 
501
729
  return { success: true };
502
- });
730
+ })
731
+ .public();
503
732
  });
504
733
 
505
734
  it("should handle optional fields", () => {
@@ -511,7 +740,8 @@ describe("ConvexBuilder Type Tests", () => {
511
740
  assertType<number | undefined>(input.age);
512
741
 
513
742
  return { success: true };
514
- });
743
+ })
744
+ .public();
515
745
  });
516
746
 
517
747
  it("should handle v.union()", () => {
@@ -523,7 +753,8 @@ describe("ConvexBuilder Type Tests", () => {
523
753
  .handler(async ({ input }) => {
524
754
  assertType<string | number>(input.value);
525
755
  return { success: true };
526
- });
756
+ })
757
+ .public();
527
758
  });
528
759
 
529
760
  it("should handle v.array()", () => {
@@ -535,7 +766,8 @@ describe("ConvexBuilder Type Tests", () => {
535
766
  .handler(async ({ input }) => {
536
767
  assertType<string[]>(input.tags);
537
768
  return { success: true };
538
- });
769
+ })
770
+ .public();
539
771
  });
540
772
 
541
773
  it("should handle nested v.object()", () => {
@@ -550,7 +782,8 @@ describe("ConvexBuilder Type Tests", () => {
550
782
  .handler(async ({ input }) => {
551
783
  assertType<{ name: string; age: number }>(input.user);
552
784
  return { success: true };
553
- });
785
+ })
786
+ .public();
554
787
  });
555
788
 
556
789
  it("should handle v.literal()", () => {
@@ -562,7 +795,8 @@ describe("ConvexBuilder Type Tests", () => {
562
795
  .handler(async ({ input }) => {
563
796
  assertType<"user">(input.kind);
564
797
  return { success: true };
565
- });
798
+ })
799
+ .public();
566
800
  });
567
801
 
568
802
  it("should handle mix of optional and required fields", () => {
@@ -578,7 +812,8 @@ describe("ConvexBuilder Type Tests", () => {
578
812
  assertType<string | undefined>(input.optional);
579
813
  assertType<number | undefined>(input.defaulted);
580
814
  return { success: true };
581
- });
815
+ })
816
+ .public();
582
817
  });
583
818
  });
584
819
 
@@ -591,7 +826,8 @@ describe("ConvexBuilder Type Tests", () => {
591
826
  assertType(context.db);
592
827
  assertType(context.auth);
593
828
  return { success: true };
594
- });
829
+ })
830
+ .public();
595
831
  });
596
832
 
597
833
  it("mutations should have db and auth", () => {
@@ -602,7 +838,8 @@ describe("ConvexBuilder Type Tests", () => {
602
838
  assertType(context.db);
603
839
  assertType(context.auth);
604
840
  return { success: true };
605
- });
841
+ })
842
+ .public();
606
843
  });
607
844
 
608
845
  it("actions should have auth and scheduler", () => {
@@ -613,31 +850,497 @@ describe("ConvexBuilder Type Tests", () => {
613
850
  assertType(context.auth);
614
851
  assertType(context.scheduler);
615
852
  return { success: true };
853
+ })
854
+ .public();
855
+ });
856
+ });
857
+
858
+ describe("function kind selection", () => {
859
+ it("should prevent calling .input() before selecting function kind", () => {
860
+ const builder = convex;
861
+
862
+ // @ts-expect-error - ConvexBuilder does not have an input method. Call .query(), .mutation(), or .action() first.
863
+ builder.input({ id: v.string() });
864
+ });
865
+
866
+ it("should prevent calling .handler() before selecting function kind", () => {
867
+ const builder = convex;
868
+
869
+ // @ts-expect-error - ConvexBuilder does not have a handler method. Call .query(), .mutation(), or .action() first.
870
+ builder.handler(async () => ({ success: true }));
871
+ });
872
+
873
+ it("should prevent calling .use() before selecting function kind", () => {
874
+ const builder = convex;
875
+ const authMiddleware = convex
876
+ .query()
877
+ .middleware(async ({ context, next }) => {
878
+ return next({ context });
616
879
  });
880
+
881
+ // @ts-expect-error - ConvexBuilder does not have a use method. Call .query(), .mutation(), or .action() first.
882
+ builder.use(authMiddleware);
883
+ });
884
+
885
+ it("should allow .query() to be called first", () => {
886
+ const builder = convex.query();
887
+ assertType<typeof builder>(builder);
888
+ });
889
+
890
+ it("should allow .mutation() to be called first", () => {
891
+ const builder = convex.mutation();
892
+ assertType<typeof builder>(builder);
893
+ });
894
+
895
+ it("should allow .action() to be called first", () => {
896
+ const builder = convex.action();
897
+ assertType<typeof builder>(builder);
617
898
  });
618
899
  });
619
900
 
620
901
  describe("order of operations", () => {
621
- it("should allow .internal() before function type", () => {
902
+ it("should prevent calling .public() before .handler()", () => {
903
+ const builder = convex.query().input({ id: v.string() });
904
+
905
+ // @ts-expect-error - ConvexBuilderWithFunctionKind does not have a public method. Call .handler() first.
906
+ builder.public();
907
+ });
908
+
909
+ it("should prevent calling .internal() before .handler()", () => {
910
+ const builder = convex.mutation().input({ name: v.string() });
911
+
912
+ // @ts-expect-error - ConvexBuilderWithFunctionKind does not have an internal method. Call .handler() first.
913
+ builder.internal();
914
+ });
915
+
916
+ it("should allow .public() after .handler()", () => {
622
917
  convex
623
- .internal()
624
918
  .query()
625
919
  .input({ id: v.string() })
626
920
  .handler(async ({ input }) => {
627
921
  assertType<string>(input.id);
628
922
  return { id: input.id };
629
- });
923
+ })
924
+ .public();
630
925
  });
631
926
 
632
- it("should allow .internal() after function type", () => {
927
+ it("should allow .internal() after .handler()", () => {
633
928
  convex
634
929
  .query()
635
- .internal()
636
930
  .input({ id: v.string() })
637
931
  .handler(async ({ input }) => {
638
932
  assertType<string>(input.id);
639
933
  return { id: input.id };
934
+ })
935
+ .internal();
936
+ });
937
+ });
938
+
939
+ describe("handler uniqueness", () => {
940
+ it("should prevent calling .handler() twice on query", () => {
941
+ const builder = convex
942
+ .query()
943
+ .input({ id: v.string() })
944
+ .handler(async ({ input }) => {
945
+ return { id: input.id };
946
+ });
947
+
948
+ // @ts-expect-error - ConvexBuilderWithHandler does not have a handler method
949
+ builder.handler(async () => ({ error: "should not be called" }));
950
+ });
951
+
952
+ it("should prevent calling .handler() twice on mutation", () => {
953
+ const builder = convex
954
+ .mutation()
955
+ .input({ name: v.string() })
956
+ .handler(async ({ input }) => {
957
+ return { name: input.name };
958
+ });
959
+
960
+ // @ts-expect-error - ConvexBuilderWithHandler does not have a handler method
961
+ builder.handler(async () => ({ error: "should not be called" }));
962
+ });
963
+
964
+ it("should prevent calling .handler() twice on action", () => {
965
+ const builder = convex
966
+ .action()
967
+ .input({ url: v.string() })
968
+ .handler(async ({ input }) => {
969
+ return { url: input.url };
640
970
  });
971
+
972
+ // @ts-expect-error - ConvexBuilderWithHandler does not have a handler method
973
+ builder.handler(async () => ({ error: "should not be called" }));
974
+ });
975
+
976
+ it("should prevent calling .handler() twice even with middleware in between", () => {
977
+ const authMiddleware = convex
978
+ .query()
979
+ .middleware(async ({ context, next }) => {
980
+ return next({ context });
981
+ });
982
+
983
+ const builder = convex
984
+ .query()
985
+ .input({ id: v.string() })
986
+ .handler(async ({ input }) => {
987
+ return { id: input.id };
988
+ })
989
+ .use(authMiddleware);
990
+
991
+ // @ts-expect-error - ConvexBuilderWithHandler does not have a handler method
992
+ builder.handler(async () => ({ error: "should not be called" }));
993
+ });
994
+
995
+ it("should prevent calling .handler() twice even with returns validator", () => {
996
+ const builder = convex
997
+ .query()
998
+ .input({ count: v.number() })
999
+ .returns(v.object({ numbers: v.array(v.number()) }))
1000
+ .handler(async () => {
1001
+ return { numbers: [1, 2, 3] };
1002
+ });
1003
+
1004
+ // @ts-expect-error - ConvexBuilderWithHandler does not have a handler method
1005
+ builder.handler(async () => ({ numbers: [] }));
1006
+ });
1007
+
1008
+ it("should allow chaining .use() after handler()", () => {
1009
+ const authMiddleware = convex
1010
+ .query()
1011
+ .middleware(async ({ context, next }) => {
1012
+ return next({ context });
1013
+ });
1014
+
1015
+ const builder = convex
1016
+ .query()
1017
+ .input({ id: v.string() })
1018
+ .handler(async ({ input }) => {
1019
+ return { id: input.id };
1020
+ })
1021
+ .use(authMiddleware);
1022
+
1023
+ // Should be able to call .public() or .internal() after .use()
1024
+ assertType<typeof builder>(builder);
1025
+ });
1026
+
1027
+ it("should prevent calling .returns() after handler()", () => {
1028
+ const builder = convex
1029
+ .query()
1030
+ .input({ count: v.number() })
1031
+ .handler(async () => {
1032
+ return { numbers: [1, 2, 3] };
1033
+ });
1034
+
1035
+ // @ts-expect-error - ConvexBuilderWithHandler does not have a returns method. Call .returns() before .handler().
1036
+ builder.returns(v.object({ numbers: v.array(v.number()) }));
1037
+ });
1038
+
1039
+ it("should allow calling .returns() before handler()", () => {
1040
+ const builder = convex
1041
+ .query()
1042
+ .input({ count: v.number() })
1043
+ .returns(v.object({ numbers: v.array(v.number()) }))
1044
+ .handler(async () => {
1045
+ return { numbers: [1, 2, 3] };
1046
+ });
1047
+
1048
+ // Should be able to call .public() or .internal() after .handler()
1049
+ assertType<typeof builder>(builder);
1050
+ });
1051
+ });
1052
+
1053
+ describe("callable builder functionality", () => {
1054
+ it("should make ConvexBuilderWithHandler callable", () => {
1055
+ const nonRegisteredQuery = convex
1056
+ .query()
1057
+ .input({ count: v.number() })
1058
+ .handler(async ({ context, input }) => {
1059
+ return `the count is ${input.count}`;
1060
+ });
1061
+
1062
+ // Should be callable
1063
+ assertType<
1064
+ (context: any) => (args: { count: number }) => Promise<string>
1065
+ >(nonRegisteredQuery);
1066
+ });
1067
+
1068
+ it("should make registered queries non-callable", () => {
1069
+ const nonRegisteredQuery = convex
1070
+ .query()
1071
+ .input({ count: v.number() })
1072
+ .handler(async ({ context, input }) => {
1073
+ return `the count is ${input.count}`;
1074
+ });
1075
+
1076
+ const registeredQuery = nonRegisteredQuery.public();
1077
+
1078
+ // Should NOT be callable - RegisteredQuery is not a function
1079
+ // @ts-expect-error - RegisteredQuery is not callable
1080
+ registeredQuery({} as any);
1081
+ });
1082
+
1083
+ it("should make registered mutations non-callable", () => {
1084
+ const nonRegisteredMutation = convex
1085
+ .mutation()
1086
+ .input({ value: v.number() })
1087
+ .handler(async ({ context, input }) => {
1088
+ return await context.db.insert("numbers", { value: input.value });
1089
+ });
1090
+
1091
+ const registeredMutation = nonRegisteredMutation.public();
1092
+
1093
+ // Should NOT be callable - RegisteredMutation is not a function
1094
+ // @ts-expect-error - RegisteredMutation is not callable
1095
+ registeredMutation({} as any);
1096
+ });
1097
+
1098
+ it("should make registered actions non-callable", () => {
1099
+ const nonRegisteredAction = convex
1100
+ .action()
1101
+ .input({ url: v.string() })
1102
+ .handler(async ({ input }) => {
1103
+ return { url: input.url };
1104
+ });
1105
+
1106
+ const registeredAction = nonRegisteredAction.public();
1107
+
1108
+ // Should NOT be callable - RegisteredAction is not a function
1109
+ // @ts-expect-error - RegisteredAction is not callable
1110
+ registeredAction({} as any);
1111
+ });
1112
+
1113
+ it("should make internal registered queries non-callable", () => {
1114
+ const nonRegisteredQuery = convex
1115
+ .query()
1116
+ .input({ count: v.number() })
1117
+ .handler(async ({ input }) => {
1118
+ return { count: input.count };
1119
+ });
1120
+
1121
+ const registeredQuery = nonRegisteredQuery.internal();
1122
+
1123
+ // Should NOT be callable - RegisteredQuery is not a function
1124
+ // @ts-expect-error - RegisteredQuery is not callable
1125
+ registeredQuery({} as any);
1126
+ });
1127
+
1128
+ it("should preserve callability through middleware chain", () => {
1129
+ const authMiddleware = convex
1130
+ .query()
1131
+ .middleware(async ({ context, next }) => {
1132
+ return next({
1133
+ context: {
1134
+ ...context,
1135
+ userId: "user-123",
1136
+ },
1137
+ });
1138
+ });
1139
+
1140
+ const callableQuery = convex
1141
+ .query()
1142
+ .input({ count: v.number() })
1143
+ .use(authMiddleware)
1144
+ .handler(async ({ context, input }) => {
1145
+ assertType<string>(context.userId);
1146
+ return { count: input.count, userId: context.userId };
1147
+ });
1148
+
1149
+ // Should still be callable after middleware
1150
+ assertType<
1151
+ (
1152
+ context: any
1153
+ ) => (args: {
1154
+ count: number;
1155
+ }) => Promise<{ count: number; userId: string }>
1156
+ >(callableQuery);
1157
+ });
1158
+
1159
+ it("should preserve callability after multiple middleware", () => {
1160
+ const authMiddleware = convex
1161
+ .query()
1162
+ .middleware(async ({ context, next }) => {
1163
+ return next({
1164
+ context: {
1165
+ ...context,
1166
+ userId: "user-123",
1167
+ },
1168
+ });
1169
+ });
1170
+
1171
+ const loggingMiddleware = convex
1172
+ .query()
1173
+ .middleware(async ({ context, next }) => {
1174
+ return next({
1175
+ context: {
1176
+ ...context,
1177
+ requestId: "req-123",
1178
+ },
1179
+ });
1180
+ });
1181
+
1182
+ const callableQuery = convex
1183
+ .query()
1184
+ .input({ count: v.number() })
1185
+ .use(authMiddleware)
1186
+ .use(loggingMiddleware)
1187
+ .handler(async ({ context, input }) => {
1188
+ assertType<string>(context.userId);
1189
+ assertType<string>(context.requestId);
1190
+ return { count: input.count };
1191
+ });
1192
+
1193
+ // Should still be callable after multiple middleware
1194
+ assertType<
1195
+ (
1196
+ context: any
1197
+ ) => (args: { count: number }) => Promise<{ count: number }>
1198
+ >(callableQuery);
1199
+ });
1200
+
1201
+ it("should work with mutations", () => {
1202
+ const callableMutation = convex
1203
+ .mutation()
1204
+ .input({ value: v.number() })
1205
+ .handler(async ({ context, input }) => {
1206
+ return await context.db.insert("numbers", { value: input.value });
1207
+ });
1208
+
1209
+ // Should be callable
1210
+ assertType<(context: any) => (args: { value: number }) => Promise<any>>(
1211
+ callableMutation
1212
+ );
1213
+ });
1214
+
1215
+ it("should work with actions", () => {
1216
+ const callableAction = convex
1217
+ .action()
1218
+ .input({ url: v.string() })
1219
+ .handler(async ({ input }) => {
1220
+ return { url: input.url };
1221
+ });
1222
+
1223
+ // Should be callable
1224
+ assertType<
1225
+ (context: any) => (args: { url: string }) => Promise<{ url: string }>
1226
+ >(callableAction);
1227
+ });
1228
+
1229
+ it("should work with optional input", () => {
1230
+ const callableQuery = convex
1231
+ .query()
1232
+ .input({
1233
+ name: v.optional(v.string()),
1234
+ count: v.optional(v.number()),
1235
+ })
1236
+ .handler(async ({ input }) => {
1237
+ return {
1238
+ name: input.name,
1239
+ count: input.count,
1240
+ };
1241
+ });
1242
+
1243
+ // Should be callable
1244
+ assertType<
1245
+ (
1246
+ context: any
1247
+ ) => (args: {
1248
+ name?: string;
1249
+ count?: number;
1250
+ }) => Promise<{ name?: string; count?: number }>
1251
+ >(callableQuery);
1252
+ });
1253
+
1254
+ it("should work with return validators", () => {
1255
+ const callableQuery = convex
1256
+ .query()
1257
+ .input({ count: v.number() })
1258
+ .returns(v.object({ numbers: v.array(v.number()) }))
1259
+ .handler(async ({ input }) => {
1260
+ return {
1261
+ numbers: Array(input.count)
1262
+ .fill(0)
1263
+ .map((_, i) => i),
1264
+ };
1265
+ });
1266
+
1267
+ // Should be callable
1268
+ assertType<
1269
+ (
1270
+ context: any
1271
+ ) => (args: { count: number }) => Promise<{ numbers: number[] }>
1272
+ >(callableQuery);
1273
+ });
1274
+
1275
+ it("should work with Zod input validators", () => {
1276
+ const callableQuery = convex
1277
+ .query()
1278
+ .input(z.object({ count: z.number() }))
1279
+ .handler(async ({ input }) => {
1280
+ return { count: input.count };
1281
+ });
1282
+
1283
+ // Should be callable
1284
+ assertType<
1285
+ (
1286
+ context: any
1287
+ ) => (args: { count: number }) => Promise<{ count: number }>
1288
+ >(callableQuery);
1289
+ });
1290
+
1291
+ it("should work with no input", () => {
1292
+ const callableQuery = convex.query().handler(async () => {
1293
+ return { success: true };
1294
+ });
1295
+
1296
+ // Should be callable
1297
+ assertType<
1298
+ (
1299
+ context: any
1300
+ ) => (args: Record<never, never>) => Promise<{ success: boolean }>
1301
+ >(callableQuery);
1302
+ });
1303
+
1304
+ it("should lose callability after .public()", () => {
1305
+ const callableQuery = convex
1306
+ .query()
1307
+ .input({ count: v.number() })
1308
+ .handler(async ({ input }) => {
1309
+ return { count: input.count };
1310
+ });
1311
+
1312
+ // Before .public(), should be callable
1313
+ assertType<
1314
+ (
1315
+ context: any
1316
+ ) => (args: { count: number }) => Promise<{ count: number }>
1317
+ >(callableQuery);
1318
+
1319
+ const registeredQuery = callableQuery.public();
1320
+
1321
+ // After .public(), should NOT be callable
1322
+ // @ts-expect-error - RegisteredQuery is not callable
1323
+ registeredQuery({} as any);
1324
+ });
1325
+
1326
+ it("should lose callability after .internal()", () => {
1327
+ const callableMutation = convex
1328
+ .mutation()
1329
+ .input({ value: v.number() })
1330
+ .handler(async ({ context, input }) => {
1331
+ return await context.db.insert("numbers", { value: input.value });
1332
+ });
1333
+
1334
+ // Before .internal(), should be callable
1335
+ assertType<(context: any) => (args: { value: number }) => Promise<any>>(
1336
+ callableMutation
1337
+ );
1338
+
1339
+ const registeredMutation = callableMutation.internal();
1340
+
1341
+ // After .internal(), should NOT be callable
1342
+ // @ts-expect-error - RegisteredMutation is not callable
1343
+ registeredMutation({} as any);
641
1344
  });
642
1345
  });
643
1346
  });