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.
@@ -0,0 +1,311 @@
1
+ import { ORPCError } from "@orpc/client";
2
+ import { Effect, Exit } from "effect";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import {
6
+ isORPCTaggedError,
7
+ ORPCErrorSymbol,
8
+ ORPCTaggedError,
9
+ toORPCError,
10
+ } from "../tagged-error";
11
+
12
+ // Define test errors with explicit code
13
+ class UserNotFoundError extends ORPCTaggedError<UserNotFoundError>()(
14
+ "UserNotFoundError",
15
+ "NOT_FOUND",
16
+ ) {}
17
+
18
+ class ValidationError extends ORPCTaggedError<
19
+ ValidationError,
20
+ { fields: string[] }
21
+ >()("ValidationError", "BAD_REQUEST", { message: "Validation failed" }) {}
22
+
23
+ class CustomStatusError extends ORPCTaggedError<CustomStatusError>()(
24
+ "CustomStatusError",
25
+ "INTERNAL_SERVER_ERROR",
26
+ { status: 503, message: "Service unavailable" },
27
+ ) {}
28
+
29
+ // Define test errors with default code (derived from tag)
30
+ class AutoCodeError extends ORPCTaggedError<AutoCodeError>()("AutoCodeError") {}
31
+
32
+ class AutoCodeWithOptionsError extends ORPCTaggedError<AutoCodeWithOptionsError>()(
33
+ "AutoCodeWithOptionsError",
34
+ { message: "Auto code error message" },
35
+ ) {}
36
+
37
+ class MyCustomError extends ORPCTaggedError<MyCustomError>()("MyCustomError") {}
38
+
39
+ describe("class ORPCTaggedError", () => {
40
+ describe("basic functionality", () => {
41
+ it("should create an error with the correct tag", () => {
42
+ const error = new UserNotFoundError();
43
+
44
+ expect(error._tag).toBe("UserNotFoundError");
45
+ expect(error.name).toBe("UserNotFoundError");
46
+ });
47
+
48
+ it("should create an error with the correct code", () => {
49
+ const error = new UserNotFoundError();
50
+
51
+ expect(error.code).toBe("NOT_FOUND");
52
+ });
53
+
54
+ it("should use default status from code", () => {
55
+ const error = new UserNotFoundError();
56
+
57
+ expect(error.status).toBe(404);
58
+ });
59
+
60
+ it("should use default message from code", () => {
61
+ const error = new UserNotFoundError();
62
+
63
+ expect(error.message).toBe("Not Found");
64
+ });
65
+
66
+ it("should be defined by default", () => {
67
+ const error = new UserNotFoundError();
68
+
69
+ expect(error.defined).toBe(true);
70
+ });
71
+
72
+ it("should allow custom message", () => {
73
+ const error = new UserNotFoundError({
74
+ message: "User with ID 123 not found",
75
+ });
76
+
77
+ expect(error.message).toBe("User with ID 123 not found");
78
+ });
79
+
80
+ it("should use default message from options", () => {
81
+ const error = new ValidationError({ data: { fields: ["email"] } });
82
+
83
+ expect(error.message).toBe("Validation failed");
84
+ });
85
+
86
+ it("should use default status from options", () => {
87
+ const error = new CustomStatusError();
88
+
89
+ expect(error.status).toBe(503);
90
+ });
91
+
92
+ it("should allow custom status override", () => {
93
+ // Custom status that's still a valid error status
94
+ const error = new UserNotFoundError({ status: 410 });
95
+
96
+ expect(error.status).toBe(410);
97
+ });
98
+
99
+ it("should throw on invalid status", () => {
100
+ expect(() => new UserNotFoundError({ status: 200 })).toThrow(
101
+ "[ORPCTaggedError] Invalid error status code.",
102
+ );
103
+ });
104
+ });
105
+
106
+ describe("data handling", () => {
107
+ it("should handle data correctly", () => {
108
+ const error = new ValidationError({
109
+ data: { fields: ["email", "password"] },
110
+ });
111
+
112
+ expect(error.data).toEqual({ fields: ["email", "password"] });
113
+ });
114
+
115
+ it("should handle undefined data", () => {
116
+ const error = new UserNotFoundError();
117
+
118
+ expect(error.data).toBeUndefined();
119
+ });
120
+ });
121
+
122
+ describe("interop with ORPCError", () => {
123
+ it("should have ORPCErrorSymbol", () => {
124
+ const error = new UserNotFoundError();
125
+
126
+ expect(ORPCErrorSymbol in error).toBe(true);
127
+ expect(error[ORPCErrorSymbol]).toBeInstanceOf(ORPCError);
128
+ });
129
+
130
+ it("should create equivalent ORPCError via toORPCError method", () => {
131
+ const error = new UserNotFoundError({ message: "Custom message" });
132
+ const orpcError = error.toORPCError();
133
+
134
+ expect(orpcError).toBeInstanceOf(ORPCError);
135
+ expect(orpcError.code).toBe("NOT_FOUND");
136
+ expect(orpcError.status).toBe(404);
137
+ expect(orpcError.message).toBe("Custom message");
138
+ expect(orpcError.defined).toBe(true);
139
+ });
140
+
141
+ it("should create equivalent ORPCError via toORPCError function", () => {
142
+ const error = new ValidationError({ data: { fields: ["name"] } });
143
+ const orpcError = toORPCError(error);
144
+
145
+ expect(orpcError).toBeInstanceOf(ORPCError);
146
+ expect(orpcError.code).toBe("BAD_REQUEST");
147
+ expect(orpcError.data).toEqual({ fields: ["name"] });
148
+ });
149
+ });
150
+
151
+ describe("isORPCTaggedError", () => {
152
+ it("should return true for ORPCTaggedError instances", () => {
153
+ const error = new UserNotFoundError();
154
+
155
+ expect(isORPCTaggedError(error)).toBe(true);
156
+ });
157
+
158
+ it("should return false for regular ORPCError", () => {
159
+ const error = new ORPCError("NOT_FOUND");
160
+
161
+ expect(isORPCTaggedError(error)).toBe(false);
162
+ });
163
+
164
+ it("should return false for regular Error", () => {
165
+ const error = new Error("test");
166
+
167
+ expect(isORPCTaggedError(error)).toBe(false);
168
+ });
169
+
170
+ it("should return false for non-objects", () => {
171
+ expect(isORPCTaggedError(null)).toBe(false);
172
+ expect(isORPCTaggedError(undefined)).toBe(false);
173
+ expect(isORPCTaggedError("string")).toBe(false);
174
+ expect(isORPCTaggedError(123)).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe("toJSON", () => {
179
+ it("should serialize to JSON with all fields", () => {
180
+ const error = new ValidationError({
181
+ data: { fields: ["email"] },
182
+ message: "Invalid input",
183
+ });
184
+
185
+ expect(error.toJSON()).toEqual({
186
+ _tag: "ValidationError",
187
+ defined: true,
188
+ code: "BAD_REQUEST",
189
+ status: 400,
190
+ message: "Invalid input",
191
+ data: { fields: ["email"] },
192
+ });
193
+ });
194
+ });
195
+
196
+ describe("effect integration", () => {
197
+ it("should work with Effect.fail", async () => {
198
+ const program = Effect.fail(
199
+ new ValidationError({ data: { fields: ["email"] } }),
200
+ );
201
+
202
+ const result = await Effect.runPromiseExit(program);
203
+
204
+ expect(Exit.isFailure(result)).toBe(true);
205
+ if (Exit.isFailure(result)) {
206
+ const error = result.cause._tag === "Fail" ? result.cause.error : null;
207
+ expect(error).toBeInstanceOf(ValidationError);
208
+ expect((error as ValidationError).data).toEqual({ fields: ["email"] });
209
+ }
210
+ });
211
+
212
+ it("should work with Effect.catchTag using Effect.fail", async () => {
213
+ const program = Effect.fail(new UserNotFoundError()).pipe(
214
+ Effect.catchTag("UserNotFoundError", () => Effect.succeed("recovered")),
215
+ );
216
+
217
+ const result = await Effect.runPromise(program);
218
+
219
+ expect(result).toBe("recovered");
220
+ });
221
+
222
+ it("should preserve type information in catchTag", async () => {
223
+ const program = Effect.fail(
224
+ new ValidationError({ data: { fields: ["email"] } }),
225
+ ).pipe(
226
+ Effect.catchTag("ValidationError", (e) => {
227
+ // e should be typed as ValidationError
228
+ return Effect.succeed(`Fields: ${e.data.fields.join(", ")}`);
229
+ }),
230
+ );
231
+
232
+ const result = await Effect.runPromise(program);
233
+
234
+ expect(result).toBe("Fields: email");
235
+ });
236
+
237
+ it("should work with commit() method for manual yielding", async () => {
238
+ const error = new UserNotFoundError({ message: "User 123 not found" });
239
+ const program = error.commit();
240
+
241
+ const result = await Effect.runPromiseExit(program);
242
+
243
+ expect(Exit.isFailure(result)).toBe(true);
244
+ if (Exit.isFailure(result)) {
245
+ const failedError =
246
+ result.cause._tag === "Fail" ? result.cause.error : null;
247
+ expect(failedError).toBeInstanceOf(UserNotFoundError);
248
+ expect((failedError as UserNotFoundError).message).toBe(
249
+ "User 123 not found",
250
+ );
251
+ }
252
+ });
253
+ });
254
+
255
+ describe("class static properties", () => {
256
+ it("should have static _tag", () => {
257
+ expect(UserNotFoundError._tag).toBe("UserNotFoundError");
258
+ });
259
+
260
+ it("should have static code", () => {
261
+ expect(UserNotFoundError.code).toBe("NOT_FOUND");
262
+ });
263
+ });
264
+
265
+ describe("error cause", () => {
266
+ it("should propagate cause to underlying error", () => {
267
+ const originalError = new Error("Original error");
268
+ const error = new UserNotFoundError({ cause: originalError });
269
+
270
+ expect(error.cause).toBe(originalError);
271
+ expect(error.toORPCError().cause).toBe(originalError);
272
+ });
273
+ });
274
+
275
+ describe("automatic code from tag", () => {
276
+ it("should derive code from tag in CONSTANT_CASE", () => {
277
+ const error = new AutoCodeError();
278
+
279
+ expect(error.code).toBe("AUTO_CODE_ERROR");
280
+ expect(error._tag).toBe("AutoCodeError");
281
+ });
282
+
283
+ it("should derive code correctly for various PascalCase names", () => {
284
+ const error = new MyCustomError();
285
+
286
+ expect(error.code).toBe("MY_CUSTOM_ERROR");
287
+ });
288
+
289
+ it("should work with options but no explicit code", () => {
290
+ const error = new AutoCodeWithOptionsError();
291
+
292
+ expect(error.code).toBe("AUTO_CODE_WITH_OPTIONS_ERROR");
293
+ expect(error.message).toBe("Auto code error message");
294
+ });
295
+
296
+ it("should have correct static code", () => {
297
+ expect(AutoCodeError.code).toBe("AUTO_CODE_ERROR");
298
+ expect(MyCustomError.code).toBe("MY_CUSTOM_ERROR");
299
+ expect(AutoCodeWithOptionsError.code).toBe(
300
+ "AUTO_CODE_WITH_OPTIONS_ERROR",
301
+ );
302
+ });
303
+
304
+ it("should use custom status of 500 for unknown codes", () => {
305
+ const error = new AutoCodeError();
306
+
307
+ // Unknown codes default to 500
308
+ expect(error.status).toBe(500);
309
+ });
310
+ });
311
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["node"],
4
+ // Environment setup & latest features
5
+ "lib": ["ESNext"],
6
+ "target": "ESNext",
7
+ "module": "Preserve",
8
+ "moduleDetection": "force",
9
+ "jsx": "react-jsx",
10
+ "allowJs": true,
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ // Some stricter flags (disabled by default)
26
+ "noUnusedLocals": false,
27
+ "noUnusedParameters": false,
28
+ "noPropertyAccessFromIndexSignature": false
29
+ }
30
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ sourcemap: true,
6
+ clean: true,
7
+ format: "esm",
8
+ });