effect-orpc 0.1.4 → 0.2.1

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,124 @@
1
+ import type { Middleware, MiddlewareOutputFn, Procedure } from "@orpc/server";
2
+ import { describe, expectTypeOf, it } from "vitest";
3
+
4
+ import { makeEffectORPC } from "../index";
5
+ import type { CurrentContext, InitialContext } from "./parity-shared";
6
+ import { runtime } from "./parity-shared";
7
+ import {
8
+ type AssertExtends,
9
+ baseErrorMap,
10
+ type BaseMeta,
11
+ inputSchema,
12
+ outputSchema,
13
+ } from "./shared";
14
+
15
+ const procedure = makeEffectORPC(runtime)
16
+ .$context<InitialContext>()
17
+ .$meta({ mode: "dev" } as BaseMeta)
18
+ .$input(inputSchema)
19
+ .errors(baseErrorMap)
20
+ .use(({ next }) => next({ context: { auth: true as boolean } }))
21
+ .output(outputSchema)
22
+ .effect(function* () {
23
+ return { output: 456 };
24
+ });
25
+
26
+ describe("parity: @orpc/server procedure-decorated.test-d.ts", () => {
27
+ it("is a procedure", () => {
28
+ expectTypeOf<
29
+ AssertExtends<
30
+ typeof procedure,
31
+ Procedure<
32
+ InitialContext & Record<never, never>,
33
+ CurrentContext,
34
+ typeof inputSchema,
35
+ typeof outputSchema,
36
+ typeof baseErrorMap,
37
+ BaseMeta
38
+ >
39
+ >
40
+ >().toEqualTypeOf<true>();
41
+ });
42
+
43
+ it(".errors / .meta / .route", () => {
44
+ expectTypeOf(
45
+ procedure.errors({
46
+ BAD_GATEWAY: { message: "BAD_GATEWAY" },
47
+ OVERRIDE: { message: "OVERRIDE" },
48
+ }),
49
+ ).toBeObject();
50
+
51
+ expectTypeOf(procedure.meta({ mode: "dev", log: true })).toBeObject();
52
+ expectTypeOf(
53
+ procedure.route({ method: "POST", path: "/v2/users", tags: ["tag"] }),
54
+ ).toBeObject();
55
+
56
+ // @ts-expect-error - invalid meta
57
+ procedure.meta({ log: "INVALID" });
58
+ // @ts-expect-error - invalid method
59
+ procedure.route({ method: "INVALID" });
60
+ });
61
+
62
+ describe(".use", () => {
63
+ it("without map input", () => {
64
+ expectTypeOf(
65
+ procedure.use(({ next }, input, output) => {
66
+ expectTypeOf(input).toEqualTypeOf<{ input: string }>();
67
+ expectTypeOf(output).toBeFunction();
68
+ return next({ context: { extra: true } });
69
+ }),
70
+ ).toBeObject();
71
+
72
+ procedure.use(
73
+ // @ts-expect-error - invalid TInContext
74
+ {} as Middleware<{ auth: "invalid" }, any, any, any, any, any>,
75
+ );
76
+ // @ts-expect-error - input is not match
77
+ procedure.use(({ next }, _input: "invalid") => next({}));
78
+ procedure.use(
79
+ // @ts-expect-error - output is not match
80
+ ({ next }, _input, _output: MiddlewareOutputFn<"invalid">) => next({}),
81
+ );
82
+ // @ts-expect-error - conflict context
83
+ procedure.use(({ next }) => next({ context: { db: undefined } }));
84
+ });
85
+
86
+ it("with map input", () => {
87
+ expectTypeOf(
88
+ procedure.use(
89
+ ({ next }, input: { mapped: string }, output) => {
90
+ expectTypeOf(input).toEqualTypeOf<{ mapped: string }>();
91
+ expectTypeOf(output).toBeFunction();
92
+ return next({ context: { extra: true } });
93
+ },
94
+ (input) => ({ mapped: input.input }),
95
+ ),
96
+ ).toBeObject();
97
+ });
98
+ });
99
+
100
+ it(".callable / .actionable", () => {
101
+ expectTypeOf(
102
+ procedure.callable({
103
+ context: async (_clientContext: { batch?: boolean }) => ({
104
+ db: "postgres",
105
+ }),
106
+ }),
107
+ ).toBeFunction();
108
+
109
+ expectTypeOf(
110
+ procedure.actionable({
111
+ context: async (_clientContext: { batch?: boolean }) => ({
112
+ db: "postgres",
113
+ }),
114
+ }),
115
+ ).toBeFunction();
116
+
117
+ procedure.actionable({
118
+ // @ts-expect-error - all clientContext must be optional
119
+ context: async (_clientContext: { batch: boolean }) => ({
120
+ db: "postgres",
121
+ }),
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,249 @@
1
+ import type { Client, ORPCError } from "@orpc/client";
2
+ import type { ErrorFromErrorMap } from "@orpc/contract";
3
+ import type {
4
+ ActionableClient,
5
+ ActionableError,
6
+ ImplementedProcedure,
7
+ Middleware,
8
+ MiddlewareOutputFn,
9
+ ProcedureImplementer,
10
+ } from "@orpc/server";
11
+ import { describe, expectTypeOf, it } from "vitest";
12
+
13
+ import { implementEffect } from "../index";
14
+ import type { InitialContext } from "./parity-shared";
15
+ import { runtime, typedContract } from "./parity-shared";
16
+ import {
17
+ type AssertExtends,
18
+ baseErrorMap,
19
+ type BaseMeta,
20
+ inputSchema,
21
+ outputSchema,
22
+ } from "./shared";
23
+
24
+ const implementer = implementEffect(typedContract, runtime)
25
+ .$context<InitialContext>()
26
+ .use(({ next }) => next({ context: { auth: true as boolean } }));
27
+
28
+ describe("parity: @orpc/server implementer-procedure.test-d.ts", () => {
29
+ const builder = implementer.ping;
30
+
31
+ it("backward compatibility", () => {
32
+ expectTypeOf<keyof typeof builder>().toEqualTypeOf<
33
+ "~orpc" | "handler" | "use" | "effect"
34
+ >();
35
+ });
36
+
37
+ it("is a contract procedure", () => {
38
+ expectTypeOf(builder["~orpc"]).toBeObject();
39
+ });
40
+
41
+ it("extends the matching upstream implementer interface", () => {
42
+ expectTypeOf<
43
+ AssertExtends<
44
+ typeof builder,
45
+ ProcedureImplementer<
46
+ InitialContext & Record<never, never>,
47
+ InitialContext & { auth: boolean },
48
+ typeof inputSchema,
49
+ typeof outputSchema,
50
+ typeof baseErrorMap,
51
+ BaseMeta
52
+ >
53
+ >
54
+ >().toEqualTypeOf<true>();
55
+ });
56
+
57
+ describe(".use", () => {
58
+ it("without map input", () => {
59
+ expectTypeOf(
60
+ builder.use(
61
+ (
62
+ { context, next, path, procedure, errors, signal },
63
+ input,
64
+ output,
65
+ ) => {
66
+ expectTypeOf(input).toEqualTypeOf<{ input: string }>();
67
+ expectTypeOf(context).toEqualTypeOf<
68
+ InitialContext & { auth: boolean }
69
+ >();
70
+ expectTypeOf(path).toExtend<readonly string[]>();
71
+ expectTypeOf(procedure).toBeObject();
72
+ expectTypeOf(output).toBeFunction();
73
+ expectTypeOf<
74
+ AssertExtends<
75
+ ReturnType<typeof errors.BASE>,
76
+ ORPCError<"BASE", { output: number }>
77
+ >
78
+ >().toEqualTypeOf<true>();
79
+ expectTypeOf<
80
+ AssertExtends<
81
+ ReturnType<typeof errors.OVERRIDE>,
82
+ ORPCError<"OVERRIDE", any>
83
+ >
84
+ >().toEqualTypeOf<true>();
85
+ expectTypeOf(signal).toExtend<AbortSignal | undefined>();
86
+
87
+ return next({ context: { extra: true } });
88
+ },
89
+ ),
90
+ ).toBeObject();
91
+
92
+ builder.use(
93
+ // @ts-expect-error - invalid TInContext
94
+ {} as Middleware<{ auth: "invalid" }, any, any, any, any, any>,
95
+ );
96
+ // @ts-expect-error - input is not match
97
+ builder.use(({ next }, _input: "invalid") => next({}));
98
+ // @ts-expect-error - output is not match
99
+ builder.use(({ next }, _input, _output: MiddlewareOutputFn<"invalid">) =>
100
+ next({}),
101
+ );
102
+ // @ts-expect-error - conflict context
103
+ builder.use(({ next }) => next({ context: { db: undefined } }));
104
+ });
105
+
106
+ it("with map input", () => {
107
+ expectTypeOf(
108
+ builder.use(
109
+ (
110
+ { context, next, path, procedure, errors, signal },
111
+ input: { mapped: boolean },
112
+ output,
113
+ ) => {
114
+ expectTypeOf(input).toEqualTypeOf<{ mapped: boolean }>();
115
+ expectTypeOf(context).toEqualTypeOf<
116
+ InitialContext & { auth: boolean }
117
+ >();
118
+ expectTypeOf(path).toExtend<readonly string[]>();
119
+ expectTypeOf(procedure).toBeObject();
120
+ expectTypeOf(output).toBeFunction();
121
+ expectTypeOf<
122
+ AssertExtends<
123
+ ReturnType<typeof errors.BASE>,
124
+ ORPCError<"BASE", { output: number }>
125
+ >
126
+ >().toEqualTypeOf<true>();
127
+ expectTypeOf<
128
+ AssertExtends<
129
+ ReturnType<typeof errors.OVERRIDE>,
130
+ ORPCError<"OVERRIDE", any>
131
+ >
132
+ >().toEqualTypeOf<true>();
133
+ expectTypeOf(signal).toExtend<AbortSignal | undefined>();
134
+
135
+ return next({ context: { extra: true } });
136
+ },
137
+ (input) => {
138
+ expectTypeOf(input).toEqualTypeOf<{ input: string }>();
139
+ return { mapped: true };
140
+ },
141
+ ),
142
+ ).toBeObject();
143
+ });
144
+
145
+ it("with TInContext", () => {
146
+ const mid = {} as Middleware<
147
+ { cacheable?: boolean } & Record<never, never>,
148
+ Record<never, never>,
149
+ unknown,
150
+ any,
151
+ any,
152
+ BaseMeta
153
+ >;
154
+
155
+ expectTypeOf(builder.use(mid)).toBeObject();
156
+ });
157
+ });
158
+
159
+ it(".handler returns an implemented procedure", () => {
160
+ const handled = builder.handler(
161
+ ({ input, context, procedure, path, signal, errors }) => {
162
+ expectTypeOf(input).toEqualTypeOf<{ input: string }>();
163
+ expectTypeOf(context).toEqualTypeOf<
164
+ InitialContext & { auth: boolean }
165
+ >();
166
+ expectTypeOf(procedure).toBeObject();
167
+ expectTypeOf(path).toExtend<readonly string[]>();
168
+ expectTypeOf(signal).toExtend<AbortSignal | undefined>();
169
+ expectTypeOf<
170
+ AssertExtends<
171
+ ReturnType<typeof errors.BASE>,
172
+ ORPCError<"BASE", { output: number }>
173
+ >
174
+ >().toEqualTypeOf<true>();
175
+ expectTypeOf<
176
+ AssertExtends<
177
+ ReturnType<typeof errors.OVERRIDE>,
178
+ ORPCError<"OVERRIDE", any>
179
+ >
180
+ >().toEqualTypeOf<true>();
181
+
182
+ return { output: 456 };
183
+ },
184
+ );
185
+
186
+ expectTypeOf<
187
+ AssertExtends<
188
+ typeof handled,
189
+ ImplementedProcedure<
190
+ InitialContext & Record<never, never>,
191
+ InitialContext & { auth: boolean },
192
+ typeof inputSchema,
193
+ typeof outputSchema,
194
+ typeof baseErrorMap,
195
+ BaseMeta
196
+ >
197
+ >
198
+ >().toEqualTypeOf<true>();
199
+
200
+ expectTypeOf(handled.use).toBeFunction();
201
+ expectTypeOf(handled.callable).toBeFunction();
202
+ expectTypeOf(handled.actionable).toBeFunction();
203
+ const callable = handled.callable({
204
+ context: async (_clientContext: { batch?: boolean }) => ({
205
+ db: "postgres",
206
+ }),
207
+ });
208
+
209
+ expectTypeOf<
210
+ AssertExtends<
211
+ typeof callable,
212
+ Client<
213
+ { batch?: boolean },
214
+ { input: number },
215
+ { output: string },
216
+ ErrorFromErrorMap<typeof baseErrorMap>
217
+ >
218
+ >
219
+ >().toEqualTypeOf<true>();
220
+
221
+ const actionable = handled.actionable({
222
+ context: async (_clientContext: { batch?: boolean }) => ({
223
+ db: "postgres",
224
+ }),
225
+ });
226
+
227
+ expectTypeOf<
228
+ AssertExtends<
229
+ typeof actionable,
230
+ ActionableClient<
231
+ { input: number },
232
+ { output: string },
233
+ ActionableError<ErrorFromErrorMap<typeof baseErrorMap>>
234
+ >
235
+ >
236
+ >().toEqualTypeOf<true>();
237
+ });
238
+ });
239
+
240
+ describe("parity: @orpc/server implementer-variants.test-d.ts", () => {
241
+ it("router-level .use returns an internal implementer", () => {
242
+ const applied = implementer.nested.use(({ next }) =>
243
+ next({ context: { extra: true } }),
244
+ );
245
+
246
+ expectTypeOf(applied).toBeObject();
247
+ expectTypeOf(applied.ping.handler).toBeFunction();
248
+ });
249
+ });
@@ -0,0 +1,280 @@
1
+ import type { ORPCError } from "@orpc/client";
2
+ import type { Meta, Schema } from "@orpc/contract";
3
+ import type { Middleware, Router } from "@orpc/server";
4
+ import { describe, expect, expectTypeOf, it } from "vitest";
5
+
6
+ import type {
7
+ EffectImplementer,
8
+ EffectImplementerInternal,
9
+ EffectProcedureImplementer,
10
+ } from "../index";
11
+ import { implementEffect } from "../index";
12
+ import type { CurrentContext, InitialContext } from "./parity-shared";
13
+ import { runtime, typedContract } from "./parity-shared";
14
+ import type { AssertExtends } from "./shared";
15
+ import {
16
+ baseErrorMap,
17
+ type BaseMeta,
18
+ inputSchema,
19
+ outputSchema,
20
+ } from "./shared";
21
+
22
+ const rootImplementer = implementEffect(typedContract, runtime);
23
+ const implementer = rootImplementer
24
+ .$context<InitialContext>()
25
+ .use(({ next }) => next({ context: { auth: true as boolean } }));
26
+
27
+ describe("parity: @orpc/server implementer.test-d.ts", () => {
28
+ describe("root level", () => {
29
+ it(".$context", () => {
30
+ expectTypeOf(rootImplementer.$context<{ anything: string }>()).toExtend<
31
+ EffectImplementer<
32
+ typeof typedContract,
33
+ { anything: string } & Record<never, never>,
34
+ { anything: string },
35
+ never,
36
+ never
37
+ >
38
+ >();
39
+ });
40
+
41
+ it(".$config", () => {
42
+ expectTypeOf(
43
+ rootImplementer.$config({
44
+ initialInputValidationIndex: Number.NEGATIVE_INFINITY,
45
+ }),
46
+ ).toExtend<
47
+ EffectImplementer<
48
+ typeof typedContract,
49
+ Record<never, never>,
50
+ Record<never, never>,
51
+ never,
52
+ never
53
+ >
54
+ >();
55
+ });
56
+ });
57
+
58
+ describe("router level", () => {
59
+ it(".middleware", () => {
60
+ expectTypeOf(
61
+ rootImplementer
62
+ .$context<InitialContext>()
63
+ .nested.middleware(
64
+ (
65
+ { context, next, path, procedure, errors, signal },
66
+ input,
67
+ output,
68
+ ) => {
69
+ expectTypeOf(context).toExtend<InitialContext>();
70
+ expectTypeOf(input).toEqualTypeOf<unknown>();
71
+ expectTypeOf(path).toExtend<readonly string[]>();
72
+ expectTypeOf(procedure).toBeObject();
73
+ expectTypeOf(output).toBeFunction();
74
+ expectTypeOf(errors).toBeObject();
75
+ expectTypeOf(signal).toExtend<AbortSignal | undefined>();
76
+
77
+ return next({ context: { extra: true } });
78
+ },
79
+ ),
80
+ ).toBeObject();
81
+ });
82
+
83
+ it(".use", () => {
84
+ expectTypeOf(
85
+ implementer.nested.use(
86
+ (
87
+ { context, next, path, procedure, errors, signal },
88
+ input,
89
+ output,
90
+ ) => {
91
+ expectTypeOf(context).toEqualTypeOf<CurrentContext>();
92
+ expectTypeOf(input).toEqualTypeOf<unknown>();
93
+ expectTypeOf(path).toExtend<readonly string[]>();
94
+ expectTypeOf(procedure).toBeObject();
95
+ expectTypeOf(output).toBeFunction();
96
+ expectTypeOf(errors).toBeObject();
97
+ expectTypeOf(signal).toExtend<AbortSignal | undefined>();
98
+
99
+ return next({ context: { extra: true } });
100
+ },
101
+ ),
102
+ ).toBeObject();
103
+
104
+ const mid = {} as Middleware<
105
+ { cacheable?: boolean } & Record<never, never>,
106
+ Record<never, never>,
107
+ unknown,
108
+ unknown,
109
+ any,
110
+ BaseMeta
111
+ >;
112
+
113
+ expectTypeOf(implementer.use(mid)).toExtend<
114
+ EffectImplementerInternal<
115
+ typeof typedContract,
116
+ InitialContext & { cacheable?: boolean },
117
+ Omit<CurrentContext, never> & Record<never, never>,
118
+ never,
119
+ never
120
+ >
121
+ >();
122
+ });
123
+
124
+ it(".router / .lazy", () => {
125
+ const implementedRouter = {
126
+ ping: implementer.ping.handler(({ input }) => ({
127
+ output: Number(input.input),
128
+ })),
129
+ pong: implementer.pong.handler(() => undefined),
130
+ nested: {
131
+ ping: implementer.nested.ping.handler(({ input }) => ({
132
+ output: Number(input.input),
133
+ })),
134
+ pong: implementer.nested.pong.handler(() => undefined),
135
+ },
136
+ };
137
+ const router = implementer.router(implementedRouter);
138
+
139
+ expectTypeOf<
140
+ AssertExtends<
141
+ typeof router,
142
+ Router<typeof typedContract, CurrentContext>
143
+ >
144
+ >().toEqualTypeOf<true>();
145
+
146
+ expectTypeOf(
147
+ implementer.lazy(async () => ({ default: implementedRouter })),
148
+ ).toBeObject();
149
+
150
+ // @ts-expect-error - meta def is not match
151
+ implementer.router({ ping: {} });
152
+
153
+ // @ts-expect-error - missing implementation
154
+ implementer.router({ ping: implementedRouter.ping });
155
+ });
156
+ });
157
+
158
+ it("each procedure is a ProcedureImplementer", () => {
159
+ type ExpectedPing = EffectProcedureImplementer<
160
+ InitialContext & Record<never, never>,
161
+ CurrentContext,
162
+ typeof inputSchema,
163
+ typeof outputSchema,
164
+ typeof baseErrorMap,
165
+ BaseMeta,
166
+ never,
167
+ never
168
+ >;
169
+
170
+ type ExpectedPong = EffectProcedureImplementer<
171
+ InitialContext & Record<never, never>,
172
+ CurrentContext,
173
+ Schema<unknown, unknown>,
174
+ Schema<unknown, unknown>,
175
+ Record<never, never>,
176
+ Meta,
177
+ never,
178
+ never
179
+ >;
180
+
181
+ expectTypeOf(implementer.ping).toExtend<ExpectedPing>();
182
+ expectTypeOf(implementer.nested.ping).toExtend<ExpectedPing>();
183
+ expectTypeOf(implementer.pong).toExtend<ExpectedPong>();
184
+ expectTypeOf(implementer.nested.pong).toExtend<ExpectedPong>();
185
+ });
186
+
187
+ it("procedure .use preserves the leaf error map", () => {
188
+ implementer.ping.use(({ errors, next }) => {
189
+ expectTypeOf<
190
+ AssertExtends<
191
+ ReturnType<typeof errors.BASE>,
192
+ ORPCError<"BASE", { output: number }>
193
+ >
194
+ >().toEqualTypeOf<true>();
195
+ expectTypeOf<
196
+ AssertExtends<
197
+ ReturnType<typeof errors.OVERRIDE>,
198
+ ORPCError<"OVERRIDE", any>
199
+ >
200
+ >().toEqualTypeOf<true>();
201
+ // @ts-expect-error - leaf middleware errors should not widen to arbitrary keys
202
+ expect(errors.MISSING).toBeUndefined();
203
+
204
+ return next({});
205
+ });
206
+
207
+ implementer.ping.use(
208
+ ({ errors, next }) => {
209
+ expectTypeOf<
210
+ AssertExtends<
211
+ ReturnType<typeof errors.BASE>,
212
+ ORPCError<"BASE", { output: number }>
213
+ >
214
+ >().toEqualTypeOf<true>();
215
+ expectTypeOf<
216
+ AssertExtends<
217
+ ReturnType<typeof errors.OVERRIDE>,
218
+ ORPCError<"OVERRIDE", any>
219
+ >
220
+ >().toEqualTypeOf<true>();
221
+ // @ts-expect-error - mapped input overload should keep the same leaf error map
222
+ expect(errors.MISSING).toBeUndefined();
223
+
224
+ return next({});
225
+ },
226
+ () => ({ mapped: true }),
227
+ );
228
+ });
229
+
230
+ it("procedure .handler preserves upstream handler option typing", () => {
231
+ implementer.ping.handler(
232
+ ({ context, input, path, procedure, errors, signal }) => {
233
+ expectTypeOf(context).toExtend<CurrentContext>();
234
+ expectTypeOf(input).toEqualTypeOf<{ input: string }>();
235
+ expectTypeOf(path).toExtend<readonly string[]>();
236
+ expectTypeOf(procedure).toBeObject();
237
+ expectTypeOf(signal).toExtend<AbortSignal | undefined>();
238
+ expectTypeOf<
239
+ AssertExtends<
240
+ ReturnType<typeof errors.BASE>,
241
+ ORPCError<"BASE", { output: number }>
242
+ >
243
+ >().toEqualTypeOf<true>();
244
+ expectTypeOf<
245
+ AssertExtends<
246
+ ReturnType<typeof errors.OVERRIDE>,
247
+ ORPCError<"OVERRIDE", any>
248
+ >
249
+ >().toEqualTypeOf<true>();
250
+ // @ts-expect-error - handler errors should not widen beyond the leaf error map
251
+ expect(errors.MISSING).toBeUndefined();
252
+
253
+ return {
254
+ output: Number(input.input),
255
+ };
256
+ },
257
+ );
258
+
259
+ implementer.ping.handler(
260
+ // @ts-expect-error - handler output must match the contract output shape
261
+ () => ({ output: "wrong" }),
262
+ );
263
+
264
+ implementer.ping.handler(
265
+ // @ts-expect-error - handler input should be the parsed contract input
266
+ ({ input }: { input: { invalid: true } }) => ({
267
+ output: Number(input.invalid),
268
+ }),
269
+ );
270
+ });
271
+
272
+ it("procedure leaves do not expose .middleware", () => {
273
+ expect(
274
+ (implementer.ping as { middleware?: unknown }).middleware,
275
+ ).toBeUndefined();
276
+ // @ts-expect-error - procedure implementer leaves should not expose middleware
277
+ expect(implementer.ping.middleware).toBeUndefined();
278
+ expectTypeOf(implementer.nested).toHaveProperty("middleware");
279
+ });
280
+ });
@@ -76,3 +76,5 @@ export const streamed = new ContractProcedure<
76
76
  inputSchema,
77
77
  outputSchema: streamedOutputSchema,
78
78
  });
79
+
80
+ export type AssertExtends<_TActual extends TExpected, TExpected> = true;