Skip to content

Commit d46ddae

Browse files
committed
feat: make builder more type safe and add metadata
1 parent 9fb67d4 commit d46ddae

File tree

4 files changed

+97
-42
lines changed

4 files changed

+97
-42
lines changed

example/worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class MyDurableObject extends SafeDurableObjectBuilder(
2929
hello: fn
3030
.input(z.string())
3131
.output(z.object({ message: z.string(), id: z.string() }))
32+
.meta({ description: "Say hello to the server" })
3233
.implement(function ({ ctx, input }) {
3334
const state = this.state;
3435
this.setState({

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
"license": "MIT",
4343
"packageManager": "pnpm@10.12.4",
4444
"peerDependencies": {
45-
"@cloudflare/workers-types": "^4.20250705.0",
46-
"zod": "^3.25.74"
45+
"@cloudflare/workers-types": "^4",
46+
"zod": "^3.25"
4747
},
4848
"devDependencies": {
4949
"tsdown": "^0.12.9",

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ export type ImplArgs<I extends z.ZodType, Env> = {
1212
export type SafeRpcHandler<
1313
I extends z.ZodType,
1414
O extends z.ZodType,
15-
InferredOutput = z.infer<O>
15+
InferredOutput = z.infer<O>,
16+
Meta = unknown
1617
> = {
1718
(input: z.infer<I>): Promise<InferredOutput>;
1819
_def: {
1920
input: I;
2021
output: O;
22+
meta: Meta;
2123
};
2224
[SafeRpcMethodSymbol]: true;
2325
};
@@ -28,11 +30,13 @@ function createHandler<
2830
I extends z.ZodType,
2931
O extends z.ZodType,
3032
Env,
31-
T extends DurableObject<Env>
33+
T extends DurableObject<Env>,
34+
Meta = unknown
3235
>(
3336
fn: (args: ImplArgs<I, Env>) => MaybePromise<z.infer<O>>,
3437
inputSchema: I,
35-
outputSchema?: O
38+
outputSchema?: O,
39+
meta?: Meta
3640
) {
3741
const handler = async function (this: T, rawInput: z.infer<I>) {
3842
const parsedInput = await inputSchema.parseAsync(rawInput);
@@ -49,6 +53,7 @@ function createHandler<
4953
value: {
5054
input: inputSchema,
5155
output: outputSchema ?? z.unknown(),
56+
meta: meta ?? {},
5257
},
5358
enumerable: true,
5459
},
@@ -61,39 +66,88 @@ function createHandler<
6166
return handlerWithDef;
6267
}
6368

64-
function routeBuilder<Env, T extends DurableObject<Env>>() {
65-
return {
66-
input<I extends z.ZodType>(inputSchema: I) {
67-
return {
68-
output<O extends z.ZodType>(outputSchema: O) {
69-
return {
70-
implement(
71-
fn: (this: T, args: ImplArgs<I, Env>) => MaybePromise<z.infer<O>>
72-
) {
73-
return createHandler(
74-
fn,
75-
inputSchema,
76-
outputSchema
77-
) as SafeRpcHandler<I, O>;
78-
},
79-
};
80-
},
81-
implement<R>(fn: (this: T, args: ImplArgs<I, Env>) => MaybePromise<R>) {
82-
return createHandler(fn, inputSchema) as SafeRpcHandler<
83-
I,
84-
z.ZodUnknown,
85-
R
86-
>;
87-
},
88-
};
89-
},
90-
};
69+
class RouteBuilder<
70+
Env,
71+
T extends DurableObject<Env>,
72+
I extends z.ZodType | undefined = undefined,
73+
O extends z.ZodType | undefined = undefined,
74+
Meta = unknown
75+
> {
76+
private inputSchema?: I;
77+
private outputSchema?: O;
78+
private metaData?: Meta;
79+
80+
constructor(inputSchema?: I, outputSchema?: O, metaData?: Meta) {
81+
this.inputSchema = inputSchema;
82+
this.outputSchema = outputSchema;
83+
this.metaData = metaData;
84+
return this;
85+
}
86+
87+
input<InputSchema extends z.ZodType>(
88+
inputSchema: InputSchema
89+
): RouteBuilder<Env, T, InputSchema, O, Meta> {
90+
if (this.inputSchema) throw new Error("Input schema already set");
91+
return new RouteBuilder<Env, T, InputSchema, O, Meta>(
92+
inputSchema,
93+
this.outputSchema,
94+
this.metaData
95+
);
96+
}
97+
98+
output<OutputSchema extends z.ZodType>(
99+
this: RouteBuilder<Env, T, I, undefined, Meta>,
100+
outputSchema: OutputSchema
101+
): RouteBuilder<Env, T, I, OutputSchema, Meta> {
102+
if (this.outputSchema) throw new Error("Output schema already set");
103+
return new RouteBuilder<Env, T, I, OutputSchema, Meta>(
104+
this.inputSchema,
105+
outputSchema,
106+
this.metaData
107+
);
108+
}
109+
110+
meta<MetaData>(metaData: MetaData): RouteBuilder<Env, T, I, O, MetaData> {
111+
if (this.metaData) throw new Error("Metadata already set");
112+
return new RouteBuilder<Env, T, I, O, MetaData>(
113+
this.inputSchema,
114+
this.outputSchema,
115+
metaData
116+
);
117+
}
118+
119+
implement<R>(
120+
this: RouteBuilder<Env, T, I, undefined, Meta>,
121+
fn: I extends z.ZodType
122+
? (this: T, args: ImplArgs<I, Env>) => MaybePromise<R>
123+
: never
124+
): I extends z.ZodType ? SafeRpcHandler<I, z.ZodUnknown, R, Meta> : never;
125+
126+
implement(
127+
this: RouteBuilder<Env, T, I, O, Meta>,
128+
fn: I extends z.ZodType
129+
? O extends z.ZodType
130+
? (this: T, args: ImplArgs<I, Env>) => MaybePromise<z.infer<O>>
131+
: never
132+
: never
133+
): I extends z.ZodType
134+
? O extends z.ZodType
135+
? SafeRpcHandler<I, O, z.infer<O>, Meta>
136+
: never
137+
: never;
138+
139+
implement(
140+
fn: (this: T, args: ImplArgs<any, Env>) => MaybePromise<any>
141+
): SafeRpcHandler<any, any, any, Meta> {
142+
if (!this.inputSchema) throw new Error("Input schema is required");
143+
return createHandler(
144+
fn,
145+
this.inputSchema as z.ZodType,
146+
this.outputSchema as z.ZodType | undefined,
147+
this.metaData
148+
) as SafeRpcHandler<any, any, any, Meta>;
149+
}
91150
}
92-
93-
type RouteBuilder<Env, T extends DurableObject<Env>> = ReturnType<
94-
typeof routeBuilder<Env, T>
95-
>;
96-
97151
type AnyDurableClass<T extends DurableObject, Env> = new (
98152
ctx: DurableObjectState,
99153
env: Env
@@ -102,15 +156,15 @@ type AnyDurableClass<T extends DurableObject, Env> = new (
102156
export function SafeDurableObjectBuilder<
103157
Env,
104158
T extends DurableObject<Env>,
105-
Router extends Record<string, SafeRpcHandler<any, any>>
159+
Router extends Record<string, SafeRpcHandler<any, any, any, any>>
106160
>(
107161
BaseClass: AnyDurableClass<T, Env>,
108162
routerBuilder: (builder: RouteBuilder<Env, T>) => Router
109163
) {
110164
// create a extended class so that we don't pollute the base class with the router
111165
class BaseClassWithSafeRpc extends BaseClass {}
112166

113-
const router = routerBuilder(routeBuilder());
167+
const router = routerBuilder(new RouteBuilder<Env, T>());
114168

115169
Object.defineProperties(
116170
BaseClassWithSafeRpc.prototype,
@@ -135,6 +189,6 @@ export function SafeDurableObjectBuilder<
135189

136190
export function isSafeDurableObjectMethod(
137191
method: any
138-
): method is SafeRpcHandler<any, any> {
192+
): method is SafeRpcHandler<any, any, any, any> {
139193
return method && method[SafeRpcMethodSymbol] === true;
140194
}

0 commit comments

Comments
 (0)