nest-result 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mannkostir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,196 @@
1
- # Temporary Holding Version
1
+ # nest-result
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Return typed errors from your NestJS handlers and let the compiler prove every one of them is mapped to an HTTP response.
4
+
5
+ Built on [neverthrow](https://github.com/supermacro/neverthrow). Works with Express and Fastify, NestJS 11 and 12.
6
+
7
+ **Status: pre-1.0 (`0.1.0`).** The public API may still change between versions.
8
+
9
+ ```bash
10
+ npm install nest-result neverthrow
11
+ ```
12
+
13
+ ## In 60 seconds
14
+
15
+ ```ts
16
+ import { Controller, Injectable, Module, Param, Post } from '@nestjs/common';
17
+ import { TaggedError, MapErrors, ResultModule } from 'nest-result';
18
+ import { err, ok, type Result } from 'neverthrow';
19
+
20
+ export type Deal = { id: string; closed: boolean };
21
+
22
+ export class DealNotFound extends TaggedError('DealNotFound')<{ dealId: string }> {}
23
+ export class DealAlreadyClosed extends TaggedError('DealAlreadyClosed') {}
24
+
25
+ @Injectable()
26
+ export class DealsService {
27
+ private readonly deals = new Map<string, Deal>([['1', { id: '1', closed: false }]]);
28
+
29
+ close(id: string): Result<Deal, DealNotFound | DealAlreadyClosed> {
30
+ const deal = this.deals.get(id);
31
+ if (deal === undefined) return err(new DealNotFound({ dealId: id }));
32
+ if (deal.closed) return err(new DealAlreadyClosed());
33
+ return ok({ ...deal, closed: true });
34
+ }
35
+ }
36
+
37
+ @Controller('deals')
38
+ export class DealsController {
39
+ constructor(private readonly deals: DealsService) {}
40
+
41
+ @Post(':id/close')
42
+ @MapErrors({ DealNotFound: 404, DealAlreadyClosed: 409 })
43
+ close(@Param('id') id: string): Result<Deal, DealNotFound | DealAlreadyClosed> {
44
+ return this.deals.close(id);
45
+ }
46
+ }
47
+
48
+ @Module({ imports: [ResultModule.forRoot()], controllers: [DealsController], providers: [DealsService] })
49
+ export class AppModule {}
50
+ ```
51
+
52
+ An `Ok` becomes the response body. An `Err` becomes its mapped status with this body:
53
+
54
+ ```json
55
+ { "statusCode": 409, "code": "DealAlreadyClosed", "message": "DealAlreadyClosed" }
56
+ ```
57
+
58
+ Now add a third error to the service's return type and forget to map it. The build fails:
59
+
60
+ ```
61
+ error TS1241: Unable to resolve signature of method decorator when called as an expression.
62
+ ...
63
+ Property '__missingErrorMapKeys' is missing in type ... but required in type 'MissingErrorMapKeys<"DealLocked">'.
64
+ ```
65
+
66
+ Map a tag that can no longer happen and you get `StaleErrorMapKeys<"...">`. Return a plain `Error` and you get `UntaggedErrorsCannotBeMapped<Error>`.
67
+
68
+ With `toHttp` a missing key shows as TypeScript's own `Property 'DealLocked' is missing in type ... but required in type ...` error instead of `MissingErrorMapKeys<...>`.
69
+
70
+ ## Tagged errors
71
+
72
+ Any value with a literal `_tag` is a tagged error. `TaggedError` is a convenience, not a requirement:
73
+
74
+ ```ts
75
+ class RateLimited extends TaggedError('RateLimited')<{ retryAfter: number; message: string }> {}
76
+ const plain = { _tag: 'RateLimited' } as const;
77
+ ```
78
+
79
+ `TaggedError` instances are real `Error`s with a stack trace, `name` equal to the tag, and `message` defaulting to the tag. Payloads may not declare `_tag` or `name`.
80
+
81
+ The default error body is `statusCode`, `code` (the tag) and `message`. No other payload field is ever sent to the client. `message` is sent as-is, so for internal failures use a fixed message or a custom `body` rather than passing through driver or exception text.
82
+
83
+ ## Mapping errors
84
+
85
+ ### In the controller body: `toHttp`
86
+
87
+ ```ts
88
+ @Get(':id')
89
+ find(@Param('id') id: string): Promise<Deal> {
90
+ return toHttp(this.deals.find(id), { DealNotFound: 404 });
91
+ }
92
+ ```
93
+
94
+ `toHttp` accepts a `Result`, `ResultAsync` or `Promise<Result>`. An `Err` is thrown as a regular `HttpException`, so your existing exception filters keep working.
95
+
96
+ ### As a decorator: `MapErrors`
97
+
98
+ `MapErrors` checks the method's return type against the map and converts the Result in a route-level interceptor. Because it runs innermost, global interceptors such as `ClassSerializerInterceptor` see the unwrapped value.
99
+
100
+ ### Custom bodies
101
+
102
+ ```ts
103
+ @MapErrors({
104
+ DealNotFound: { status: 404, body: (e: DealNotFound) => ({ dealId: e.dealId }) },
105
+ DealAlreadyClosed: 409,
106
+ })
107
+ ```
108
+
109
+ With `toHttp` the parameter type is inferred. With `MapErrors` annotate it; the annotation is checked against the method's actual error type.
110
+
111
+ ### The safety net: `ResultModule`
112
+
113
+ `ResultModule.forRoot()` registers a global interceptor. A route that returns a Result without `MapErrors` fails with 500 and a logged `MissingErrorMapError` instead of leaking `{ "value": ... }` to the client. A Result from a second copy of neverthrow in your dependency tree fails the same way with `DuplicateNeverthrowError`.
114
+
115
+ ## Swagger
116
+
117
+ ```ts
118
+ import { MapErrors } from 'nest-result/swagger';
119
+ ```
120
+
121
+ Same decorator, plus one `@ApiResponse` per mapped status, listing the tags that share it. Requires `@nestjs/swagger`.
122
+
123
+ ## Transactions that roll back on Err
124
+
125
+ With [`@nestjs-cls/transactional`](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional), a transaction rolls back when the method throws. A method that returns `Err` does not throw, so its partial writes are committed. `nest-result/transactional` fixes that. It works with any adapter, because it relies only on `@nestjs-cls/transactional`'s rollback-on-throw, and it is tested against TypeORM:
126
+
127
+ ```ts
128
+ import { Injectable } from '@nestjs/common';
129
+ import { TransactionHost } from '@nestjs-cls/transactional';
130
+ import { TransactionalAdapterTypeOrm } from '@nestjs-cls/transactional-adapter-typeorm';
131
+ import { TransactionalResult } from 'nest-result/transactional';
132
+ import { err, type Result } from 'neverthrow';
133
+ import { DealEntity } from './deal.entity';
134
+ import { type Deal, DealAlreadyClosed, DealNotFound } from './deals';
135
+
136
+ @Injectable()
137
+ export class DealsService {
138
+ constructor(private readonly txHost: TransactionHost<TransactionalAdapterTypeOrm>) {}
139
+
140
+ @TransactionalResult()
141
+ async close(id: string): Promise<Result<Deal, DealNotFound | DealAlreadyClosed>> {
142
+ await this.txHost.tx.getRepository(DealEntity).update(id, { closed: true });
143
+ return err(new DealAlreadyClosed());
144
+ }
145
+ }
146
+ ```
147
+
148
+ `Ok` commits, `Err` rolls back and is returned unchanged, and a thrown exception rolls back and is rethrown. `@TransactionalResult` takes the same arguments as `@Transactional` and requires the method to return `Promise<Result>`. For the `ResultAsync` style, use the function form:
149
+
150
+ ```ts
151
+ withResultTransaction(this.txHost, () => this.repository.close(id), { propagation: Propagation.RequiresNew });
152
+ ```
153
+
154
+ ### Nested transactions
155
+
156
+ With the default `Required` propagation, an inner method joins the outer transaction and its `Err` is returned to the outer method as a value. The outer method decides:
157
+
158
+ - if it returns `Err` too, everything rolls back;
159
+ - if it recovers and returns `Ok`, everything commits, including the inner method's writes made before it failed.
160
+
161
+ To discard the inner work independently, use `Propagation.Nested` (a savepoint) or `Propagation.RequiresNew`.
162
+
163
+ ## Migrating from a hand-rolled interceptor
164
+
165
+ 1. Replace your error base class with `TaggedError(...)` subclasses, or add a literal `readonly _tag` to existing errors.
166
+ 2. Add `ResultModule.forRoot()` to your root module and delete your interceptor.
167
+ 3. Add `@MapErrors({...})` to each route that returns a Result; the compiler lists every tag you still need to map.
168
+ 4. Remove `HttpException`s from your domain code; map domain errors at the controller instead.
169
+
170
+ ## Testing with Jest
171
+
172
+ The package's implementation is ES modules. A CommonJS Jest setup, the NestJS 11 default, cannot `require` it unless Jest runs with Node's VM modules support. Start Jest through Node with the flag:
173
+
174
+ ```json
175
+ {
176
+ "scripts": {
177
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
178
+ }
179
+ }
180
+ ```
181
+
182
+ ## Requirements
183
+
184
+ Node 22.12+, TypeScript 5.5+, `experimentalDecorators`, NestJS 11 or 12, neverthrow 8.
185
+
186
+ A CommonJS host on NestJS 12 needs a TypeScript version and `moduleResolution` that understand `require` of ES modules: TypeScript 5.8+ with `module: nodenext`. Older combinations report TS1479 on NestJS's own imports before they reach this package.
187
+
188
+ On Node 22.12 a CommonJS host prints an `ExperimentalWarning` when it loads the package, because the CommonJS entry points `require()` the ES module build. The warning is harmless.
189
+
190
+ ## Example
191
+
192
+ `examples/deals-api` is a runnable app. Build it against the packed package with `npm run example:build`, then start it with `npm --prefix examples/deals-api start`.
193
+
194
+ ## License
195
+
196
+ MIT
@@ -0,0 +1,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ export {
5
+ __name
6
+ };
7
+ //# sourceMappingURL=chunk-7QVYU63E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,7 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+
5
+
6
+ exports.__name = __name;
7
+ //# sourceMappingURL=chunk-PK6SKIKE.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/mannkostir/Documents/nodejs-result/dist/chunk-PK6SKIKE.cjs"],"names":[],"mappings":"AAAA,6EAAI,UAAU,EAAE,MAAM,CAAC,cAAc;AACrC,IAAI,OAAO,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;AACxF;AACA;AACE;AACF,wBAAC","file":"/Users/mannkostir/Documents/nodejs-result/dist/chunk-PK6SKIKE.cjs"}
@@ -0,0 +1,234 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/core/tagged-error.ts
6
+ function TaggedError(tag) {
7
+ let TaggedErrorBase = class TaggedErrorBase extends Error {
8
+ static {
9
+ __name(this, "TaggedErrorBase");
10
+ }
11
+ _tag;
12
+ constructor(payload) {
13
+ super(payload?.message ?? tag);
14
+ Object.assign(this, payload);
15
+ this._tag = tag;
16
+ this.name = tag;
17
+ }
18
+ };
19
+ return TaggedErrorBase;
20
+ }
21
+ __name(TaggedError, "TaggedError");
22
+
23
+ // src/core/library-errors.ts
24
+ var UnmappedErrorTagError = class _UnmappedErrorTagError extends TaggedError("UnmappedErrorTagError") {
25
+ static {
26
+ __name(this, "UnmappedErrorTagError");
27
+ }
28
+ static forTag(tag) {
29
+ return new _UnmappedErrorTagError({
30
+ tag,
31
+ message: `No HTTP mapping exists for error tag "${tag}"`
32
+ });
33
+ }
34
+ };
35
+ var UntaggedErrorValueError = class _UntaggedErrorValueError extends TaggedError("UntaggedErrorValueError") {
36
+ static {
37
+ __name(this, "UntaggedErrorValueError");
38
+ }
39
+ static forValue(value) {
40
+ return new _UntaggedErrorValueError({
41
+ value,
42
+ message: "An Err value without a string _tag cannot be mapped to HTTP"
43
+ });
44
+ }
45
+ };
46
+ var MissingErrorMapError = class _MissingErrorMapError extends TaggedError("MissingErrorMapError") {
47
+ static {
48
+ __name(this, "MissingErrorMapError");
49
+ }
50
+ static forHandler(handler) {
51
+ return new _MissingErrorMapError({
52
+ handler,
53
+ message: `${handler} returned a Result but has no @MapErrors`
54
+ });
55
+ }
56
+ };
57
+ var DuplicateNeverthrowError = class _DuplicateNeverthrowError extends TaggedError("DuplicateNeverthrowError") {
58
+ static {
59
+ __name(this, "DuplicateNeverthrowError");
60
+ }
61
+ static forHandler(handler) {
62
+ return new _DuplicateNeverthrowError({
63
+ handler,
64
+ message: `${handler} returned a Result from a different copy of neverthrow; deduplicate neverthrow in your dependency tree`
65
+ });
66
+ }
67
+ };
68
+
69
+ // src/http/result.interceptor.ts
70
+ import { Inject, Injectable } from "@nestjs/common";
71
+ import { Reflector } from "@nestjs/core";
72
+ import { from, mergeMap } from "rxjs";
73
+
74
+ // src/core/result-detection.ts
75
+ import { Err, Ok, ResultAsync } from "neverthrow";
76
+ function isResult(value) {
77
+ return value instanceof Ok || value instanceof Err;
78
+ }
79
+ __name(isResult, "isResult");
80
+ function isResultAsync(value) {
81
+ return value instanceof ResultAsync;
82
+ }
83
+ __name(isResultAsync, "isResultAsync");
84
+ function looksLikeForeignResult(value) {
85
+ if (typeof value !== "object" || value === null) return false;
86
+ const candidate = value;
87
+ const hasSyncShape = typeof candidate["isOk"] === "function" && typeof candidate["isErr"] === "function";
88
+ const hasAsyncShape = typeof candidate["then"] === "function" && typeof candidate["andThen"] === "function" && typeof candidate["mapErr"] === "function";
89
+ return hasSyncShape || hasAsyncShape;
90
+ }
91
+ __name(looksLikeForeignResult, "looksLikeForeignResult");
92
+
93
+ // src/http/error-map-metadata.ts
94
+ var ERROR_MAP_METADATA = "nest-result:error-map";
95
+
96
+ // src/http/exception-for.ts
97
+ import { HttpException } from "@nestjs/common";
98
+
99
+ // src/core/resolve-http-error.ts
100
+ import { err, ok } from "neverthrow";
101
+ function resolveHttpError(error, map) {
102
+ const tag = readTag(error);
103
+ if (tag === void 0) return err(UntaggedErrorValueError.forValue(error));
104
+ const spec = Object.hasOwn(map, tag) ? map[tag] : void 0;
105
+ if (spec === void 0) return err(UnmappedErrorTagError.forTag(tag));
106
+ return ok(toResponse(spec, tag, error));
107
+ }
108
+ __name(resolveHttpError, "resolveHttpError");
109
+ function statusOf(spec) {
110
+ return typeof spec === "number" ? spec : spec.status;
111
+ }
112
+ __name(statusOf, "statusOf");
113
+ function toResponse(spec, tag, error) {
114
+ if (typeof spec === "number") return {
115
+ status: spec,
116
+ body: defaultBody(spec, tag, error)
117
+ };
118
+ return {
119
+ status: spec.status,
120
+ body: spec.body(error)
121
+ };
122
+ }
123
+ __name(toResponse, "toResponse");
124
+ function defaultBody(status, tag, error) {
125
+ return {
126
+ statusCode: status,
127
+ code: tag,
128
+ message: readMessage(error) ?? tag
129
+ };
130
+ }
131
+ __name(defaultBody, "defaultBody");
132
+ function readTag(error) {
133
+ if (typeof error !== "object" || error === null) return void 0;
134
+ const tag = error._tag;
135
+ return typeof tag === "string" ? tag : void 0;
136
+ }
137
+ __name(readTag, "readTag");
138
+ function readMessage(error) {
139
+ const message = error.message;
140
+ return typeof message === "string" && message.length > 0 ? message : void 0;
141
+ }
142
+ __name(readMessage, "readMessage");
143
+
144
+ // src/http/exception-for.ts
145
+ function exceptionFor(error, map) {
146
+ return resolveHttpError(error, map).match(({ status, body }) => new HttpException(body, status, {
147
+ cause: error
148
+ }), (libraryError) => libraryError);
149
+ }
150
+ __name(exceptionFor, "exceptionFor");
151
+
152
+ // src/http/result.interceptor.ts
153
+ function _ts_decorate(decorators, target, key, desc) {
154
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
155
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
156
+ r = Reflect.decorate(decorators, target, key, desc);
157
+ } else {
158
+ for (var i = decorators.length - 1; i >= 0; i--) {
159
+ if (d = decorators[i]) {
160
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
161
+ }
162
+ }
163
+ }
164
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
165
+ }
166
+ __name(_ts_decorate, "_ts_decorate");
167
+ function _ts_metadata(metadataKey, metadataValue) {
168
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
169
+ return Reflect.metadata(metadataKey, metadataValue);
170
+ }
171
+ }
172
+ __name(_ts_metadata, "_ts_metadata");
173
+ function _ts_param(paramIndex, decorator) {
174
+ return function(target, key) {
175
+ decorator(target, key, paramIndex);
176
+ };
177
+ }
178
+ __name(_ts_param, "_ts_param");
179
+ var ResultInterceptor = class {
180
+ static {
181
+ __name(this, "ResultInterceptor");
182
+ }
183
+ reflector;
184
+ constructor(reflector) {
185
+ this.reflector = reflector;
186
+ }
187
+ intercept(context, next) {
188
+ const map = this.reflector.get(ERROR_MAP_METADATA, context.getHandler());
189
+ const handler = `${context.getClass().name}.${context.getHandler().name}`;
190
+ return next.handle().pipe(mergeMap((value) => from(unwrapResponse(value, map, handler))));
191
+ }
192
+ };
193
+ ResultInterceptor = _ts_decorate([
194
+ Injectable(),
195
+ _ts_param(0, Inject(Reflector)),
196
+ _ts_metadata("design:type", Function),
197
+ _ts_metadata("design:paramtypes", [
198
+ typeof Reflector === "undefined" ? Object : Reflector
199
+ ])
200
+ ], ResultInterceptor);
201
+ async function unwrapResponse(value, map, handler) {
202
+ const settled = isResultAsync(value) ? await value : value;
203
+ if (!isResult(settled)) return passThrough(settled, handler);
204
+ if (map === void 0) throw MissingErrorMapError.forHandler(handler);
205
+ if (settled.isErr()) throw exceptionFor(settled.error, map);
206
+ return settled.value;
207
+ }
208
+ __name(unwrapResponse, "unwrapResponse");
209
+ function passThrough(value, handler) {
210
+ if (looksLikeForeignResult(value)) throw DuplicateNeverthrowError.forHandler(handler);
211
+ return value;
212
+ }
213
+ __name(passThrough, "passThrough");
214
+
215
+ // src/http/apply-error-map.ts
216
+ import { SetMetadata, UseInterceptors } from "@nestjs/common";
217
+ function applyErrorMap(map, target, key, descriptor) {
218
+ SetMetadata(ERROR_MAP_METADATA, map)(target, key, descriptor);
219
+ UseInterceptors(ResultInterceptor)(target, key, descriptor);
220
+ }
221
+ __name(applyErrorMap, "applyErrorMap");
222
+
223
+ export {
224
+ TaggedError,
225
+ UnmappedErrorTagError,
226
+ UntaggedErrorValueError,
227
+ MissingErrorMapError,
228
+ DuplicateNeverthrowError,
229
+ statusOf,
230
+ exceptionFor,
231
+ ResultInterceptor,
232
+ applyErrorMap
233
+ };
234
+ //# sourceMappingURL=chunk-T2LT5Y2F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/tagged-error.ts","../src/core/library-errors.ts","../src/http/result.interceptor.ts","../src/core/result-detection.ts","../src/http/error-map-metadata.ts","../src/http/exception-for.ts","../src/core/resolve-http-error.ts","../src/http/apply-error-map.ts"],"sourcesContent":["export type TaggedErrorPayload = object & {\n readonly message?: string;\n readonly _tag?: never;\n readonly name?: never;\n};\n\ntype ConstructorArgs<P> = {} extends P ? [payload?: P] : [payload: P];\n\nexport type TaggedErrorInstance<Tag extends string, P> = Error & {\n readonly _tag: Tag;\n} & Readonly<Omit<P, 'message'>>;\n\nexport type TaggedErrorClass<Tag extends string> = new <P extends TaggedErrorPayload = {}>(\n ...args: ConstructorArgs<P>\n) => TaggedErrorInstance<Tag, P>;\n\nexport function TaggedError<const Tag extends string>(tag: Tag): TaggedErrorClass<Tag> {\n class TaggedErrorBase extends Error {\n readonly _tag: Tag;\n\n constructor(payload?: TaggedErrorPayload) {\n super(payload?.message ?? tag);\n Object.assign(this, payload);\n this._tag = tag;\n this.name = tag;\n }\n }\n return TaggedErrorBase as TaggedErrorClass<Tag>;\n}\n","import { TaggedError } from './tagged-error.js';\n\nexport class UnmappedErrorTagError extends TaggedError('UnmappedErrorTagError')<{\n readonly tag: string;\n readonly message: string;\n}> {\n static forTag(tag: string): UnmappedErrorTagError {\n return new UnmappedErrorTagError({ tag, message: `No HTTP mapping exists for error tag \"${tag}\"` });\n }\n}\n\nexport class UntaggedErrorValueError extends TaggedError('UntaggedErrorValueError')<{\n readonly value: unknown;\n readonly message: string;\n}> {\n static forValue(value: unknown): UntaggedErrorValueError {\n return new UntaggedErrorValueError({ value, message: 'An Err value without a string _tag cannot be mapped to HTTP' });\n }\n}\n\nexport class MissingErrorMapError extends TaggedError('MissingErrorMapError')<{\n readonly handler: string;\n readonly message: string;\n}> {\n static forHandler(handler: string): MissingErrorMapError {\n return new MissingErrorMapError({ handler, message: `${handler} returned a Result but has no @MapErrors` });\n }\n}\n\nexport class DuplicateNeverthrowError extends TaggedError('DuplicateNeverthrowError')<{\n readonly handler: string;\n readonly message: string;\n}> {\n static forHandler(handler: string): DuplicateNeverthrowError {\n return new DuplicateNeverthrowError({\n handler,\n message: `${handler} returned a Result from a different copy of neverthrow; deduplicate neverthrow in your dependency tree`,\n });\n }\n}\n","import { type CallHandler, type ExecutionContext, Inject, Injectable, type NestInterceptor } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { from, mergeMap, type Observable } from 'rxjs';\nimport type { AnyErrorMap } from '../core/error-map.js';\nimport { DuplicateNeverthrowError, MissingErrorMapError } from '../core/library-errors.js';\nimport { isResult, isResultAsync, looksLikeForeignResult } from '../core/result-detection.js';\nimport { ERROR_MAP_METADATA } from './error-map-metadata.js';\nimport { exceptionFor } from './exception-for.js';\n\n@Injectable()\nexport class ResultInterceptor implements NestInterceptor {\n constructor(@Inject(Reflector) private readonly reflector: Reflector) {}\n\n intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {\n const map = this.reflector.get<AnyErrorMap | undefined>(ERROR_MAP_METADATA, context.getHandler());\n const handler = `${context.getClass().name}.${context.getHandler().name}`;\n return next.handle().pipe(mergeMap((value: unknown) => from(unwrapResponse(value, map, handler))));\n }\n}\n\nasync function unwrapResponse(value: unknown, map: AnyErrorMap | undefined, handler: string): Promise<unknown> {\n const settled = isResultAsync(value) ? await value : value;\n if (!isResult(settled)) return passThrough(settled, handler);\n if (map === undefined) throw MissingErrorMapError.forHandler(handler);\n if (settled.isErr()) throw exceptionFor(settled.error, map);\n return settled.value;\n}\n\nfunction passThrough(value: unknown, handler: string): unknown {\n if (looksLikeForeignResult(value)) throw DuplicateNeverthrowError.forHandler(handler);\n return value;\n}\n","import { Err, Ok, ResultAsync, type Result } from 'neverthrow';\n\nexport function isResult(value: unknown): value is Result<unknown, unknown> {\n return value instanceof Ok || value instanceof Err;\n}\n\nexport function isResultAsync(value: unknown): value is ResultAsync<unknown, unknown> {\n return value instanceof ResultAsync;\n}\n\nexport function looksLikeForeignResult(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Readonly<Record<string, unknown>>;\n const hasSyncShape = typeof candidate['isOk'] === 'function' && typeof candidate['isErr'] === 'function';\n const hasAsyncShape =\n typeof candidate['then'] === 'function' &&\n typeof candidate['andThen'] === 'function' &&\n typeof candidate['mapErr'] === 'function';\n return hasSyncShape || hasAsyncShape;\n}\n","export const ERROR_MAP_METADATA = 'nest-result:error-map';\n","import { HttpException } from '@nestjs/common';\nimport type { AnyErrorMap } from '../core/error-map.js';\nimport type { UnmappedErrorTagError, UntaggedErrorValueError } from '../core/library-errors.js';\nimport { resolveHttpError } from '../core/resolve-http-error.js';\n\nexport function exceptionFor(\n error: unknown,\n map: AnyErrorMap,\n): HttpException | UnmappedErrorTagError | UntaggedErrorValueError {\n return resolveHttpError(error, map).match(\n ({ status, body }) => new HttpException(body, status, { cause: error }),\n (libraryError) => libraryError,\n );\n}\n","import { err, ok, type Result } from 'neverthrow';\nimport type { AnyErrorMap, AnyHttpErrorSpec } from './error-map.js';\nimport { UnmappedErrorTagError, UntaggedErrorValueError } from './library-errors.js';\n\nexport type HttpErrorResponse = { readonly status: number; readonly body: object };\n\nexport function resolveHttpError(\n error: unknown,\n map: AnyErrorMap,\n): Result<HttpErrorResponse, UnmappedErrorTagError | UntaggedErrorValueError> {\n const tag = readTag(error);\n if (tag === undefined) return err(UntaggedErrorValueError.forValue(error));\n const spec = Object.hasOwn(map, tag) ? map[tag] : undefined;\n if (spec === undefined) return err(UnmappedErrorTagError.forTag(tag));\n return ok(toResponse(spec, tag, error));\n}\n\nexport function statusOf(spec: AnyHttpErrorSpec): number {\n return typeof spec === 'number' ? spec : spec.status;\n}\n\nfunction toResponse(spec: AnyHttpErrorSpec, tag: string, error: unknown): HttpErrorResponse {\n if (typeof spec === 'number') return { status: spec, body: defaultBody(spec, tag, error) };\n return { status: spec.status, body: spec.body(error as never) };\n}\n\nfunction defaultBody(status: number, tag: string, error: unknown): object {\n return { statusCode: status, code: tag, message: readMessage(error) ?? tag };\n}\n\nfunction readTag(error: unknown): string | undefined {\n if (typeof error !== 'object' || error === null) return undefined;\n const tag: unknown = (error as { readonly _tag?: unknown })._tag;\n return typeof tag === 'string' ? tag : undefined;\n}\n\nfunction readMessage(error: unknown): string | undefined {\n const message: unknown = (error as { readonly message?: unknown }).message;\n return typeof message === 'string' && message.length > 0 ? message : undefined;\n}\n","import { SetMetadata, UseInterceptors } from '@nestjs/common';\nimport type { AnyErrorMap } from '../core/error-map.js';\nimport { ERROR_MAP_METADATA } from './error-map-metadata.js';\nimport { ResultInterceptor } from './result.interceptor.js';\n\nexport function applyErrorMap(\n map: AnyErrorMap,\n target: object,\n key: string | symbol,\n descriptor: PropertyDescriptor,\n): void {\n SetMetadata(ERROR_MAP_METADATA, map)(target, key, descriptor);\n UseInterceptors(ResultInterceptor)(target, key, descriptor);\n}\n"],"mappings":"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","names":["TaggedError","tag","TaggedErrorBase","Error","_tag","payload","message","Object","assign","name","UnmappedErrorTagError","TaggedError","forTag","tag","message","UntaggedErrorValueError","forValue","value","MissingErrorMapError","forHandler","handler","DuplicateNeverthrowError","Inject","Injectable","Reflector","from","mergeMap","Err","Ok","ResultAsync","isResult","value","Ok","Err","isResultAsync","ResultAsync","looksLikeForeignResult","candidate","hasSyncShape","hasAsyncShape","ERROR_MAP_METADATA","HttpException","err","ok","resolveHttpError","error","map","tag","readTag","undefined","err","UntaggedErrorValueError","forValue","spec","Object","hasOwn","UnmappedErrorTagError","forTag","ok","toResponse","statusOf","status","body","defaultBody","statusCode","code","message","readMessage","_tag","length","exceptionFor","error","map","resolveHttpError","match","status","body","HttpException","cause","libraryError","ResultInterceptor","reflector","intercept","context","next","map","get","ERROR_MAP_METADATA","getHandler","handler","getClass","name","handle","pipe","mergeMap","value","from","unwrapResponse","settled","isResultAsync","isResult","passThrough","undefined","MissingErrorMapError","forHandler","isErr","exceptionFor","error","looksLikeForeignResult","DuplicateNeverthrowError","SetMetadata","UseInterceptors","applyErrorMap","map","target","key","descriptor","SetMetadata","ERROR_MAP_METADATA","UseInterceptors","ResultInterceptor"]}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./index.js');
@@ -0,0 +1,61 @@
1
+ import { E as ErrorMap, R as ResultSource, a as ExactErrorMap } from './map-errors.decorator-_ik4VDK6.cjs';
2
+ export { b as ErrorBodyParameterMismatch, H as HttpErrorSpec, M as MapErrors, c as MapErrorsDecorator, d as MissingErrorMapKeys, S as StaleErrorMapKeys, T as TagOf, e as Tagged, U as UntaggedErrorsCannotBeMapped } from './map-errors.decorator-_ik4VDK6.cjs';
3
+ import { NestInterceptor, ExecutionContext, CallHandler, DynamicModule } from '@nestjs/common';
4
+ import { Reflector } from '@nestjs/core';
5
+ import { Observable } from 'rxjs';
6
+ import 'neverthrow';
7
+
8
+ type TaggedErrorPayload = object & {
9
+ readonly message?: string;
10
+ readonly _tag?: never;
11
+ readonly name?: never;
12
+ };
13
+ type ConstructorArgs<P> = {} extends P ? [payload?: P] : [payload: P];
14
+ type TaggedErrorInstance<Tag extends string, P> = Error & {
15
+ readonly _tag: Tag;
16
+ } & Readonly<Omit<P, 'message'>>;
17
+ type TaggedErrorClass<Tag extends string> = new <P extends TaggedErrorPayload = {}>(...args: ConstructorArgs<P>) => TaggedErrorInstance<Tag, P>;
18
+ declare function TaggedError<const Tag extends string>(tag: Tag): TaggedErrorClass<Tag>;
19
+
20
+ declare const UnmappedErrorTagError_base: TaggedErrorClass<"UnmappedErrorTagError">;
21
+ declare class UnmappedErrorTagError extends UnmappedErrorTagError_base<{
22
+ readonly tag: string;
23
+ readonly message: string;
24
+ }> {
25
+ static forTag(tag: string): UnmappedErrorTagError;
26
+ }
27
+ declare const UntaggedErrorValueError_base: TaggedErrorClass<"UntaggedErrorValueError">;
28
+ declare class UntaggedErrorValueError extends UntaggedErrorValueError_base<{
29
+ readonly value: unknown;
30
+ readonly message: string;
31
+ }> {
32
+ static forValue(value: unknown): UntaggedErrorValueError;
33
+ }
34
+ declare const MissingErrorMapError_base: TaggedErrorClass<"MissingErrorMapError">;
35
+ declare class MissingErrorMapError extends MissingErrorMapError_base<{
36
+ readonly handler: string;
37
+ readonly message: string;
38
+ }> {
39
+ static forHandler(handler: string): MissingErrorMapError;
40
+ }
41
+ declare const DuplicateNeverthrowError_base: TaggedErrorClass<"DuplicateNeverthrowError">;
42
+ declare class DuplicateNeverthrowError extends DuplicateNeverthrowError_base<{
43
+ readonly handler: string;
44
+ readonly message: string;
45
+ }> {
46
+ static forHandler(handler: string): DuplicateNeverthrowError;
47
+ }
48
+
49
+ declare function toHttp<T, E, const M extends ErrorMap<E>>(source: ResultSource<T, E>, map: ExactErrorMap<E, M>): Promise<T>;
50
+
51
+ declare class ResultInterceptor implements NestInterceptor {
52
+ private readonly reflector;
53
+ constructor(reflector: Reflector);
54
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
55
+ }
56
+
57
+ declare class ResultModule {
58
+ static forRoot(): DynamicModule;
59
+ }
60
+
61
+ export { DuplicateNeverthrowError, ErrorMap, MissingErrorMapError, ResultInterceptor, ResultModule, TaggedError, type TaggedErrorClass, type TaggedErrorInstance, type TaggedErrorPayload, UnmappedErrorTagError, UntaggedErrorValueError, toHttp };
@@ -0,0 +1,61 @@
1
+ import { E as ErrorMap, R as ResultSource, a as ExactErrorMap } from './map-errors.decorator-_ik4VDK6.js';
2
+ export { b as ErrorBodyParameterMismatch, H as HttpErrorSpec, M as MapErrors, c as MapErrorsDecorator, d as MissingErrorMapKeys, S as StaleErrorMapKeys, T as TagOf, e as Tagged, U as UntaggedErrorsCannotBeMapped } from './map-errors.decorator-_ik4VDK6.js';
3
+ import { NestInterceptor, ExecutionContext, CallHandler, DynamicModule } from '@nestjs/common';
4
+ import { Reflector } from '@nestjs/core';
5
+ import { Observable } from 'rxjs';
6
+ import 'neverthrow';
7
+
8
+ type TaggedErrorPayload = object & {
9
+ readonly message?: string;
10
+ readonly _tag?: never;
11
+ readonly name?: never;
12
+ };
13
+ type ConstructorArgs<P> = {} extends P ? [payload?: P] : [payload: P];
14
+ type TaggedErrorInstance<Tag extends string, P> = Error & {
15
+ readonly _tag: Tag;
16
+ } & Readonly<Omit<P, 'message'>>;
17
+ type TaggedErrorClass<Tag extends string> = new <P extends TaggedErrorPayload = {}>(...args: ConstructorArgs<P>) => TaggedErrorInstance<Tag, P>;
18
+ declare function TaggedError<const Tag extends string>(tag: Tag): TaggedErrorClass<Tag>;
19
+
20
+ declare const UnmappedErrorTagError_base: TaggedErrorClass<"UnmappedErrorTagError">;
21
+ declare class UnmappedErrorTagError extends UnmappedErrorTagError_base<{
22
+ readonly tag: string;
23
+ readonly message: string;
24
+ }> {
25
+ static forTag(tag: string): UnmappedErrorTagError;
26
+ }
27
+ declare const UntaggedErrorValueError_base: TaggedErrorClass<"UntaggedErrorValueError">;
28
+ declare class UntaggedErrorValueError extends UntaggedErrorValueError_base<{
29
+ readonly value: unknown;
30
+ readonly message: string;
31
+ }> {
32
+ static forValue(value: unknown): UntaggedErrorValueError;
33
+ }
34
+ declare const MissingErrorMapError_base: TaggedErrorClass<"MissingErrorMapError">;
35
+ declare class MissingErrorMapError extends MissingErrorMapError_base<{
36
+ readonly handler: string;
37
+ readonly message: string;
38
+ }> {
39
+ static forHandler(handler: string): MissingErrorMapError;
40
+ }
41
+ declare const DuplicateNeverthrowError_base: TaggedErrorClass<"DuplicateNeverthrowError">;
42
+ declare class DuplicateNeverthrowError extends DuplicateNeverthrowError_base<{
43
+ readonly handler: string;
44
+ readonly message: string;
45
+ }> {
46
+ static forHandler(handler: string): DuplicateNeverthrowError;
47
+ }
48
+
49
+ declare function toHttp<T, E, const M extends ErrorMap<E>>(source: ResultSource<T, E>, map: ExactErrorMap<E, M>): Promise<T>;
50
+
51
+ declare class ResultInterceptor implements NestInterceptor {
52
+ private readonly reflector;
53
+ constructor(reflector: Reflector);
54
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
55
+ }
56
+
57
+ declare class ResultModule {
58
+ static forRoot(): DynamicModule;
59
+ }
60
+
61
+ export { DuplicateNeverthrowError, ErrorMap, MissingErrorMapError, ResultInterceptor, ResultModule, TaggedError, type TaggedErrorClass, type TaggedErrorInstance, type TaggedErrorPayload, UnmappedErrorTagError, UntaggedErrorValueError, toHttp };