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.
- package/dist/builder.d.ts +35 -13
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +104 -28
- package/dist/builder.js.map +1 -1
- package/dist/decorators.d.ts +40 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +148 -0
- package/dist/decorators.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/builder.test-d.ts +749 -46
- package/src/builder.ts +357 -144
- package/src/decorators.ts +218 -0
- package/src/experimental.test-d.ts +65 -0
- package/src/index.ts +9 -0
- package/src/types.ts +2 -0
|
@@ -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
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> =
|