effect-orpc 0.0.6 → 0.0.7
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/README.md +175 -474
- package/dist/index.js +271 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/effect-builder.ts +367 -178
- package/src/effect-enhance-router.ts +114 -0
- package/src/effect-procedure.ts +95 -32
- package/src/index.ts +20 -7
- package/src/tagged-error.ts +148 -98
- package/src/tests/effect-builder.test.ts +52 -39
- package/src/tests/effect-error-map.test.ts +32 -30
- package/src/tests/effect-procedure.test.ts +12 -12
- package/src/tests/tagged-error.test.ts +84 -27
- package/src/types/index.ts +422 -0
- package/src/types/variants.ts +1327 -0
package/README.md
CHANGED
|
@@ -10,9 +10,8 @@ Inspired by [effect-trpc](https://github.com/mikearnaldi/effect-trpc).
|
|
|
10
10
|
- **Type-safe service injection** - Use `ManagedRuntime<R>` to provide services to procedures with compile-time safety
|
|
11
11
|
- **Tagged errors** - Create Effect-native error classes with `ORPCTaggedError` that integrate with oRPC's error handling
|
|
12
12
|
- **Full oRPC compatibility** - Mix Effect procedures with standard oRPC procedures in the same router
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **Server actions support** - Full compatibility with framework server actions
|
|
13
|
+
- **Telemetry support with automatic tracing** - Procedures are automatically traced with OpenTelemetry-compatible spans. Customize span names with `.traced()`.
|
|
14
|
+
- **Builder pattern preserved** - oRPC builder methods (`.errors()`, `.meta()`, `.route()`, `.input()`, `.output()`, `.use()`) work seamlessly
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
@@ -24,71 +23,68 @@ pnpm add effect-orpc
|
|
|
24
23
|
bun add effect-orpc
|
|
25
24
|
```
|
|
26
25
|
|
|
27
|
-
##
|
|
26
|
+
## Demo of the features
|
|
28
27
|
|
|
29
28
|
```ts
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { z } from 'zod'
|
|
29
|
+
import { os } from "@orpc/server";
|
|
30
|
+
import { Effect, ManagedRuntime } from "effect";
|
|
31
|
+
import { makeEffectORPC, ORPCTaggedError } from "effect-orpc";
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
findById: (id: string) => Effect.Effect<User | undefined>
|
|
40
|
-
findAll: () => Effect.Effect<User[]>
|
|
41
|
-
create: (name: string) => Effect.Effect<User>
|
|
42
|
-
}
|
|
43
|
-
>() {}
|
|
44
|
-
|
|
45
|
-
// Create service implementation
|
|
46
|
-
const UserServiceLive = Layer.succeed(UserService, {
|
|
47
|
-
findById: id => Effect.succeed(users.find(u => u.id === id)),
|
|
48
|
-
findAll: () => Effect.succeed(users),
|
|
49
|
-
create: name => Effect.succeed({ id: crypto.randomUUID(), name })
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
// Create runtime with your services
|
|
53
|
-
const runtime = ManagedRuntime.make(UserServiceLive)
|
|
54
|
-
|
|
55
|
-
// Create Effect-aware oRPC builder
|
|
56
|
-
const effectOs = makeEffectORPC(runtime)
|
|
57
|
-
|
|
58
|
-
// Define your procedures
|
|
59
|
-
const getUser = effectOs
|
|
60
|
-
.input(z.object({ id: z.string() }))
|
|
61
|
-
.effect(function* ({ input }) {
|
|
62
|
-
const userService = yield* UserService
|
|
63
|
-
return yield* userService.findById(input.id)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
const listUsers = effectOs.effect(function* () {
|
|
67
|
-
const userService = yield* UserService
|
|
68
|
-
return yield* userService.findAll()
|
|
69
|
-
})
|
|
33
|
+
interface User {
|
|
34
|
+
id: number;
|
|
35
|
+
name: string;
|
|
36
|
+
}
|
|
70
37
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
})
|
|
38
|
+
let users: User[] = [
|
|
39
|
+
{ id: 1, name: "John Doe" },
|
|
40
|
+
{ id: 2, name: "Jane Doe" },
|
|
41
|
+
{ id: 3, name: "James Dane" },
|
|
42
|
+
];
|
|
77
43
|
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
44
|
+
// Authenticated os with initial context & errors set
|
|
45
|
+
const authedOs = os
|
|
46
|
+
.errors({ UNAUTHORIZED: { status: 401 } })
|
|
47
|
+
.$context<{ userId?: number }>()
|
|
48
|
+
.use(({ context, errors, next }) => {
|
|
49
|
+
if (context.userId === undefined) throw errors.UNAUTHORIZED();
|
|
50
|
+
return next({ context: { ...context, userId: context.userId } });
|
|
51
|
+
});
|
|
82
52
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
})
|
|
53
|
+
// Define your services
|
|
54
|
+
class UsersRepo extends Effect.Service<UsersRepo>()("UserService", {
|
|
55
|
+
accessors: true,
|
|
56
|
+
sync: () => ({
|
|
57
|
+
get: (id: number) => users.find((u) => u.id === id),
|
|
58
|
+
}),
|
|
59
|
+
}) {}
|
|
60
|
+
|
|
61
|
+
// Special yieldable oRPC error class
|
|
62
|
+
class UserNotFoundError extends ORPCTaggedError()("UserNotFoundError", {
|
|
63
|
+
status: 404,
|
|
64
|
+
}) {}
|
|
90
65
|
|
|
91
|
-
|
|
66
|
+
// Create runtime with your services
|
|
67
|
+
const runtime = ManagedRuntime.make(UsersRepo.Default);
|
|
68
|
+
// Create Effect-aware oRPC builder from an other (optional) base oRPC builder
|
|
69
|
+
const effectOs = makeEffectORPC(runtime, authedOs).errors({
|
|
70
|
+
UserNotFoundError,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Create the router with mixed procedures
|
|
74
|
+
export const router = {
|
|
75
|
+
health: os.handler(() => "ok"),
|
|
76
|
+
users: {
|
|
77
|
+
me: effectOs.effect(function* ({ context: { userId } }) {
|
|
78
|
+
const user = yield* UsersRepo.get(userId);
|
|
79
|
+
if (!user) {
|
|
80
|
+
return yield* new UserNotFoundError();
|
|
81
|
+
}
|
|
82
|
+
return user;
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type Router = typeof router;
|
|
92
88
|
```
|
|
93
89
|
|
|
94
90
|
## Type Safety
|
|
@@ -96,397 +92,131 @@ export type Router = typeof router
|
|
|
96
92
|
The wrapper enforces that Effect procedures only use services provided by the `ManagedRuntime`. If you try to use a service that isn't in the runtime, you'll get a compile-time error:
|
|
97
93
|
|
|
98
94
|
```ts
|
|
99
|
-
|
|
95
|
+
import { Context, Effect, Layer, ManagedRuntime } from "effect";
|
|
96
|
+
import { makeEffectORPC } from "effect-orpc";
|
|
97
|
+
|
|
98
|
+
class ProvidedService extends Context.Tag("ProvidedService")<
|
|
100
99
|
ProvidedService,
|
|
101
100
|
{ doSomething: () => Effect.Effect<string> }
|
|
102
101
|
>() {}
|
|
103
102
|
|
|
104
|
-
class MissingService extends Context.Tag(
|
|
103
|
+
class MissingService extends Context.Tag("MissingService")<
|
|
105
104
|
MissingService,
|
|
106
105
|
{ doSomething: () => Effect.Effect<string> }
|
|
107
106
|
>() {}
|
|
108
107
|
|
|
109
|
-
const runtime = ManagedRuntime.make(
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
const runtime = ManagedRuntime.make(
|
|
109
|
+
Layer.succeed(ProvidedService, {
|
|
110
|
+
doSomething: () => Effect.succeed("ok"),
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
112
113
|
|
|
113
|
-
const effectOs = makeEffectORPC(runtime)
|
|
114
|
+
const effectOs = makeEffectORPC(runtime);
|
|
114
115
|
|
|
115
116
|
// ✅ This compiles - ProvidedService is in the runtime
|
|
116
117
|
const works = effectOs.effect(function* () {
|
|
117
|
-
const service = yield* ProvidedService
|
|
118
|
-
return yield* service.doSomething()
|
|
119
|
-
})
|
|
118
|
+
const service = yield* ProvidedService;
|
|
119
|
+
return yield* service.doSomething();
|
|
120
|
+
});
|
|
120
121
|
|
|
121
122
|
// ❌ This fails to compile - MissingService is not in the runtime
|
|
122
123
|
const fails = effectOs.effect(function* () {
|
|
123
|
-
const service = yield* MissingService // Type error!
|
|
124
|
-
return yield* service.doSomething()
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Wrapping a Customized Builder
|
|
129
|
-
|
|
130
|
-
You can pass a customized oRPC builder as the second argument to inherit middleware, errors, and configuration:
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
import { makeEffectORPC } from 'effect-orpc'
|
|
134
|
-
import { ORPCError, os } from '@orpc/server'
|
|
135
|
-
import { Effect } from 'effect'
|
|
136
|
-
|
|
137
|
-
// Create a customized base builder with auth middleware
|
|
138
|
-
const authedOs = os
|
|
139
|
-
.errors({
|
|
140
|
-
UNAUTHORIZED: { message: 'Not authenticated' },
|
|
141
|
-
FORBIDDEN: { message: 'Access denied' },
|
|
142
|
-
})
|
|
143
|
-
.use(async ({ context, next, errors }) => {
|
|
144
|
-
if (!context.user) {
|
|
145
|
-
throw errors.UNAUTHORIZED()
|
|
146
|
-
}
|
|
147
|
-
return next({ context: { ...context, userId: context.user.id } })
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
// Wrap the customized builder with Effect support
|
|
151
|
-
const effectAuthedOs = makeEffectORPC(runtime, authedOs)
|
|
152
|
-
|
|
153
|
-
// All procedures inherit the auth middleware and error definitions
|
|
154
|
-
const getProfile = effectAuthedOs.effect(function* ({ context }) {
|
|
155
|
-
const userService = yield* UserService
|
|
156
|
-
return yield* userService.findById(context.userId)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const updateProfile = effectAuthedOs
|
|
160
|
-
.input(z.object({ name: z.string() }))
|
|
161
|
-
.effect(function* ({ context, input }) {
|
|
162
|
-
const userService = yield* UserService
|
|
163
|
-
return yield* userService.update(context.userId, input)
|
|
164
|
-
})
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Chaining Builder Methods
|
|
168
|
-
|
|
169
|
-
The `EffectBuilder` supports all standard oRPC builder methods:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
const createPost = effectOs
|
|
173
|
-
// Add custom errors
|
|
174
|
-
.errors({
|
|
175
|
-
NOT_FOUND: { message: 'User not found' },
|
|
176
|
-
VALIDATION_ERROR: {
|
|
177
|
-
message: 'Invalid input',
|
|
178
|
-
data: z.object({ field: z.string(), issue: z.string() })
|
|
179
|
-
},
|
|
180
|
-
})
|
|
181
|
-
// Add metadata
|
|
182
|
-
.meta({ auth: true, rateLimit: 100 })
|
|
183
|
-
// Configure route for OpenAPI
|
|
184
|
-
.route({ method: 'POST', path: '/posts', tags: ['posts'] })
|
|
185
|
-
// Define input schema
|
|
186
|
-
.input(z.object({
|
|
187
|
-
title: z.string().min(1).max(200),
|
|
188
|
-
content: z.string(),
|
|
189
|
-
authorId: z.string(),
|
|
190
|
-
}))
|
|
191
|
-
// Define output schema
|
|
192
|
-
.output(z.object({
|
|
193
|
-
id: z.string(),
|
|
194
|
-
title: z.string(),
|
|
195
|
-
content: z.string(),
|
|
196
|
-
createdAt: z.date(),
|
|
197
|
-
}))
|
|
198
|
-
// Define Effect handler
|
|
199
|
-
.effect(function* ({ input, errors }) {
|
|
200
|
-
const userService = yield* UserService
|
|
201
|
-
const user = yield* userService.findById(input.authorId)
|
|
202
|
-
|
|
203
|
-
if (!user) {
|
|
204
|
-
return yield Effect.Fail(errors.NOT_FOUND())
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const postService = yield* PostService
|
|
208
|
-
return yield* postService.create({
|
|
209
|
-
title: input.title,
|
|
210
|
-
content: input.content,
|
|
211
|
-
authorId: input.authorId,
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Making Procedures Callable
|
|
217
|
-
|
|
218
|
-
Use `.callable()` to make procedures directly invocable:
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
const greet = effectOs
|
|
222
|
-
.input(z.object({ name: z.string() }))
|
|
223
|
-
.effect(function* ({ input }) {
|
|
224
|
-
return `Hello, ${input.name}!`
|
|
225
|
-
})
|
|
226
|
-
.callable()
|
|
227
|
-
|
|
228
|
-
// Can be called directly as a function
|
|
229
|
-
const result = await greet({ name: 'World' })
|
|
230
|
-
// => "Hello, World!"
|
|
231
|
-
|
|
232
|
-
// Still a valid procedure for routers
|
|
233
|
-
const router = os.router({ greet })
|
|
124
|
+
const service = yield* MissingService; // Type error!
|
|
125
|
+
return yield* service.doSomething();
|
|
126
|
+
});
|
|
234
127
|
```
|
|
235
128
|
|
|
236
|
-
##
|
|
237
|
-
|
|
238
|
-
Use `.actionable()` for framework server actions (Next.js, etc.):
|
|
129
|
+
## Error Handling
|
|
239
130
|
|
|
240
|
-
|
|
241
|
-
const createTodo = effectOs
|
|
242
|
-
.input(z.object({ title: z.string() }))
|
|
243
|
-
.effect(function* ({ input }) {
|
|
244
|
-
const todoService = yield* TodoService
|
|
245
|
-
return yield* todoService.create(input.title)
|
|
246
|
-
})
|
|
247
|
-
.actionable({ context: async () => ({ user: await getSession() }) })
|
|
248
|
-
|
|
249
|
-
// Use in React Server Components
|
|
250
|
-
export async function TodoForm() {
|
|
251
|
-
return (
|
|
252
|
-
<form action={createTodo}>
|
|
253
|
-
<input name="title" />
|
|
254
|
-
<button type="submit">Add Todo</button>
|
|
255
|
-
</form>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
```
|
|
131
|
+
`ORPCTaggedError` lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:
|
|
259
132
|
|
|
260
|
-
|
|
133
|
+
- Can be yielded in Effect generators (`yield* new MyError()` or `yield* Effect.fail(errors.MyError)`)
|
|
134
|
+
- Can be used in Effect builder's `.errors()` maps for type-safe error handling alongside regular oRPC errors
|
|
135
|
+
- Automatically convert to ORPCError when thrown
|
|
261
136
|
|
|
262
|
-
|
|
137
|
+
Make sure the tagged error class is passed to the effect `.errors()` to be able to yield the error class directly and make the client recognize it as defined.
|
|
263
138
|
|
|
264
139
|
```ts
|
|
265
140
|
const getUser = effectOs
|
|
141
|
+
// Mixed error maps
|
|
266
142
|
.errors({
|
|
143
|
+
// Regular oRPC error
|
|
267
144
|
NOT_FOUND: {
|
|
268
|
-
message:
|
|
269
|
-
data: z.object({ id: z.string() })
|
|
145
|
+
message: "User not found",
|
|
146
|
+
data: z.object({ id: z.string() }),
|
|
270
147
|
},
|
|
148
|
+
// Effect oRPC tagged error
|
|
149
|
+
UserNotFoundError,
|
|
150
|
+
// Note: The key of an oRPC error is not used as the error code
|
|
151
|
+
// So the following will only change the key of the error when accessing it
|
|
152
|
+
// from the errors object passed to the handler, but not the actual error code itself.
|
|
153
|
+
// To change the error's code, please see the next section on creating tagged errors.
|
|
154
|
+
USER_NOT_FOUND: UserNotFoundError,
|
|
155
|
+
// ^^^ same code as the `UserNotFoundError` error key, defined at the class level
|
|
271
156
|
})
|
|
272
|
-
.input(z.object({ id: z.string() }))
|
|
273
157
|
.effect(function* ({ input, errors }) {
|
|
274
|
-
const
|
|
275
|
-
const user = yield* userService.findById(input.id)
|
|
276
|
-
|
|
158
|
+
const user = yield* UsersRepo.findById(input.id);
|
|
277
159
|
if (!user) {
|
|
278
|
-
|
|
279
|
-
yield* Effect.fail(errors.
|
|
160
|
+
return yield* new UserNotFoundError();
|
|
161
|
+
// or return `yield* Effect.fail(errors.USER_NOT_FOUND())`
|
|
280
162
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
})
|
|
163
|
+
return user;
|
|
164
|
+
});
|
|
284
165
|
```
|
|
285
166
|
|
|
286
|
-
## Tagged Errors
|
|
287
|
-
|
|
288
|
-
`ORPCTaggedError` lets you create Effect-native error classes that integrate seamlessly with oRPC. These errors:
|
|
289
|
-
|
|
290
|
-
- Can be yielded in Effect generators (`yield* new MyError()` or `yield* Effect.fail(errors.MyError)`)
|
|
291
|
-
- Have all ORPCError properties (code, status, data, defined)
|
|
292
|
-
- Can be used in `.errors()` maps for type-safe error handling
|
|
293
|
-
- Automatically convert to ORPCError when thrown
|
|
294
|
-
|
|
295
167
|
### Creating Tagged Errors
|
|
296
168
|
|
|
297
169
|
```ts
|
|
298
|
-
import { ORPCTaggedError } from
|
|
170
|
+
import { ORPCTaggedError } from "effect-orpc";
|
|
299
171
|
|
|
300
172
|
// Basic tagged error - code defaults to 'USER_NOT_FOUND' (CONSTANT_CASE of tag)
|
|
301
|
-
class UserNotFound extends ORPCTaggedError
|
|
173
|
+
class UserNotFound extends ORPCTaggedError()("UserNotFound") {}
|
|
302
174
|
|
|
303
175
|
// With explicit code
|
|
304
|
-
class NotFound extends ORPCTaggedError
|
|
176
|
+
class NotFound extends ORPCTaggedError()("NotFound", "NOT_FOUND") {}
|
|
305
177
|
|
|
306
178
|
// With default options (code defaults to 'VALIDATION_ERROR') (CONSTANT_CASE of tag)
|
|
307
|
-
class ValidationError extends ORPCTaggedError
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
) {}
|
|
179
|
+
class ValidationError extends ORPCTaggedError()("ValidationError", {
|
|
180
|
+
status: 400,
|
|
181
|
+
message: "Validation failed",
|
|
182
|
+
}) {}
|
|
311
183
|
|
|
312
184
|
// With explicit code and options
|
|
313
|
-
class Forbidden extends ORPCTaggedError
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
{ userId: string }
|
|
323
|
-
>()('UserNotFoundWithData') {}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Using Tagged Errors in Procedures
|
|
327
|
-
|
|
328
|
-
Tagged errors can be yielded directly in Effect generators:
|
|
329
|
-
|
|
330
|
-
```ts
|
|
331
|
-
class UserNotFound extends ORPCTaggedError<
|
|
332
|
-
UserNotFound,
|
|
333
|
-
{ userId: string }
|
|
334
|
-
>()('UserNotFound', { status: 404, message: 'User not found' })
|
|
335
|
-
|
|
336
|
-
const getUser = effectOs
|
|
337
|
-
.input(z.object({ id: z.string() }))
|
|
338
|
-
.effect(function* ({ input }) {
|
|
339
|
-
const userService = yield* UserService
|
|
340
|
-
const user = yield* userService.findById(input.id)
|
|
341
|
-
|
|
342
|
-
if (!user) {
|
|
343
|
-
// Yield the error - it will be converted to ORPCError automatically
|
|
344
|
-
return yield* new UserNotFound({ data: { userId: input.id } })
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return user
|
|
348
|
-
})
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Using Tagged Errors in Error Maps
|
|
352
|
-
|
|
353
|
-
Tagged error classes can be passed directly to `.errors()`:
|
|
354
|
-
|
|
355
|
-
```ts
|
|
356
|
-
class UserNotFound extends ORPCTaggedError<
|
|
357
|
-
UserNotFound,
|
|
358
|
-
{ userId: string }
|
|
359
|
-
>()('UserNotFound', 'NOT_FOUND', { status: 404, message: 'User not found' })
|
|
360
|
-
|
|
361
|
-
class InvalidInput extends ORPCTaggedError<
|
|
362
|
-
InvalidInput,
|
|
363
|
-
{ field: string }
|
|
364
|
-
>()('InvalidInput', 'BAD_REQUEST', { status: 400 })
|
|
365
|
-
|
|
366
|
-
const getUser = effectOs
|
|
367
|
-
.errors({
|
|
368
|
-
// Tagged error class - use the class directly
|
|
369
|
-
// The only difference is that the code is defined by the constant version of the tag
|
|
370
|
-
// Or when defined explicitely like in the Forbidden tagged error above
|
|
371
|
-
UserNotFound,
|
|
372
|
-
INVALID_INPUT: InvalidInput,
|
|
373
|
-
// Traditional format still works, and can be colocated
|
|
374
|
-
INTERNAL_ERROR: { status: 500, message: 'Something went wrong' },
|
|
375
|
-
})
|
|
376
|
-
.input(z.object({ id: z.string() }))
|
|
377
|
-
.effect(function* ({ input, errors }) {
|
|
378
|
-
if (!input.id) {
|
|
379
|
-
// errors.BAD_REQUEST is the InvalidInput class
|
|
380
|
-
return yield* new errors.INVALID_INPUT({ data: { field: 'id' } })
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const userService = yield* UserService
|
|
384
|
-
const user = yield* userService.findById(input.id)
|
|
385
|
-
|
|
386
|
-
if (!user) {
|
|
387
|
-
// errors.UserNotFound is the UserNotFound class
|
|
388
|
-
// with the code USER_NOT_FOUND (defined at the class level)
|
|
389
|
-
return yield* new errors.UserNotFound({ data: { userId: input.id } })
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return user
|
|
393
|
-
})
|
|
185
|
+
class Forbidden extends ORPCTaggedError()("Forbidden", "FORBIDDEN", {
|
|
186
|
+
status: 403,
|
|
187
|
+
message: "Access denied",
|
|
188
|
+
}) {}
|
|
189
|
+
|
|
190
|
+
// With typed data using Standard Schema
|
|
191
|
+
class UserNotFoundWithData extends ORPCTaggedError(
|
|
192
|
+
z.object({ userId: z.string() }),
|
|
193
|
+
)("UserNotFoundWithData") {}
|
|
394
194
|
```
|
|
395
195
|
|
|
396
|
-
### Converting Tagged Errors
|
|
397
|
-
|
|
398
|
-
Use `toORPCError` to convert a tagged error to a plain ORPCError:
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
import { toORPCError, ORPCTaggedError } from 'effect-orpc'
|
|
402
|
-
import { Effect } from 'effect'
|
|
403
|
-
|
|
404
|
-
class MyError extends ORPCTaggedError<MyError>()('MyError', 'BAD_REQUEST') {}
|
|
405
|
-
|
|
406
|
-
const procedure = effectOs.effect(function* () {
|
|
407
|
-
const result = yield* someOperation.pipe(
|
|
408
|
-
Effect.catchTag('MyError', (e) =>
|
|
409
|
-
// Convert to plain ORPCError if needed
|
|
410
|
-
Effect.fail(toORPCError(e))
|
|
411
|
-
)
|
|
412
|
-
)
|
|
413
|
-
return result
|
|
414
|
-
})
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Tagged Error Properties
|
|
418
|
-
|
|
419
|
-
Tagged errors have all the properties of ORPCError plus Effect integration:
|
|
420
|
-
|
|
421
|
-
```ts
|
|
422
|
-
const error = new UserNotFound({
|
|
423
|
-
data: { userId: '123' },
|
|
424
|
-
message: 'Custom message', // Override default message
|
|
425
|
-
cause: originalError, // Attach cause for debugging
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
error._tag // 'UserNotFound' - for Effect's catchTag
|
|
429
|
-
error.code // 'USER_NOT_FOUND' - ORPCError code
|
|
430
|
-
error.status // 404 - HTTP status
|
|
431
|
-
error.data // { userId: '123' } - typed data
|
|
432
|
-
error.message // 'Custom message'
|
|
433
|
-
error.defined // true - whether error is defined in error map
|
|
434
|
-
|
|
435
|
-
// Convert to plain ORPCError
|
|
436
|
-
const orpcError = error.toORPCError()
|
|
437
|
-
|
|
438
|
-
// Serialize to JSON
|
|
439
|
-
const json = error.toJSON()
|
|
440
|
-
// { _tag: 'UserNotFound', code: 'USER_NOT_FOUND', status: 404, ... }
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
## Generator Syntax
|
|
444
|
-
|
|
445
|
-
Pass a generator function directly to `.effect()` — no need to wrap it with `Effect.fn()` or `Effect.gen()`:
|
|
446
|
-
|
|
447
|
-
```ts
|
|
448
|
-
// Recommended: Pass generator function directly
|
|
449
|
-
const procedureWithGen = effectOs
|
|
450
|
-
.input(z.object({ id: z.string() }))
|
|
451
|
-
.effect(function* ({ input }) {
|
|
452
|
-
const service = yield* MyService
|
|
453
|
-
return yield* service.doSomething(input.id)
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
// Simple procedures without yield*
|
|
457
|
-
const simpleProcedure = effectOs
|
|
458
|
-
.input(z.object({ name: z.string() }))
|
|
459
|
-
.effect(function* ({ input }) {
|
|
460
|
-
return `Hello, ${input.name}!`
|
|
461
|
-
})
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
The handler receives `{ context, input, path, procedure, signal, lastEventId, errors }` as its argument, giving you full access to the oRPC procedure context.
|
|
465
|
-
|
|
466
196
|
## Traceable Spans
|
|
467
197
|
|
|
468
198
|
All Effect procedures are automatically traced with `Effect.withSpan`. By default, the span name is the procedure path (e.g., `users.getUser`):
|
|
469
199
|
|
|
470
200
|
```ts
|
|
471
201
|
// Router structure determines span names automatically
|
|
472
|
-
const router =
|
|
473
|
-
users:
|
|
202
|
+
const router = {
|
|
203
|
+
users: {
|
|
474
204
|
// Span name: "users.get"
|
|
475
|
-
get: effectOs
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
205
|
+
get: effectOs.input(z.object({ id: z.string() })).effect(function* ({
|
|
206
|
+
input,
|
|
207
|
+
}) {
|
|
208
|
+
const userService = yield* UserService;
|
|
209
|
+
return yield* userService.findById(input.id);
|
|
210
|
+
}),
|
|
481
211
|
// Span name: "users.create"
|
|
482
|
-
create: effectOs
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
}
|
|
212
|
+
create: effectOs.input(z.object({ name: z.string() })).effect(function* ({
|
|
213
|
+
input,
|
|
214
|
+
}) {
|
|
215
|
+
const userService = yield* UserService;
|
|
216
|
+
return yield* userService.create(input.name);
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
490
220
|
```
|
|
491
221
|
|
|
492
222
|
Use `.traced()` to override the default span name:
|
|
@@ -494,11 +224,11 @@ Use `.traced()` to override the default span name:
|
|
|
494
224
|
```ts
|
|
495
225
|
const getUser = effectOs
|
|
496
226
|
.input(z.object({ id: z.string() }))
|
|
497
|
-
.traced(
|
|
227
|
+
.traced("custom.span.name") // Override the default path-based name
|
|
498
228
|
.effect(function* ({ input }) {
|
|
499
|
-
const userService = yield* UserService
|
|
500
|
-
return yield* userService.findById(input.id)
|
|
501
|
-
})
|
|
229
|
+
const userService = yield* UserService;
|
|
230
|
+
return yield* userService.findById(input.id);
|
|
231
|
+
});
|
|
502
232
|
```
|
|
503
233
|
|
|
504
234
|
### Enabling OpenTelemetry
|
|
@@ -506,22 +236,21 @@ const getUser = effectOs
|
|
|
506
236
|
To enable tracing, include the OpenTelemetry layer in your runtime:
|
|
507
237
|
|
|
508
238
|
```ts
|
|
509
|
-
import { NodeSdk } from
|
|
510
|
-
import { OTLPTraceExporter } from
|
|
511
|
-
import { SimpleSpanProcessor } from
|
|
512
|
-
|
|
513
|
-
const TracingLive = NodeSdk.layer(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
const effectOs = makeEffectORPC(runtime)
|
|
239
|
+
import { NodeSdk } from "@effect/opentelemetry";
|
|
240
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
241
|
+
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
242
|
+
|
|
243
|
+
const TracingLive = NodeSdk.layer(
|
|
244
|
+
Effect.sync(() => ({
|
|
245
|
+
resource: { serviceName: "my-service" },
|
|
246
|
+
spanProcessor: [new SimpleSpanProcessor(new OTLPTraceExporter())],
|
|
247
|
+
})),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const AppLive = Layer.mergeAll(UserServiceLive, TracingLive);
|
|
251
|
+
|
|
252
|
+
const runtime = ManagedRuntime.make(AppLive);
|
|
253
|
+
const effectOs = makeEffectORPC(runtime);
|
|
525
254
|
```
|
|
526
255
|
|
|
527
256
|
### Error Stack Traces
|
|
@@ -547,26 +276,36 @@ Returns an `EffectBuilder` instance.
|
|
|
547
276
|
|
|
548
277
|
```ts
|
|
549
278
|
// With default builder
|
|
550
|
-
const effectOs = makeEffectORPC(runtime)
|
|
279
|
+
const effectOs = makeEffectORPC(runtime);
|
|
551
280
|
|
|
552
281
|
// With customized builder
|
|
553
|
-
const effectAuthedOs = makeEffectORPC(runtime, authedBuilder)
|
|
282
|
+
const effectAuthedOs = makeEffectORPC(runtime, authedBuilder);
|
|
554
283
|
```
|
|
555
284
|
|
|
556
285
|
### `EffectBuilder`
|
|
557
286
|
|
|
558
287
|
Wraps an oRPC Builder with Effect support. Available methods:
|
|
559
288
|
|
|
560
|
-
| Method
|
|
561
|
-
|
|
|
562
|
-
|
|
|
563
|
-
|
|
|
564
|
-
|
|
|
565
|
-
|
|
|
566
|
-
|
|
|
567
|
-
| `.
|
|
568
|
-
| `.
|
|
569
|
-
| `.
|
|
289
|
+
| Method | Description |
|
|
290
|
+
| ------------------- | ------------------------------------------------------------------------------- |
|
|
291
|
+
| `.$config(config)` | Set or override the builder config |
|
|
292
|
+
| `.$context<U>()` | Set or override the initial context type |
|
|
293
|
+
| `.$meta(meta)` | Set or override the initial metadata |
|
|
294
|
+
| `.$route(route)` | Set or override the initial route configuration |
|
|
295
|
+
| `.$input(schema)` | Set or override the initial input schema |
|
|
296
|
+
| `.errors(map)` | Add type-safe custom errors |
|
|
297
|
+
| `.meta(meta)` | Set procedure metadata (merged with existing) |
|
|
298
|
+
| `.route(route)` | Configure OpenAPI route (merged with existing) |
|
|
299
|
+
| `.input(schema)` | Define input validation schema |
|
|
300
|
+
| `.output(schema)` | Define output validation schema |
|
|
301
|
+
| `.use(middleware)` | Add middleware |
|
|
302
|
+
| `.traced(name)` | Add a traceable span for telemetry (optional, defaults to the procedure's path) |
|
|
303
|
+
| `.handler(handler)` | Define a non-Effect handler (standard oRPC handler) |
|
|
304
|
+
| `.effect(handler)` | Define the Effect handler |
|
|
305
|
+
| `.prefix(prefix)` | Prefix all procedures in the router (for OpenAPI) |
|
|
306
|
+
| `.tag(...tags)` | Add tags to all procedures in the router (for OpenAPI) |
|
|
307
|
+
| `.router(router)` | Apply all options to a router |
|
|
308
|
+
| `.lazy(loader)` | Create and apply options to a lazy-loaded router |
|
|
570
309
|
|
|
571
310
|
### `EffectDecoratedProcedure`
|
|
572
311
|
|
|
@@ -575,60 +314,22 @@ The result of calling `.effect()`. Extends standard oRPC `DecoratedProcedure` wi
|
|
|
575
314
|
| Method | Description |
|
|
576
315
|
| ----------------------- | --------------------------------------------- |
|
|
577
316
|
| `.errors(map)` | Add more custom errors |
|
|
578
|
-
| `.meta(meta)` | Update metadata
|
|
579
|
-
| `.route(route)` | Update route configuration
|
|
317
|
+
| `.meta(meta)` | Update metadata (merged with existing) |
|
|
318
|
+
| `.route(route)` | Update route configuration (merged) |
|
|
580
319
|
| `.use(middleware)` | Add middleware |
|
|
581
320
|
| `.callable(options?)` | Make procedure directly invocable |
|
|
582
321
|
| `.actionable(options?)` | Make procedure compatible with server actions |
|
|
583
322
|
|
|
584
|
-
### `ORPCTaggedError
|
|
323
|
+
### `ORPCTaggedError(schema?)(tag, codeOrOptions?, defaultOptions?)`
|
|
585
324
|
|
|
586
325
|
Factory function to create Effect-native tagged error classes.
|
|
326
|
+
If no code is provided, it defaults to CONSTANT_CASE of the tag (e.g., `UserNotFoundError` → `USER_NOT_FOUND_ERROR`).
|
|
587
327
|
|
|
588
|
-
- `
|
|
589
|
-
- `TData` - Optional type for the error's data payload
|
|
328
|
+
- `schema` - Optional Standard Schema for the error's data payload (e.g., `z.object({ userId: z.string() })`)
|
|
590
329
|
- `tag` - Unique tag for discriminated unions (used by Effect's `catchTag`)
|
|
591
330
|
- `codeOrOptions` - Either an ORPCErrorCode string or `{ status?, message? }` options
|
|
592
331
|
- `defaultOptions` - Default `{ status?, message? }` when code is provided explicitly
|
|
593
332
|
|
|
594
|
-
If no code is provided, it defaults to CONSTANT_CASE of the tag (e.g., `UserNotFound` → `USER_NOT_FOUND`).
|
|
595
|
-
|
|
596
|
-
```ts
|
|
597
|
-
// Tag only - code defaults to 'MY_ERROR'
|
|
598
|
-
class MyError extends ORPCTaggedError<MyError>()('MyError') {}
|
|
599
|
-
|
|
600
|
-
// With options - code defaults to 'MY_ERROR'
|
|
601
|
-
class MyError extends ORPCTaggedError<MyError>()(
|
|
602
|
-
'MyError',
|
|
603
|
-
{ status: 400, message: 'Bad request' }
|
|
604
|
-
) {}
|
|
605
|
-
|
|
606
|
-
// With explicit code
|
|
607
|
-
class MyError extends ORPCTaggedError<MyError>()(
|
|
608
|
-
'MyError',
|
|
609
|
-
'CUSTOM_CODE',
|
|
610
|
-
{ status: 400 }
|
|
611
|
-
) {}
|
|
612
|
-
|
|
613
|
-
// With typed data
|
|
614
|
-
class MyError extends ORPCTaggedError<MyError, { field: string }>()(
|
|
615
|
-
'MyError',
|
|
616
|
-
'BAD_REQUEST'
|
|
617
|
-
) {}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
### `toORPCError(taggedError)`
|
|
621
|
-
|
|
622
|
-
Converts an `ORPCTaggedError` instance to a plain `ORPCError`.
|
|
623
|
-
|
|
624
|
-
```ts
|
|
625
|
-
import { toORPCError } from 'effect-orpc'
|
|
626
|
-
|
|
627
|
-
const taggedError = new UserNotFound({ data: { userId: '123' } })
|
|
628
|
-
const orpcError = toORPCError(taggedError)
|
|
629
|
-
// => ORPCError { code: 'USER_NOT_FOUND', status: 404, data: { userId: '123' } }
|
|
630
|
-
```
|
|
631
|
-
|
|
632
333
|
## License
|
|
633
334
|
|
|
634
335
|
MIT
|