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.
@@ -0,0 +1,218 @@
1
+ import type {
2
+ ConvexArgsValidator,
3
+ ConvexReturnsValidator,
4
+ } from "./types";
5
+ import type { ValidatorInput, ReturnsValidatorInput } from "./zod_support";
6
+ import { isZodSchema, toConvexValidator } from "./zod_support";
7
+
8
+ // Metadata stored on decorated methods
9
+ // Using WeakMap for legacy decorator compatibility with esbuild
10
+ const methodMetadataMap = new WeakMap<object, Map<string | symbol, CallableMethodMetadata>>();
11
+
12
+ interface CallableMethodMetadata {
13
+ inputValidator?: ConvexArgsValidator;
14
+ inputValidatorOriginal?: ValidatorInput; // Store original for runtime validation
15
+ returnsValidator?: ConvexReturnsValidator;
16
+ returnsValidatorOriginal?: ReturnsValidatorInput; // Store original for runtime validation
17
+ }
18
+
19
+ // Get metadata from a method (legacy decorator support)
20
+ function getMethodMetadata(
21
+ target: any,
22
+ propertyKey: string | symbol,
23
+ ): CallableMethodMetadata {
24
+ const prototype = typeof target === "function" ? target.prototype : target;
25
+ const metadataMap = methodMetadataMap.get(prototype);
26
+ if (!metadataMap) return {};
27
+ return metadataMap.get(propertyKey) || {};
28
+ }
29
+
30
+ // Set metadata on a method (legacy decorator support)
31
+ function setMethodMetadata(
32
+ target: any,
33
+ propertyKey: string | symbol,
34
+ metadata: CallableMethodMetadata,
35
+ ): void {
36
+ const prototype = typeof target === "function" ? target.prototype : target;
37
+ let metadataMap = methodMetadataMap.get(prototype);
38
+ if (!metadataMap) {
39
+ metadataMap = new Map();
40
+ methodMetadataMap.set(prototype, metadataMap);
41
+ }
42
+ const existing = metadataMap.get(propertyKey) || {};
43
+ metadataMap.set(propertyKey, { ...existing, ...metadata });
44
+ }
45
+
46
+ /**
47
+ * Decorator to specify input validation for a callable method
48
+ * Compatible with both legacy (esbuild) and modern decorator syntax
49
+ */
50
+ export function input<UInput extends ValidatorInput>(
51
+ validator: UInput,
52
+ ): any {
53
+ return function (
54
+ target: any,
55
+ propertyKey: string | symbol,
56
+ descriptor?: PropertyDescriptor,
57
+ ) {
58
+ // Handle both legacy (3 args) and modern (2 args) decorator calls
59
+ if (descriptor === undefined) {
60
+ // Modern decorator - get descriptor from target
61
+ descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {};
62
+ }
63
+
64
+ const convexValidator = isZodSchema(validator)
65
+ ? toConvexValidator(validator)
66
+ : (validator as ConvexArgsValidator);
67
+
68
+ setMethodMetadata(target, propertyKey, {
69
+ inputValidator: convexValidator,
70
+ inputValidatorOriginal: validator,
71
+ });
72
+
73
+ return descriptor;
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Decorator to specify return validation for a callable method
79
+ * Compatible with both legacy (esbuild) and modern decorator syntax
80
+ */
81
+ export function returns<UReturns extends ReturnsValidatorInput>(
82
+ validator: UReturns,
83
+ ): any {
84
+ return function (
85
+ target: any,
86
+ propertyKey: string | symbol,
87
+ descriptor?: PropertyDescriptor,
88
+ ) {
89
+ // Handle both legacy (3 args) and modern (2 args) decorator calls
90
+ if (descriptor === undefined) {
91
+ // Modern decorator - get descriptor from target
92
+ descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {};
93
+ }
94
+
95
+ // For returns validators, we need GenericValidator, not PropertyValidators
96
+ // If it's a Zod schema, convert it; otherwise assume it's already a GenericValidator
97
+ const convexValidator: ConvexReturnsValidator = isZodSchema(validator)
98
+ ? (toConvexValidator(validator) as ConvexReturnsValidator)
99
+ : (validator as ConvexReturnsValidator);
100
+
101
+ setMethodMetadata(target, propertyKey, {
102
+ returnsValidator: convexValidator,
103
+ returnsValidatorOriginal: validator,
104
+ });
105
+
106
+ return descriptor;
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Get metadata for a specific method
112
+ */
113
+ export function getMetadata(
114
+ target: any,
115
+ propertyKey: string | symbol,
116
+ ): CallableMethodMetadata {
117
+ return getMethodMetadata(target, propertyKey);
118
+ }
119
+
120
+ /**
121
+ * Get metadata for a method from a class constructor
122
+ * This is the public API for accessing decorator metadata
123
+ */
124
+ export function getMethodMetadataFromClass<T extends new (...args: any[]) => any>(
125
+ ModelClass: T,
126
+ methodName: string | symbol,
127
+ ): CallableMethodMetadata {
128
+ return getMethodMetadata(ModelClass.prototype, methodName);
129
+ }
130
+
131
+ /**
132
+ * Create a proxy that automatically makes all decorated methods callable
133
+ * Usage: const callableModel = makeCallableMethods(new MyQueryModel(context));
134
+ * Then: await callableModel.listNumbers({ count: 10 });
135
+ */
136
+ export function makeCallableMethods<T extends object>(instance: T): T {
137
+ const callableMethods = new Map<
138
+ string | symbol,
139
+ (...args: any[]) => Promise<any>
140
+ >();
141
+ const prototype = Object.getPrototypeOf(instance);
142
+
143
+ // Find all methods on the prototype and check if they have metadata
144
+ const propertyNames = Object.getOwnPropertyNames(prototype);
145
+ for (const propName of propertyNames) {
146
+ if (propName !== "constructor") {
147
+ const metadata = getMethodMetadata(prototype, propName);
148
+ // If method has metadata, make it callable
149
+ if (metadata.inputValidator || metadata.returnsValidator) {
150
+ callableMethods.set(
151
+ propName,
152
+ makeCallable(instance, propName as keyof T),
153
+ );
154
+ }
155
+ }
156
+ }
157
+
158
+ return new Proxy(instance, {
159
+ get(target, prop) {
160
+ // Return callable version if available
161
+ if (callableMethods.has(prop)) {
162
+ return callableMethods.get(prop);
163
+ }
164
+ // Otherwise return original property
165
+ return (target as any)[prop];
166
+ },
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Make a method callable with validation
172
+ * This wraps the original method to validate inputs and optionally outputs
173
+ */
174
+ export function makeCallable<T extends object>(
175
+ instance: T,
176
+ methodName: keyof T,
177
+ ): (...args: any[]) => Promise<any> {
178
+ const originalMethod = (instance as any)[methodName];
179
+
180
+ if (typeof originalMethod !== "function") {
181
+ throw new Error(
182
+ `Method '${String(methodName)}' is not a function on ${instance.constructor.name}`,
183
+ );
184
+ }
185
+
186
+ // Get metadata from the prototype
187
+ const prototype = Object.getPrototypeOf(instance);
188
+ const metadata = getMethodMetadata(prototype, methodName as string | symbol);
189
+
190
+ return async (...args: any[]) => {
191
+ // Validate input if validator is provided
192
+ if (metadata.inputValidatorOriginal) {
193
+ const input = args[0] || {};
194
+ if (isZodSchema(metadata.inputValidatorOriginal)) {
195
+ // Use Zod's parse for runtime validation
196
+ const parsed = metadata.inputValidatorOriginal.parse(input);
197
+ args[0] = parsed;
198
+ }
199
+ // For Convex validators, we skip runtime validation
200
+ // as they're primarily for type checking and Convex handles validation
201
+ }
202
+
203
+ // Call the original method
204
+ const result = await originalMethod.apply(instance, args);
205
+
206
+ // Validate return value if validator is provided
207
+ if (metadata.returnsValidatorOriginal) {
208
+ if (isZodSchema(metadata.returnsValidatorOriginal)) {
209
+ // Use Zod's parse for runtime validation
210
+ return metadata.returnsValidatorOriginal.parse(result);
211
+ }
212
+ // For Convex validators, we skip runtime validation
213
+ }
214
+
215
+ return result;
216
+ };
217
+ }
218
+
@@ -0,0 +1,65 @@
1
+ import { describe, it, assertType, test, expectTypeOf } from "vitest";
2
+ import { v } from "convex/values";
3
+ import { z } from "zod";
4
+ import {
5
+ defineSchema,
6
+ defineTable,
7
+ FunctionReference,
8
+ FilterApi,
9
+ RegisteredQuery,
10
+ GenericQueryCtx,
11
+ GenericDataModel,
12
+ ApiFromModules,
13
+ queryGeneric,
14
+ } from "convex/server";
15
+ import { createBuilder } from "./builder";
16
+
17
+ const schema = defineSchema({
18
+ numbers: defineTable({
19
+ value: v.number(),
20
+ }),
21
+ });
22
+
23
+ const convex = createBuilder(schema);
24
+
25
+ // Base types
26
+ type TArgs = { count: number };
27
+ type THandlerReturn = { numbers: number[] };
28
+ type TContext = GenericQueryCtx<any>;
29
+
30
+ // This type works - it's a direct intersection, not from a conditional type
31
+ type TestQuery = RegisteredQuery<"public", TArgs, Promise<THandlerReturn>>;
32
+
33
+ type CallableTestQuery = RegisteredQuery<
34
+ "public",
35
+ TArgs,
36
+ Promise<THandlerReturn>
37
+ > &
38
+ ((context: TContext) => (args: TArgs) => Promise<THandlerReturn>);
39
+
40
+ test("classic convex ", () => {
41
+ type Api = ApiFromModules<{
42
+ module1: {
43
+ someFunction: RegisteredQuery<"public", any, any>;
44
+ };
45
+ }>;
46
+
47
+ type FilteredApi = FilterApi<Api, FunctionReference<any, "public">>;
48
+
49
+ expectTypeOf<FilteredApi["module1"]>().not.toBeNever();
50
+ });
51
+
52
+ test("classic convex and callable", () => {
53
+ type Api = ApiFromModules<{
54
+ module1: {
55
+ someFunction: RegisteredQuery<"public", any, any> &
56
+ // I really wanted queries to be directly callable like this but it doesnt work :(
57
+ ((context: any) => (args: any) => Promise<any>);
58
+ };
59
+ }>;
60
+
61
+ type FilteredApi = FilterApi<Api, FunctionReference<any, "public">>;
62
+
63
+ // @ts-expect-error the intersection type on someFunction isnt accepted
64
+ expectTypeOf<FilteredApi["module1"]>().not.toBeNever();
65
+ });
package/src/index.ts CHANGED
@@ -24,3 +24,12 @@ export {
24
24
  type ValidatorInput,
25
25
  type ReturnsValidatorInput,
26
26
  } from "./zod_support";
27
+
28
+ export {
29
+ input,
30
+ returns,
31
+ getMetadata,
32
+ getMethodMetadataFromClass,
33
+ makeCallableMethods,
34
+ makeCallable,
35
+ } from "./decorators";
package/src/types.ts CHANGED
@@ -49,6 +49,8 @@ type RequiredArgs<T extends Record<PropertyKey, any>> = {
49
49
  export type InferArgs<T extends ConvexArgsValidator> =
50
50
  T extends GenericValidator ? T["type"] : RequiredArgs<T> & OptionalArgs<T>;
51
51
 
52
+ export type InferReturns<T extends ConvexReturnsValidator> = ValidatorType<T>;
53
+
52
54
  export type Promisable<T> = T | PromiseLike<T>;
53
55
 
54
56
  export type QueryCtx<DataModel extends GenericDataModel = GenericDataModel> =