effect-orpc 0.1.3 → 0.2.0

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,222 @@
1
+ import { ContractBuilder, isContractProcedure } from "@orpc/contract";
2
+ import { describe, expect, expectTypeOf, it } from "vitest";
3
+
4
+ import { effectErrorMapToErrorMap, eoc } from "../index";
5
+ import {
6
+ baseErrorMap,
7
+ baseMeta,
8
+ baseRoute,
9
+ generalSchema,
10
+ inputSchema,
11
+ outputSchema,
12
+ ping,
13
+ pong,
14
+ } from "./shared";
15
+
16
+ const builder = eoc
17
+ .$meta(baseMeta)
18
+ .$route(baseRoute)
19
+ .$input(inputSchema)
20
+ .errors(baseErrorMap);
21
+
22
+ describe("parity: @orpc/contract builder.test.ts", () => {
23
+ it("is a contract procedure", () => {
24
+ expect(eoc).toSatisfy(isContractProcedure);
25
+ });
26
+
27
+ it(".$meta", () => {
28
+ const meta = { dev: true, log: true };
29
+ const applied = builder.$meta(meta);
30
+
31
+ expect(applied).toBeInstanceOf(ContractBuilder);
32
+ expect(applied).not.toBe(builder);
33
+ expect(applied["~orpc"]).toEqual({
34
+ ...builder["~orpc"],
35
+ meta,
36
+ });
37
+ });
38
+
39
+ it(".$route", () => {
40
+ const route = { method: "GET", path: "/api" } as const;
41
+ const applied = builder.$route(route);
42
+
43
+ expect(applied).toBeInstanceOf(ContractBuilder);
44
+ expect(applied).not.toBe(builder);
45
+ expect(applied["~orpc"]).toEqual({
46
+ ...builder["~orpc"],
47
+ route,
48
+ });
49
+ });
50
+
51
+ it(".$input", () => {
52
+ const applied = builder.$input(generalSchema);
53
+
54
+ expect(applied).toBeInstanceOf(ContractBuilder);
55
+ expect(applied).not.toBe(builder);
56
+ expect(applied["~orpc"]).toEqual({
57
+ ...builder["~orpc"],
58
+ inputSchema: generalSchema,
59
+ });
60
+ });
61
+
62
+ it(".errors", () => {
63
+ const errors = {
64
+ BAD_GATEWAY: { data: outputSchema },
65
+ OVERRIDE: { message: "override" },
66
+ } as const;
67
+ const applied = builder.errors(errors);
68
+
69
+ expect(applied).toBeInstanceOf(ContractBuilder);
70
+ expect(applied).not.toBe(builder);
71
+ expect(applied["~orpc"]).toEqual({
72
+ ...builder["~orpc"],
73
+ errorMap: effectErrorMapToErrorMap({
74
+ ...baseErrorMap,
75
+ ...errors,
76
+ }),
77
+ });
78
+ });
79
+
80
+ it(".meta", () => {
81
+ const meta = { dev: true, log: true };
82
+ const applied = builder.meta(meta);
83
+
84
+ expect(applied).toBeInstanceOf(ContractBuilder);
85
+ expect(applied).not.toBe(builder);
86
+ expect(applied["~orpc"]).toEqual({
87
+ ...builder["~orpc"],
88
+ meta: { ...builder["~orpc"].meta, ...meta },
89
+ });
90
+ });
91
+
92
+ it(".route", () => {
93
+ const route = { method: "GET", path: "/path" } as const;
94
+ const applied = builder.route(route);
95
+
96
+ expect(applied).toBeInstanceOf(ContractBuilder);
97
+ expect(applied).not.toBe(builder);
98
+ expect(applied["~orpc"]).toEqual({
99
+ ...builder["~orpc"],
100
+ route: { ...builder["~orpc"].route, ...route },
101
+ });
102
+ });
103
+
104
+ it(".input", () => {
105
+ const applied = builder.input(generalSchema);
106
+
107
+ expect(applied).toBeInstanceOf(ContractBuilder);
108
+ expect(applied).not.toBe(builder);
109
+ expect(applied["~orpc"]).toEqual({
110
+ ...builder["~orpc"],
111
+ inputSchema: generalSchema,
112
+ });
113
+ });
114
+
115
+ it(".output", () => {
116
+ const applied = builder.output(generalSchema);
117
+
118
+ expect(applied).toBeInstanceOf(ContractBuilder);
119
+ expect(applied).not.toBe(builder);
120
+ expect(applied["~orpc"]).toEqual({
121
+ ...builder["~orpc"],
122
+ outputSchema: generalSchema,
123
+ });
124
+ });
125
+
126
+ it(".prefix", () => {
127
+ const applied = builder.prefix("/api") as any;
128
+
129
+ expect(applied).toBeInstanceOf(ContractBuilder);
130
+ expect(applied).not.toBe(builder);
131
+ expect(applied["~orpc"]).toEqual({
132
+ ...builder["~orpc"],
133
+ prefix: "/api",
134
+ });
135
+ });
136
+
137
+ it(".tag", () => {
138
+ const applied = builder.tag("tag1", "tag2") as any;
139
+
140
+ expect(applied).toBeInstanceOf(ContractBuilder);
141
+ expect(applied).not.toBe(builder);
142
+ expect(applied["~orpc"]).toEqual({
143
+ ...builder["~orpc"],
144
+ tags: ["tag1", "tag2"],
145
+ });
146
+ });
147
+
148
+ it(".router", () => {
149
+ const router = builder.router({ ping, pong });
150
+
151
+ expect(router.ping["~orpc"].route.path).toBe("/base");
152
+ expect(router.ping["~orpc"].meta).toEqual(baseMeta);
153
+ expect(router.ping["~orpc"].errorMap).toEqual(baseErrorMap);
154
+ });
155
+ });
156
+
157
+ describe("parity: @orpc/contract builder.test-d.ts", () => {
158
+ const typedBuilder = builder;
159
+
160
+ it("is a contract procedure", () => {
161
+ expectTypeOf(typedBuilder["~orpc"]).toBeObject();
162
+ });
163
+
164
+ it("preserves ContractBuilder typing for root methods", () => {
165
+ expectTypeOf(typedBuilder.$meta<{ auth?: boolean }>({})).toBeObject();
166
+
167
+ expectTypeOf(typedBuilder.$route({ method: "GET", path: "/api" })).toExtend<
168
+ typeof typedBuilder
169
+ >();
170
+
171
+ expectTypeOf(typedBuilder.$input(generalSchema)).toBeObject();
172
+
173
+ expectTypeOf(
174
+ typedBuilder.errors({
175
+ INVALID: { message: "invalid" },
176
+ OVERRIDE: { message: "override" },
177
+ }),
178
+ ).toBeObject();
179
+
180
+ expectTypeOf(typedBuilder.meta({ log: true })).toBeObject();
181
+ expectTypeOf(typedBuilder.route({ method: "GET" })).toBeObject();
182
+ expectTypeOf(typedBuilder.input(generalSchema)).toBeObject();
183
+ expectTypeOf(typedBuilder.output(generalSchema)).toBeObject();
184
+ expectTypeOf(typedBuilder.prefix("/api")).toBeObject();
185
+ expectTypeOf(typedBuilder.tag("tag1", "tag2")).toBeObject();
186
+ expectTypeOf(typedBuilder.router({ ping, pong })).toExtend<{
187
+ ping: typeof ping;
188
+ pong: typeof pong;
189
+ }>();
190
+ });
191
+
192
+ it("preserves ContractBuilder constraints", () => {
193
+ // @ts-expect-error - initial meta is required
194
+ typedBuilder.$meta<{ auth?: boolean }>();
195
+ // @ts-expect-error - auth is missing in initial meta
196
+ typedBuilder.$meta<{ auth: boolean }>({});
197
+ // @ts-expect-error - invalid method
198
+ typedBuilder.$route({ method: "INVALID" });
199
+ // @ts-expect-error - invalid schema
200
+ typedBuilder.$input({});
201
+ // @ts-expect-error - schema is invalid
202
+ typedBuilder.errors({ TOO_MANY_REQUESTS: { data: {} } });
203
+ // @ts-expect-error - invalid meta
204
+ typedBuilder.meta({ meta: "INVALID" });
205
+ // @ts-expect-error - invalid method
206
+ typedBuilder.route({ method: "INVALID" });
207
+ // @ts-expect-error - invalid schema
208
+ typedBuilder.input({});
209
+ // @ts-expect-error - invalid schema
210
+ typedBuilder.output({});
211
+ // @ts-expect-error - invalid prefix
212
+ typedBuilder.prefix(1);
213
+ // @ts-expect-error - invalid tag
214
+ typedBuilder.tag(1);
215
+ });
216
+
217
+ it("keeps eoc-specific tagged error normalization separate", () => {
218
+ const applied = eoc.errors(baseErrorMap);
219
+
220
+ expectTypeOf(applied["~orpc"].errorMap).toBeObject();
221
+ });
222
+ });
@@ -0,0 +1,193 @@
1
+ import type { ContractProcedure, Schema } from "@orpc/contract";
2
+ import type {
3
+ DecoratedMiddleware,
4
+ Middleware,
5
+ MiddlewareOutputFn,
6
+ } from "@orpc/server";
7
+ import type { OmitChainMethodDeep } from "@orpc/shared";
8
+ import { describe, expectTypeOf, it } from "vitest";
9
+ import z from "zod";
10
+
11
+ import {
12
+ EffectBuilder,
13
+ EffectDecoratedProcedure,
14
+ makeEffectORPC,
15
+ } from "../index";
16
+ import type { CurrentContext, InitialContext } from "./parity-shared";
17
+ import { runtime } from "./parity-shared";
18
+ import type { AssertExtends } from "./shared";
19
+ import {
20
+ baseErrorMap,
21
+ baseMeta,
22
+ type BaseMeta,
23
+ inputSchema,
24
+ outputSchema,
25
+ } from "./shared";
26
+
27
+ const typedBuilder = {} as EffectBuilder<
28
+ InitialContext,
29
+ CurrentContext,
30
+ typeof inputSchema,
31
+ typeof outputSchema,
32
+ typeof baseErrorMap,
33
+ BaseMeta,
34
+ never,
35
+ never
36
+ >;
37
+
38
+ const rootBuilder = makeEffectORPC(runtime)
39
+ .$context<InitialContext>()
40
+ .$meta(baseMeta)
41
+ .$input(inputSchema)
42
+ .errors(baseErrorMap);
43
+
44
+ const withMiddlewares = rootBuilder.use(({ next }) =>
45
+ next({ context: { auth: true as boolean } }),
46
+ );
47
+
48
+ const procedureBuilder = withMiddlewares.meta(baseMeta);
49
+ const withInput = procedureBuilder.input(inputSchema);
50
+ const withOutput = procedureBuilder.output(outputSchema);
51
+ const withInputOutput = withInput.output(outputSchema);
52
+
53
+ describe("parity: @orpc/server builder.test-d.ts", () => {
54
+ it("is a contract procedure", () => {
55
+ expectTypeOf<
56
+ AssertExtends<
57
+ typeof typedBuilder,
58
+ ContractProcedure<
59
+ typeof inputSchema,
60
+ typeof outputSchema,
61
+ typeof baseErrorMap,
62
+ BaseMeta
63
+ >
64
+ >
65
+ >().toEqualTypeOf<true>();
66
+ });
67
+
68
+ it(".$config / .$context / .$meta / .$route / .$input", () => {
69
+ expectTypeOf(
70
+ rootBuilder.$config({
71
+ initialInputValidationIndex: Number.NEGATIVE_INFINITY,
72
+ initialOutputValidationIndex: Number.POSITIVE_INFINITY,
73
+ }),
74
+ ).toBeObject();
75
+
76
+ expectTypeOf(rootBuilder.$context()).toBeObject();
77
+ expectTypeOf(rootBuilder.$context<{ anything: string }>()).toBeObject();
78
+ expectTypeOf(rootBuilder.$meta<{ auth?: boolean }>({})).toBeObject();
79
+ expectTypeOf(rootBuilder.$route({ method: "GET" })).toBeObject();
80
+
81
+ const schema = z.void();
82
+ expectTypeOf(rootBuilder.$input(schema)).toBeObject();
83
+ expectTypeOf(rootBuilder.$input<Schema<void, unknown>>()).toBeObject();
84
+
85
+ rootBuilder.$config({
86
+ // @ts-expect-error - must be number
87
+ initialInputValidationIndex: "INVALID",
88
+ });
89
+ // @ts-expect-error - initial meta is required
90
+ rootBuilder.$meta<{ auth?: boolean }>();
91
+ // @ts-expect-error - invalid method
92
+ rootBuilder.$route({ method: "INVALID" });
93
+ // @ts-expect-error - invalid schema
94
+ rootBuilder.$input<"invalid">();
95
+ });
96
+
97
+ it(".middleware", () => {
98
+ expectTypeOf(
99
+ rootBuilder.middleware(
100
+ ({ next }, input: "input", output: MiddlewareOutputFn<"output">) => {
101
+ expectTypeOf(input).toEqualTypeOf<"input">();
102
+ expectTypeOf(output).toEqualTypeOf<MiddlewareOutputFn<"output">>();
103
+ return next({ context: { extra: true } });
104
+ },
105
+ ),
106
+ ).toExtend<
107
+ DecoratedMiddleware<
108
+ InitialContext,
109
+ { extra: boolean },
110
+ "input",
111
+ "output",
112
+ any,
113
+ BaseMeta
114
+ >
115
+ >();
116
+ });
117
+
118
+ it(".errors / .use / .meta / .route / .input / .output", () => {
119
+ expectTypeOf(
120
+ rootBuilder.errors({ BAD_GATEWAY: { message: "BAD" } }),
121
+ ).toBeObject();
122
+ expectTypeOf(withMiddlewares.use).toBeFunction();
123
+ expectTypeOf(withMiddlewares.meta).toBeFunction();
124
+ expectTypeOf(withMiddlewares.route).toBeFunction();
125
+ expectTypeOf(withMiddlewares.input).toBeFunction();
126
+ expectTypeOf(withMiddlewares.output).toBeFunction();
127
+
128
+ const mid = {} as Middleware<
129
+ { cacheable?: boolean } & Record<never, never>,
130
+ Record<never, never>,
131
+ unknown,
132
+ unknown,
133
+ any,
134
+ BaseMeta
135
+ >;
136
+ expectTypeOf(withMiddlewares.use(mid)).toBeObject();
137
+ });
138
+
139
+ it(".handler / .effect / .prefix / .tag / .router / .lazy", () => {
140
+ expectTypeOf(withOutput.handler(() => ({ output: 456 }))).toExtend<
141
+ EffectDecoratedProcedure<
142
+ InitialContext & Record<never, never>,
143
+ CurrentContext,
144
+ typeof inputSchema,
145
+ typeof outputSchema,
146
+ typeof baseErrorMap,
147
+ BaseMeta,
148
+ never,
149
+ never
150
+ >
151
+ >();
152
+
153
+ expectTypeOf(
154
+ withOutput.effect(function* () {
155
+ return { output: 456 };
156
+ }),
157
+ ).toExtend<
158
+ EffectDecoratedProcedure<
159
+ InitialContext & Record<never, never>,
160
+ CurrentContext,
161
+ typeof inputSchema,
162
+ typeof outputSchema,
163
+ typeof baseErrorMap,
164
+ BaseMeta,
165
+ never,
166
+ never
167
+ >
168
+ >();
169
+
170
+ expectTypeOf(rootBuilder.prefix("/api")).toBeObject();
171
+ expectTypeOf(rootBuilder.tag("tag1", "tag2")).toBeObject();
172
+ });
173
+ });
174
+
175
+ describe("parity: @orpc/server builder-variants.test-d.ts", () => {
176
+ it("keeps variant method surface aligned with upstream", () => {
177
+ void ({} as OmitChainMethodDeep<
178
+ typeof typedBuilder,
179
+ "$config" | "$context" | "$meta" | "$route" | "$input" | "middleware"
180
+ >);
181
+
182
+ expectTypeOf(withMiddlewares.errors).toBeFunction();
183
+ expectTypeOf(withMiddlewares.use).toBeFunction();
184
+ expectTypeOf(withMiddlewares.handler).toBeFunction();
185
+ expectTypeOf(withMiddlewares.effect).toBeFunction();
186
+
187
+ expectTypeOf(withMiddlewares.handler).toBeFunction();
188
+ expectTypeOf(procedureBuilder.traced).toBeFunction();
189
+ expectTypeOf(withInput.output).toBeFunction();
190
+ expectTypeOf(withOutput.input).toBeFunction();
191
+ expectTypeOf(withInputOutput.effect).toBeFunction();
192
+ });
193
+ });
@@ -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
+ });