effect-orpc 0.0.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.
- package/.cursor/hooks.json +10 -0
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/.github/workflows/publish.yml +45 -0
- package/.oxfmtrc.jsonc +23 -0
- package/.oxlintrc.json +4 -0
- package/.vscode/settings.json +59 -0
- package/LICENSE +21 -0
- package/README.md +538 -0
- package/bun.lock +403 -0
- package/package.json +55 -0
- package/src/effect-builder.ts +680 -0
- package/src/effect-procedure.ts +354 -0
- package/src/index.ts +36 -0
- package/src/tagged-error.ts +515 -0
- package/src/tests/effect-builder.test.ts +488 -0
- package/src/tests/effect-error-map.test.ts +313 -0
- package/src/tests/effect-procedure.test.ts +213 -0
- package/src/tests/shared.ts +79 -0
- package/src/tests/tagged-error.test.ts +311 -0
- package/tsconfig.json +30 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ORPCErrorCode,
|
|
3
|
+
ORPCErrorJSON,
|
|
4
|
+
ORPCErrorOptions,
|
|
5
|
+
} from "@orpc/client";
|
|
6
|
+
import type { AnySchema, ErrorMap, ErrorMapItem } from "@orpc/contract";
|
|
7
|
+
import type { ORPCErrorConstructorMapItemOptions } from "@orpc/server";
|
|
8
|
+
import type { MaybeOptionalOptions } from "@orpc/shared";
|
|
9
|
+
import type { Pipeable, Types } from "effect";
|
|
10
|
+
import type * as Cause from "effect/Cause";
|
|
11
|
+
import type * as Effect from "effect/Effect";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
fallbackORPCErrorMessage,
|
|
15
|
+
fallbackORPCErrorStatus,
|
|
16
|
+
isORPCErrorStatus,
|
|
17
|
+
ORPCError,
|
|
18
|
+
} from "@orpc/client";
|
|
19
|
+
import { resolveMaybeOptionalOptions } from "@orpc/shared";
|
|
20
|
+
import * as Data from "effect/Data";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Symbol to access the underlying ORPCError instance
|
|
24
|
+
*/
|
|
25
|
+
export const ORPCErrorSymbol: unique symbol = Symbol.for(
|
|
26
|
+
"@orpc/effect/ORPCTaggedError",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Instance type for ORPCTaggedError that combines YieldableError with ORPCError properties
|
|
31
|
+
*/
|
|
32
|
+
export interface ORPCTaggedErrorInstance<
|
|
33
|
+
TTag extends string,
|
|
34
|
+
TCode extends ORPCErrorCode,
|
|
35
|
+
TData,
|
|
36
|
+
>
|
|
37
|
+
extends Cause.YieldableError, Pipeable.Pipeable {
|
|
38
|
+
readonly _tag: TTag;
|
|
39
|
+
readonly code: TCode;
|
|
40
|
+
readonly status: number;
|
|
41
|
+
readonly data: TData;
|
|
42
|
+
readonly defined: boolean;
|
|
43
|
+
readonly [ORPCErrorSymbol]: ORPCError<TCode, TData>;
|
|
44
|
+
|
|
45
|
+
toJSON(): ORPCErrorJSON<TCode, TData> & { _tag: TTag };
|
|
46
|
+
toORPCError(): ORPCError<TCode, TData>;
|
|
47
|
+
commit(): Effect.Effect<never, this, never>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for creating an ORPCTaggedError
|
|
52
|
+
*/
|
|
53
|
+
export type ORPCTaggedErrorOptions<TData> = Omit<
|
|
54
|
+
ORPCErrorOptions<TData>,
|
|
55
|
+
"defined"
|
|
56
|
+
> & { defined?: boolean };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Constructor type for ORPCTaggedError classes
|
|
60
|
+
*/
|
|
61
|
+
export interface ORPCTaggedErrorClass<
|
|
62
|
+
TTag extends string,
|
|
63
|
+
TCode extends ORPCErrorCode,
|
|
64
|
+
TData,
|
|
65
|
+
> {
|
|
66
|
+
readonly _tag: TTag;
|
|
67
|
+
readonly code: TCode;
|
|
68
|
+
new (
|
|
69
|
+
...args: MaybeOptionalOptions<ORPCTaggedErrorOptions<TData>>
|
|
70
|
+
): ORPCTaggedErrorInstance<TTag, TCode, TData>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Type helper to infer the ORPCError type from an ORPCTaggedError
|
|
75
|
+
*/
|
|
76
|
+
export type InferORPCError<T> =
|
|
77
|
+
T extends ORPCTaggedErrorInstance<string, infer TCode, infer TData>
|
|
78
|
+
? ORPCError<TCode, TData>
|
|
79
|
+
: never;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Any ORPCTaggedErrorClass
|
|
83
|
+
*/
|
|
84
|
+
export type AnyORPCTaggedErrorClass = ORPCTaggedErrorClass<
|
|
85
|
+
string,
|
|
86
|
+
ORPCErrorCode,
|
|
87
|
+
any
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a value is an ORPCTaggedErrorClass (constructor)
|
|
92
|
+
*/
|
|
93
|
+
export function isORPCTaggedErrorClass(
|
|
94
|
+
value: unknown,
|
|
95
|
+
): value is AnyORPCTaggedErrorClass {
|
|
96
|
+
return (
|
|
97
|
+
typeof value === "function" &&
|
|
98
|
+
"_tag" in value &&
|
|
99
|
+
"code" in value &&
|
|
100
|
+
typeof value._tag === "string" &&
|
|
101
|
+
typeof value.code === "string"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a value is an ORPCTaggedError instance
|
|
107
|
+
*/
|
|
108
|
+
export function isORPCTaggedError(
|
|
109
|
+
value: unknown,
|
|
110
|
+
): value is ORPCTaggedErrorInstance<string, ORPCErrorCode, unknown> {
|
|
111
|
+
return (
|
|
112
|
+
typeof value === "object" && value !== null && ORPCErrorSymbol in value
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Converts a PascalCase or camelCase string to CONSTANT_CASE.
|
|
118
|
+
* e.g., "UserNotFoundError" -> "USER_NOT_FOUND_ERROR"
|
|
119
|
+
*/
|
|
120
|
+
function toConstantCase(str: string): string {
|
|
121
|
+
return str
|
|
122
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
123
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
124
|
+
.toUpperCase();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Type-level conversion: split on capital letters and join with underscore
|
|
128
|
+
type SplitOnCapital<
|
|
129
|
+
S extends string,
|
|
130
|
+
Acc extends string = "",
|
|
131
|
+
> = S extends `${infer Head}${infer Tail}`
|
|
132
|
+
? Head extends Uppercase<Head>
|
|
133
|
+
? Head extends Lowercase<Head>
|
|
134
|
+
? SplitOnCapital<Tail, `${Acc}${Head}`>
|
|
135
|
+
: Acc extends ""
|
|
136
|
+
? SplitOnCapital<Tail, Head>
|
|
137
|
+
: `${Acc}_${SplitOnCapital<Tail, Head>}`
|
|
138
|
+
: SplitOnCapital<Tail, `${Acc}${Uppercase<Head>}`>
|
|
139
|
+
: Acc;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Converts a tag name to an error code in CONSTANT_CASE.
|
|
143
|
+
*/
|
|
144
|
+
export type TagToCode<TTag extends string> = SplitOnCapital<TTag>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates a tagged error class that combines Effect's YieldableError with ORPCError.
|
|
148
|
+
*
|
|
149
|
+
* This allows you to create errors that:
|
|
150
|
+
* - Can be yielded in Effect generators (`yield* myError`)
|
|
151
|
+
* - Have all ORPCError properties (code, status, data, defined)
|
|
152
|
+
* - Can be converted to a plain ORPCError for oRPC handlers
|
|
153
|
+
*
|
|
154
|
+
* The returned factory function takes:
|
|
155
|
+
* - `tag` - The unique tag for this error type (used for discriminated unions)
|
|
156
|
+
* - `codeOrOptions` - Optional ORPC error code or options. If omitted, code defaults to CONSTANT_CASE of tag
|
|
157
|
+
* - `defaultOptions` - Optional default options for status and message (when code is provided)
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* import { ORPCTaggedError } from '@orpc/effect'
|
|
162
|
+
* import { Effect } from 'effect'
|
|
163
|
+
*
|
|
164
|
+
* // Define a custom error (code defaults to 'USER_NOT_FOUND_ERROR')
|
|
165
|
+
* class UserNotFoundError extends ORPCTaggedError<UserNotFoundError>()('UserNotFoundError') {}
|
|
166
|
+
*
|
|
167
|
+
* // With explicit code
|
|
168
|
+
* class NotFoundError extends ORPCTaggedError<NotFoundError>()('NotFoundError', 'NOT_FOUND') {}
|
|
169
|
+
*
|
|
170
|
+
* // Use in an Effect
|
|
171
|
+
* const getUser = (id: string) => Effect.gen(function* () {
|
|
172
|
+
* const user = yield* findUser(id)
|
|
173
|
+
* if (!user) {
|
|
174
|
+
* return yield* new UserNotFoundError({ data: { userId: id } })
|
|
175
|
+
* }
|
|
176
|
+
* return user
|
|
177
|
+
* })
|
|
178
|
+
*
|
|
179
|
+
* // With custom data type
|
|
180
|
+
* class ValidationError extends ORPCTaggedError<ValidationError, { fields: string[] }>()('ValidationError', 'BAD_REQUEST') {}
|
|
181
|
+
*
|
|
182
|
+
* // With options only (code defaults to 'VALIDATION_ERROR')
|
|
183
|
+
* class ValidationError2 extends ORPCTaggedError<ValidationError2, { fields: string[] }>()(
|
|
184
|
+
* 'ValidationError2',
|
|
185
|
+
* { message: 'Validation failed' }
|
|
186
|
+
* ) {}
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
/**
|
|
190
|
+
* Return type for the factory function with overloads
|
|
191
|
+
*/
|
|
192
|
+
interface ORPCTaggedErrorFactory<Self, TData> {
|
|
193
|
+
// Overload 1: tag only (code defaults to CONSTANT_CASE of tag)
|
|
194
|
+
<TTag extends string>(
|
|
195
|
+
tag: TTag,
|
|
196
|
+
): Types.Equals<Self, unknown> extends true
|
|
197
|
+
? `Missing \`Self\` generic - use \`class MyError extends ORPCTaggedError<MyError>()(tag) {}\``
|
|
198
|
+
: ORPCTaggedErrorClass<TTag, TagToCode<TTag>, TData>;
|
|
199
|
+
|
|
200
|
+
// Overload 2: tag + options (code defaults to CONSTANT_CASE of tag)
|
|
201
|
+
<TTag extends string>(
|
|
202
|
+
tag: TTag,
|
|
203
|
+
options: { status?: number; message?: string },
|
|
204
|
+
): Types.Equals<Self, unknown> extends true
|
|
205
|
+
? `Missing \`Self\` generic - use \`class MyError extends ORPCTaggedError<MyError>()(tag, options) {}\``
|
|
206
|
+
: ORPCTaggedErrorClass<TTag, TagToCode<TTag>, TData>;
|
|
207
|
+
|
|
208
|
+
// Overload 3: tag + explicit code
|
|
209
|
+
<TTag extends string, TCode extends ORPCErrorCode>(
|
|
210
|
+
tag: TTag,
|
|
211
|
+
code: TCode,
|
|
212
|
+
defaultOptions?: { status?: number; message?: string },
|
|
213
|
+
): Types.Equals<Self, unknown> extends true
|
|
214
|
+
? `Missing \`Self\` generic - use \`class MyError extends ORPCTaggedError<MyError>()(tag, code) {}\``
|
|
215
|
+
: ORPCTaggedErrorClass<TTag, TCode, TData>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function ORPCTaggedError<
|
|
219
|
+
Self,
|
|
220
|
+
TData = undefined,
|
|
221
|
+
>(): ORPCTaggedErrorFactory<Self, TData> {
|
|
222
|
+
const factory = <TTag extends string, TCode extends ORPCErrorCode>(
|
|
223
|
+
tag: TTag,
|
|
224
|
+
codeOrOptions?: TCode | { status?: number; message?: string },
|
|
225
|
+
defaultOptions?: { status?: number; message?: string },
|
|
226
|
+
): ORPCTaggedErrorClass<TTag, TCode, TData> => {
|
|
227
|
+
// Determine if second arg is code or options
|
|
228
|
+
const isCodeProvided = typeof codeOrOptions === "string";
|
|
229
|
+
const code = (
|
|
230
|
+
isCodeProvided ? codeOrOptions : toConstantCase(tag)
|
|
231
|
+
) as TCode;
|
|
232
|
+
const options = isCodeProvided ? defaultOptions : codeOrOptions;
|
|
233
|
+
|
|
234
|
+
const defaultStatus = options?.status;
|
|
235
|
+
const defaultMessage = options?.message;
|
|
236
|
+
|
|
237
|
+
// Use Effect's TaggedError as the base - this handles all Effect internals
|
|
238
|
+
// (YieldableError, type symbols, commit(), Symbol.iterator, pipe(), etc.)
|
|
239
|
+
const BaseTaggedError = Data.TaggedError(tag) as unknown as new (args: {
|
|
240
|
+
message?: string;
|
|
241
|
+
cause?: unknown;
|
|
242
|
+
code: TCode;
|
|
243
|
+
status: number;
|
|
244
|
+
data: TData;
|
|
245
|
+
defined: boolean;
|
|
246
|
+
}) => Cause.YieldableError & {
|
|
247
|
+
readonly _tag: TTag;
|
|
248
|
+
readonly code: TCode;
|
|
249
|
+
readonly status: number;
|
|
250
|
+
readonly data: TData;
|
|
251
|
+
readonly defined: boolean;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
class ORPCTaggedErrorBase extends BaseTaggedError {
|
|
255
|
+
static readonly _tag = tag;
|
|
256
|
+
static readonly code = code;
|
|
257
|
+
|
|
258
|
+
readonly [ORPCErrorSymbol]: ORPCError<TCode, TData>;
|
|
259
|
+
|
|
260
|
+
constructor(
|
|
261
|
+
...rest: MaybeOptionalOptions<ORPCTaggedErrorOptions<TData>>
|
|
262
|
+
) {
|
|
263
|
+
const opts = resolveMaybeOptionalOptions(rest);
|
|
264
|
+
const status = opts.status ?? defaultStatus;
|
|
265
|
+
const inputMessage = opts.message ?? defaultMessage;
|
|
266
|
+
|
|
267
|
+
if (status !== undefined && !isORPCErrorStatus(status)) {
|
|
268
|
+
throw new globalThis.Error(
|
|
269
|
+
"[ORPCTaggedError] Invalid error status code.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const finalStatus = fallbackORPCErrorStatus(code, status);
|
|
274
|
+
const finalMessage = fallbackORPCErrorMessage(code, inputMessage);
|
|
275
|
+
|
|
276
|
+
// Pass to Effect's TaggedError - it spreads these onto the instance
|
|
277
|
+
super({
|
|
278
|
+
message: finalMessage,
|
|
279
|
+
cause: opts.cause,
|
|
280
|
+
code,
|
|
281
|
+
status: finalStatus,
|
|
282
|
+
data: opts.data as TData,
|
|
283
|
+
defined: opts.defined ?? true,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Create the underlying ORPCError for interop
|
|
287
|
+
this[ORPCErrorSymbol] = new ORPCError(code, {
|
|
288
|
+
status: finalStatus,
|
|
289
|
+
message: finalMessage,
|
|
290
|
+
data: opts.data as TData,
|
|
291
|
+
defined: this.defined,
|
|
292
|
+
cause: opts.cause,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Converts this error to a plain ORPCError.
|
|
298
|
+
* Useful when you need to return from an oRPC handler.
|
|
299
|
+
*/
|
|
300
|
+
toORPCError(): ORPCError<TCode, TData> {
|
|
301
|
+
return this[ORPCErrorSymbol];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
override toJSON(): ORPCErrorJSON<TCode, TData> & { _tag: TTag } {
|
|
305
|
+
return {
|
|
306
|
+
_tag: this._tag,
|
|
307
|
+
defined: this.defined,
|
|
308
|
+
code: this.code,
|
|
309
|
+
status: this.status,
|
|
310
|
+
message: this.message,
|
|
311
|
+
data: this.data,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return ORPCTaggedErrorBase as any;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return factory as ORPCTaggedErrorFactory<Self, TData>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Converts an ORPCTaggedError to a plain ORPCError.
|
|
324
|
+
* Useful in handlers that need to throw ORPCError.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const handler = effectOs.effect(function* () {
|
|
329
|
+
* const result = yield* someOperation.pipe(
|
|
330
|
+
* Effect.catchTag('UserNotFoundError', (e) =>
|
|
331
|
+
* Effect.fail(toORPCError(e))
|
|
332
|
+
* )
|
|
333
|
+
* )
|
|
334
|
+
* return result
|
|
335
|
+
* })
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
export function toORPCError<TCode extends ORPCErrorCode, TData>(
|
|
339
|
+
error: ORPCTaggedErrorInstance<string, TCode, TData>,
|
|
340
|
+
): ORPCError<TCode, TData> {
|
|
341
|
+
return error[ORPCErrorSymbol];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Extended Error Map Types for Effect
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* An item in the EffectErrorMap - can be either a traditional ErrorMapItem or an ORPCTaggedErrorClass
|
|
350
|
+
*/
|
|
351
|
+
export type EffectErrorMapItem =
|
|
352
|
+
| ErrorMapItem<AnySchema>
|
|
353
|
+
| AnyORPCTaggedErrorClass;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Extended error map that supports both traditional oRPC errors and ORPCTaggedError classes.
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```ts
|
|
360
|
+
* const errorMap = {
|
|
361
|
+
* // Traditional format
|
|
362
|
+
* BAD_REQUEST: { status: 400, message: 'Bad request' },
|
|
363
|
+
*
|
|
364
|
+
* // Tagged error class reference
|
|
365
|
+
* USER_NOT_FOUND: UserNotFoundError,
|
|
366
|
+
* } satisfies EffectErrorMap
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
export type EffectErrorMap = {
|
|
370
|
+
[key in ORPCErrorCode]?: EffectErrorMapItem;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Merges two EffectErrorMaps, with the second map taking precedence.
|
|
375
|
+
*/
|
|
376
|
+
export type MergedEffectErrorMap<
|
|
377
|
+
T1 extends EffectErrorMap,
|
|
378
|
+
T2 extends EffectErrorMap,
|
|
379
|
+
> = T1 & T2;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Extracts the instance type from an EffectErrorMapItem
|
|
383
|
+
*/
|
|
384
|
+
export type EffectErrorMapItemToInstance<
|
|
385
|
+
TCode extends ORPCErrorCode,
|
|
386
|
+
T extends EffectErrorMapItem,
|
|
387
|
+
> = T extends AnyORPCTaggedErrorClass
|
|
388
|
+
? InstanceType<T>
|
|
389
|
+
: T extends { data?: infer TData }
|
|
390
|
+
? ORPCError<TCode, TData>
|
|
391
|
+
: ORPCError<TCode, unknown>;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Converts an EffectErrorMap to a union of error instances.
|
|
395
|
+
*/
|
|
396
|
+
export type EffectErrorMapToUnion<T extends EffectErrorMap> = {
|
|
397
|
+
[K in keyof T]: K extends ORPCErrorCode
|
|
398
|
+
? T[K] extends EffectErrorMapItem
|
|
399
|
+
? EffectErrorMapItemToInstance<K, T[K]>
|
|
400
|
+
: never
|
|
401
|
+
: never;
|
|
402
|
+
}[keyof T];
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Type for the error constructors available in Effect handlers.
|
|
406
|
+
* For tagged errors, it's the class constructor itself.
|
|
407
|
+
* For traditional errors, it's a function that creates ORPCError.
|
|
408
|
+
*/
|
|
409
|
+
export type EffectErrorConstructorMapItem<
|
|
410
|
+
TCode extends ORPCErrorCode,
|
|
411
|
+
T extends EffectErrorMapItem,
|
|
412
|
+
> =
|
|
413
|
+
T extends ORPCTaggedErrorClass<infer _TTag, TCode, infer TData>
|
|
414
|
+
? ORPCTaggedErrorClass<_TTag, TCode, TData>
|
|
415
|
+
: T extends { data?: infer TData }
|
|
416
|
+
? (
|
|
417
|
+
...args: MaybeOptionalOptions<
|
|
418
|
+
ORPCErrorConstructorMapItemOptions<TData>
|
|
419
|
+
>
|
|
420
|
+
) => ORPCError<TCode, TData>
|
|
421
|
+
: (
|
|
422
|
+
...args: MaybeOptionalOptions<
|
|
423
|
+
ORPCErrorConstructorMapItemOptions<unknown>
|
|
424
|
+
>
|
|
425
|
+
) => ORPCError<TCode, unknown>;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Constructor map for EffectErrorMap - provides typed error constructors for handlers.
|
|
429
|
+
*/
|
|
430
|
+
export type EffectErrorConstructorMap<T extends EffectErrorMap> = {
|
|
431
|
+
[K in keyof T]: K extends ORPCErrorCode
|
|
432
|
+
? T[K] extends EffectErrorMapItem
|
|
433
|
+
? EffectErrorConstructorMapItem<K, T[K]>
|
|
434
|
+
: never
|
|
435
|
+
: never;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Creates an error constructor map from an EffectErrorMap.
|
|
440
|
+
* Tagged error classes are passed through directly.
|
|
441
|
+
* Traditional error items become ORPCError factory functions.
|
|
442
|
+
*/
|
|
443
|
+
export function createEffectErrorConstructorMap<T extends EffectErrorMap>(
|
|
444
|
+
errors: T | undefined,
|
|
445
|
+
): EffectErrorConstructorMap<T> {
|
|
446
|
+
const target = errors ?? ({} as T);
|
|
447
|
+
const proxy = new Proxy(target, {
|
|
448
|
+
get(proxyTarget, code) {
|
|
449
|
+
if (typeof code !== "string") {
|
|
450
|
+
return Reflect.get(proxyTarget, code);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const config = target[code];
|
|
454
|
+
|
|
455
|
+
// If it's a tagged error class, return it directly
|
|
456
|
+
if (isORPCTaggedErrorClass(config)) {
|
|
457
|
+
return config;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Otherwise, create a factory function for ORPCError
|
|
461
|
+
return (
|
|
462
|
+
...rest: MaybeOptionalOptions<
|
|
463
|
+
Omit<ORPCErrorOptions<unknown>, "defined" | "status">
|
|
464
|
+
>
|
|
465
|
+
) => {
|
|
466
|
+
const options = resolveMaybeOptionalOptions(rest);
|
|
467
|
+
return new ORPCError(code, {
|
|
468
|
+
defined: Boolean(config),
|
|
469
|
+
status: config?.status,
|
|
470
|
+
message: options.message ?? config?.message,
|
|
471
|
+
data: options.data,
|
|
472
|
+
cause: options.cause,
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return proxy as EffectErrorConstructorMap<T>;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Converts an EffectErrorMap to a standard oRPC ErrorMap for interop.
|
|
483
|
+
* Tagged error classes are converted to their equivalent ErrorMapItem format.
|
|
484
|
+
*/
|
|
485
|
+
export function effectErrorMapToErrorMap<T extends EffectErrorMap>(
|
|
486
|
+
errorMap: T | undefined,
|
|
487
|
+
): ErrorMap {
|
|
488
|
+
const result: ErrorMap = {};
|
|
489
|
+
|
|
490
|
+
if (!errorMap) {
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
for (const [code, ClassOrErrorItem] of Object.entries(errorMap)) {
|
|
495
|
+
if (!ClassOrErrorItem) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (isORPCTaggedErrorClass(ClassOrErrorItem)) {
|
|
500
|
+
const error = new ClassOrErrorItem().toORPCError();
|
|
501
|
+
|
|
502
|
+
// For tagged errors, we create a minimal entry
|
|
503
|
+
// The actual validation will be handled by the tagged error class
|
|
504
|
+
result[code] = {
|
|
505
|
+
status: error.status,
|
|
506
|
+
message: error.message,
|
|
507
|
+
data: error.data,
|
|
508
|
+
};
|
|
509
|
+
} else {
|
|
510
|
+
result[code] = ClassOrErrorItem;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return result;
|
|
515
|
+
}
|