@apisr/response 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/package.json +31 -0
- package/src/error/default.ts +122 -0
- package/src/error/index.ts +66 -0
- package/src/handler.ts +482 -0
- package/src/headers.ts +21 -0
- package/src/index.ts +7 -0
- package/src/options/base.ts +36 -0
- package/src/options/binary.ts +14 -0
- package/src/options/error.ts +53 -0
- package/src/options/index.ts +17 -0
- package/src/options/json.ts +45 -0
- package/src/options/meta.ts +33 -0
- package/src/response/base.ts +23 -0
- package/src/response/binary/index.ts +11 -0
- package/src/response/default.ts +8 -0
- package/src/response/error/index.ts +42 -0
- package/src/response/index.ts +7 -0
- package/src/response/json/index.ts +44 -0
- package/src/response/meta/index.ts +4 -0
- package/src/response/text/index.ts +9 -0
- package/src/symbol.ts +1 -0
- package/src/types.ts +9 -0
- package/tests/index.test.ts +204 -0
- package/tests/json-symbol.test.ts +14 -0
- package/tsconfig.json +34 -0
- package/tsdown.config.ts +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apisr/response",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"module": "src/index.ts",
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@repo/eslint-config": "*",
|
|
7
|
+
"@repo/typescript-config": "*",
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
"@types/node": "^22.15.3",
|
|
10
|
+
"dotenv": "^17.2.3",
|
|
11
|
+
"eslint": "^9.39.1",
|
|
12
|
+
"typescript": "5.9.2"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@apisr/schema": "",
|
|
16
|
+
"@apisr/zod": ""
|
|
17
|
+
},
|
|
18
|
+
"private": false,
|
|
19
|
+
"scripts": {
|
|
20
|
+
"lint": "eslint . --max-warnings 0",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"type": "module"
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from "@apisr/zod";
|
|
2
|
+
import type { ErrorOptions, Options } from "@/options";
|
|
3
|
+
import type { ErrorRegistry } from ".";
|
|
4
|
+
|
|
5
|
+
export function generateDefaultErrors<TOptions extends Options>(
|
|
6
|
+
_mapDefaultError: ErrorOptions.Base["mapDefaultError"]
|
|
7
|
+
): ErrorRegistry<TOptions> {
|
|
8
|
+
const mapDefaultError = _mapDefaultError ?? ((err) => err);
|
|
9
|
+
|
|
10
|
+
const inputSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
cause: z.any().optional(),
|
|
13
|
+
})
|
|
14
|
+
.optional();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
unauthorized: {
|
|
18
|
+
handler: ({ input }) =>
|
|
19
|
+
mapDefaultError({
|
|
20
|
+
message: "Unauthorized",
|
|
21
|
+
code: "UNAUTHORIZED",
|
|
22
|
+
name: "UnauthorizedError",
|
|
23
|
+
cause: input.cause,
|
|
24
|
+
}),
|
|
25
|
+
options: {
|
|
26
|
+
input: inputSchema,
|
|
27
|
+
status: 401,
|
|
28
|
+
statusText: "Unauthorized",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
forbidden: {
|
|
33
|
+
handler: ({ input }) =>
|
|
34
|
+
mapDefaultError({
|
|
35
|
+
message: "Forbidden",
|
|
36
|
+
code: "FORBIDDEN",
|
|
37
|
+
name: "ForbiddenError",
|
|
38
|
+
cause: input.cause,
|
|
39
|
+
}),
|
|
40
|
+
options: {
|
|
41
|
+
input: inputSchema,
|
|
42
|
+
status: 403,
|
|
43
|
+
statusText: "Forbidden",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
notFound: {
|
|
48
|
+
handler: ({ input }) =>
|
|
49
|
+
mapDefaultError({
|
|
50
|
+
message: "Not Found",
|
|
51
|
+
code: "NOT_FOUND",
|
|
52
|
+
name: "NotFoundError",
|
|
53
|
+
cause: input.cause,
|
|
54
|
+
}),
|
|
55
|
+
options: {
|
|
56
|
+
input: inputSchema,
|
|
57
|
+
status: 404,
|
|
58
|
+
statusText: "Not Found",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
badRequest: {
|
|
63
|
+
handler: ({ input }) =>
|
|
64
|
+
mapDefaultError({
|
|
65
|
+
message: "Bad Request",
|
|
66
|
+
code: "BAD_REQUEST",
|
|
67
|
+
name: "BadRequestError",
|
|
68
|
+
cause: input.cause,
|
|
69
|
+
}),
|
|
70
|
+
options: {
|
|
71
|
+
input: inputSchema,
|
|
72
|
+
status: 400,
|
|
73
|
+
statusText: "Bad Request",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
conflict: {
|
|
78
|
+
handler: ({ input }) =>
|
|
79
|
+
mapDefaultError({
|
|
80
|
+
message: "Conflict",
|
|
81
|
+
code: "CONFLICT",
|
|
82
|
+
name: "ConflictError",
|
|
83
|
+
cause: input.cause,
|
|
84
|
+
}),
|
|
85
|
+
options: {
|
|
86
|
+
input: inputSchema,
|
|
87
|
+
status: 409,
|
|
88
|
+
statusText: "Conflict",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
tooMany: {
|
|
93
|
+
handler: ({ input }) =>
|
|
94
|
+
mapDefaultError({
|
|
95
|
+
message: "Too Many Requests",
|
|
96
|
+
code: "TOO_MANY_REQUESTS",
|
|
97
|
+
name: "TooManyRequestsError",
|
|
98
|
+
cause: input.cause,
|
|
99
|
+
}),
|
|
100
|
+
options: {
|
|
101
|
+
input: inputSchema,
|
|
102
|
+
status: 429,
|
|
103
|
+
statusText: "Too Many Requests",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
internal: {
|
|
108
|
+
handler: ({ input }) =>
|
|
109
|
+
mapDefaultError({
|
|
110
|
+
message: "Internal Server Error",
|
|
111
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
112
|
+
name: "InternalServerError",
|
|
113
|
+
cause: input.cause,
|
|
114
|
+
}),
|
|
115
|
+
options: {
|
|
116
|
+
input: inputSchema,
|
|
117
|
+
status: 500,
|
|
118
|
+
statusText: "Internal Server Error",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtractSchema,
|
|
3
|
+
Infer,
|
|
4
|
+
Schema,
|
|
5
|
+
ValidationType,
|
|
6
|
+
} from "@apisr/schema";
|
|
7
|
+
import type { ErrorOptions, Options } from "@/options";
|
|
8
|
+
|
|
9
|
+
export * from "./default";
|
|
10
|
+
|
|
11
|
+
// Start of types ------------------
|
|
12
|
+
|
|
13
|
+
export type DefaultErrorTypes =
|
|
14
|
+
| "unauthorized"
|
|
15
|
+
| "forbidden"
|
|
16
|
+
| "notFound"
|
|
17
|
+
| "badRequest"
|
|
18
|
+
| "conflict"
|
|
19
|
+
| "tooMany"
|
|
20
|
+
| "internal";
|
|
21
|
+
|
|
22
|
+
export type DefaultError = {
|
|
23
|
+
message: string;
|
|
24
|
+
code: string;
|
|
25
|
+
name: string;
|
|
26
|
+
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
stack?: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ErrorHandler<
|
|
32
|
+
TOptions extends Options,
|
|
33
|
+
THandlerOptions extends ErrorHandlerOptions | undefined,
|
|
34
|
+
> = (data: {
|
|
35
|
+
meta: TOptions["meta"] extends undefined
|
|
36
|
+
? never
|
|
37
|
+
: Infer<ExtractSchema<TOptions["meta"]>>;
|
|
38
|
+
input: THandlerOptions extends undefined
|
|
39
|
+
? never
|
|
40
|
+
: Infer<Exclude<THandlerOptions, undefined>["input"]>;
|
|
41
|
+
}) => ErrorOptions.InferedSchema<TOptions>;
|
|
42
|
+
|
|
43
|
+
export interface ErrorHandlerOptions<TSchema extends Schema = Schema> {
|
|
44
|
+
input?: TSchema;
|
|
45
|
+
|
|
46
|
+
status?: number;
|
|
47
|
+
statusText?: string;
|
|
48
|
+
validationType?: ValidationType;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ErrorDefinition<
|
|
52
|
+
TOptions extends Options,
|
|
53
|
+
THandlerOptions extends ErrorHandlerOptions | undefined,
|
|
54
|
+
> = {
|
|
55
|
+
handler:
|
|
56
|
+
| ErrorHandler<TOptions, THandlerOptions>
|
|
57
|
+
| ErrorOptions.InferedSchema<TOptions>;
|
|
58
|
+
options: THandlerOptions;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type ErrorRegistry<TOptions extends Options> = Record<
|
|
62
|
+
string,
|
|
63
|
+
ErrorDefinition<TOptions, any>
|
|
64
|
+
>;
|
|
65
|
+
|
|
66
|
+
// End of types ------------------
|
package/src/handler.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { checkSchema, type Infer } from "@apisr/schema";
|
|
2
|
+
import type {
|
|
3
|
+
DefaultErrorTypes,
|
|
4
|
+
ErrorDefinition,
|
|
5
|
+
ErrorHandler,
|
|
6
|
+
ErrorHandlerOptions,
|
|
7
|
+
ErrorRegistry,
|
|
8
|
+
} from "./error";
|
|
9
|
+
import { generateDefaultErrors } from "./error/default";
|
|
10
|
+
import { resolveHeaders } from "./headers";
|
|
11
|
+
import type {
|
|
12
|
+
BinaryOptions,
|
|
13
|
+
ErrorOptions,
|
|
14
|
+
JsonOptions,
|
|
15
|
+
MetaOptions,
|
|
16
|
+
Options,
|
|
17
|
+
} from "./options";
|
|
18
|
+
import { options as optionMethods } from "./options";
|
|
19
|
+
import { BaseResponse } from "./response/base";
|
|
20
|
+
import { type Binary, BinaryResponse } from "./response/binary";
|
|
21
|
+
import type { DefaultResponse } from "./response/default";
|
|
22
|
+
import { ErrorResponse } from "./response/error";
|
|
23
|
+
import { JsonResponse } from "./response/json";
|
|
24
|
+
import { TextResponse } from "./response/text";
|
|
25
|
+
import type { PromiseOr } from "./types";
|
|
26
|
+
|
|
27
|
+
export function createResponseHandler<
|
|
28
|
+
TMeta extends MetaOptions.Base,
|
|
29
|
+
TError extends ErrorOptions.Base,
|
|
30
|
+
TJson extends JsonOptions.Base,
|
|
31
|
+
TBinary extends BinaryOptions.Base,
|
|
32
|
+
TOptions extends Options<TMeta, TError, TJson, TBinary> = Options<
|
|
33
|
+
TMeta,
|
|
34
|
+
TError,
|
|
35
|
+
TJson,
|
|
36
|
+
TBinary
|
|
37
|
+
>,
|
|
38
|
+
>(opts: TOptions | ((options: typeof optionMethods) => TOptions)) {
|
|
39
|
+
const _options = typeof opts === "function" ? opts(optionMethods) : opts;
|
|
40
|
+
|
|
41
|
+
return new ResponseHandler<TMeta, TError, TJson, TBinary, TOptions>({
|
|
42
|
+
options: _options,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ResolveOptionsMeta<TOptions extends Options> =
|
|
47
|
+
TOptions["meta"] extends undefined
|
|
48
|
+
? never
|
|
49
|
+
: Exclude<TOptions["meta"], undefined>;
|
|
50
|
+
|
|
51
|
+
export type AnyResponseHandler = ResponseHandler<any, any, any, any, any>;
|
|
52
|
+
|
|
53
|
+
export class ResponseHandler<
|
|
54
|
+
TMeta extends MetaOptions.Base,
|
|
55
|
+
TError extends ErrorOptions.Base,
|
|
56
|
+
TJson extends JsonOptions.Base,
|
|
57
|
+
TBinary extends BinaryOptions.Base,
|
|
58
|
+
TOptions extends Options<TMeta, TError, TJson, TBinary>,
|
|
59
|
+
TErrors extends ErrorRegistry<TOptions> = {},
|
|
60
|
+
> {
|
|
61
|
+
public options: TOptions;
|
|
62
|
+
|
|
63
|
+
private errors: TErrors;
|
|
64
|
+
private preasignedMeta: Partial<MetaOptions.InferedSchema<TOptions>>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a `ResponseHandler` instance.
|
|
68
|
+
*
|
|
69
|
+
* Prefer using {@link createResponseHandler} instead of calling this directly.
|
|
70
|
+
*
|
|
71
|
+
* @param params - Constructor params.
|
|
72
|
+
* @param params.options - Handler options.
|
|
73
|
+
* @param params.errors - Optional pre-defined error registry.
|
|
74
|
+
* @param params.preasignedMeta - Optional meta merged into each response.
|
|
75
|
+
*/
|
|
76
|
+
constructor({
|
|
77
|
+
options,
|
|
78
|
+
errors,
|
|
79
|
+
preasignedMeta,
|
|
80
|
+
}: {
|
|
81
|
+
options: TOptions;
|
|
82
|
+
errors?: TErrors;
|
|
83
|
+
preasignedMeta?: Partial<MetaOptions.InferedSchema<TOptions>>;
|
|
84
|
+
}) {
|
|
85
|
+
this.options = options;
|
|
86
|
+
this.errors =
|
|
87
|
+
errors ??
|
|
88
|
+
(generateDefaultErrors<TOptions>(
|
|
89
|
+
options?.error?.mapDefaultError
|
|
90
|
+
) as TErrors) ??
|
|
91
|
+
({} as TErrors);
|
|
92
|
+
this.preasignedMeta = preasignedMeta ?? {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fail<
|
|
96
|
+
TKey extends keyof TErrors & string,
|
|
97
|
+
TInput extends Infer<TErrors[TKey]["options"]["input"]>,
|
|
98
|
+
>(
|
|
99
|
+
name: TKey,
|
|
100
|
+
input?: TInput
|
|
101
|
+
): ErrorResponse.Base<TKey, MetaOptions.InferedSchema<TOptions>, TInput>;
|
|
102
|
+
|
|
103
|
+
fail(
|
|
104
|
+
name: DefaultErrorTypes,
|
|
105
|
+
input?: Record<string, any>
|
|
106
|
+
): ErrorResponse.Base<
|
|
107
|
+
DefaultErrorTypes,
|
|
108
|
+
MetaOptions.InferedSchema<TOptions>,
|
|
109
|
+
Record<string, any>
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create an `ErrorResponse` by name.
|
|
114
|
+
*
|
|
115
|
+
* If the error has an `input` schema, the `input` is validated according to `validationType`.
|
|
116
|
+
* The resulting error response also includes prepared `meta`.
|
|
117
|
+
*
|
|
118
|
+
* @param name - Error name (registered via `defineError` or built-in default types).
|
|
119
|
+
* @param _input - Optional error payload to validate (if a schema exists).
|
|
120
|
+
* @returns Error response instance.
|
|
121
|
+
*/
|
|
122
|
+
fail(name: string, _input?: unknown) {
|
|
123
|
+
// TODO: validate zod schema (input)
|
|
124
|
+
const error = this.errors[name];
|
|
125
|
+
const errorOptions = error?.options as ErrorHandlerOptions | undefined;
|
|
126
|
+
|
|
127
|
+
const input = errorOptions?.input
|
|
128
|
+
? checkSchema(errorOptions?.input, _input, {
|
|
129
|
+
validationType: errorOptions?.validationType ?? "parse",
|
|
130
|
+
})
|
|
131
|
+
: _input;
|
|
132
|
+
|
|
133
|
+
const meta = this.prepareMeta();
|
|
134
|
+
|
|
135
|
+
const handler = error?.handler;
|
|
136
|
+
const output =
|
|
137
|
+
typeof handler === "function"
|
|
138
|
+
? // @ts-expect-error
|
|
139
|
+
handler({ meta, input })
|
|
140
|
+
: handler;
|
|
141
|
+
|
|
142
|
+
const status = errorOptions?.status ?? 500;
|
|
143
|
+
const statusText = errorOptions?.statusText ?? "Unknown";
|
|
144
|
+
|
|
145
|
+
return new ErrorResponse.Base({
|
|
146
|
+
meta,
|
|
147
|
+
name,
|
|
148
|
+
output,
|
|
149
|
+
|
|
150
|
+
status,
|
|
151
|
+
statusText,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a JSON `Response`.
|
|
157
|
+
*
|
|
158
|
+
* Validates input/output using configured schemas (if present), applies `onData` transformation,
|
|
159
|
+
* and resolves/merges headers.
|
|
160
|
+
*
|
|
161
|
+
* @typeParam IInput - Inferred input type for configured JSON input schema.
|
|
162
|
+
* @param _input - JSON input value.
|
|
163
|
+
* @param options - Optional response init options.
|
|
164
|
+
* @returns JSON response instance.
|
|
165
|
+
*/
|
|
166
|
+
json<IInput extends JsonOptions.InferedSchema<TOptions>>(
|
|
167
|
+
_input: IInput,
|
|
168
|
+
options?: JsonResponse.Options
|
|
169
|
+
): JsonResponse.Base<IInput> {
|
|
170
|
+
const jsonOptions = this.options?.json;
|
|
171
|
+
|
|
172
|
+
// Check input by schema
|
|
173
|
+
// const input = jsonOptions?.inputSchema
|
|
174
|
+
// ? checkSchema(jsonOptions.inputSchema, _input, {
|
|
175
|
+
// validationType: jsonOptions.validationType ?? "parse"
|
|
176
|
+
// })
|
|
177
|
+
// : _input;
|
|
178
|
+
|
|
179
|
+
// Get raw output from input
|
|
180
|
+
const _output = jsonOptions?.mapData
|
|
181
|
+
? jsonOptions.mapData(_input)
|
|
182
|
+
: JsonResponse.defaultOnDataOutput(_input);
|
|
183
|
+
|
|
184
|
+
// Check output by schema
|
|
185
|
+
// const output = jsonOptions?.outputSchema
|
|
186
|
+
// ? checkSchema(jsonOptions.outputSchema, _output, {
|
|
187
|
+
// validationType: jsonOptions.validationType ?? "parse"
|
|
188
|
+
// })
|
|
189
|
+
// : _output;
|
|
190
|
+
|
|
191
|
+
const str = JSON.stringify(_output, null, 2);
|
|
192
|
+
const headers: Record<string, string> = {
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
...(options?.headers ?? {}),
|
|
195
|
+
...(resolveHeaders(jsonOptions?.headers, {
|
|
196
|
+
output: _output,
|
|
197
|
+
}) ?? {}),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return new JsonResponse.Base(str, {
|
|
201
|
+
headers,
|
|
202
|
+
status: options?.status ?? 200,
|
|
203
|
+
statusText: options?.statusText,
|
|
204
|
+
output: _output,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a plain text `Response`.
|
|
210
|
+
*
|
|
211
|
+
* @param text - Raw text response body.
|
|
212
|
+
* @param options - Optional response init options.
|
|
213
|
+
* @returns Text response instance.
|
|
214
|
+
*/
|
|
215
|
+
text(text: string, options?: TextResponse.Options) {
|
|
216
|
+
return new TextResponse.Base(text, {
|
|
217
|
+
...(options ?? {}),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a binary `Response`.
|
|
223
|
+
*
|
|
224
|
+
* Applies optional `binary.onData` transformation and resolves/merges headers.
|
|
225
|
+
*
|
|
226
|
+
* @param binary - Binary payload (Blob/ArrayBuffer/Uint8Array/ReadableStream).
|
|
227
|
+
* @param options - Optional response init options.
|
|
228
|
+
* @returns Binary response instance.
|
|
229
|
+
*/
|
|
230
|
+
binary(binary: Binary, options?: BinaryResponse.Options) {
|
|
231
|
+
const binaryOptions = this.options?.binary;
|
|
232
|
+
|
|
233
|
+
const data = binaryOptions?.mapData
|
|
234
|
+
? binaryOptions.mapData(binary)
|
|
235
|
+
: binary;
|
|
236
|
+
|
|
237
|
+
const headers: Record<string, string> = {
|
|
238
|
+
...(options?.headers ?? {}),
|
|
239
|
+
...(resolveHeaders(binaryOptions?.headers, data) ?? {}),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return new BinaryResponse.Base(data as any, {
|
|
243
|
+
headers,
|
|
244
|
+
status: options?.status ?? 200,
|
|
245
|
+
statusText: options?.statusText,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a new `ResponseHandler` with preassigned meta.
|
|
251
|
+
*
|
|
252
|
+
* The provided meta is validated against the configured meta schema (if present).
|
|
253
|
+
*
|
|
254
|
+
* @param _meta - Partial meta that will be merged into each next response.
|
|
255
|
+
* @returns New `ResponseHandler` instance.
|
|
256
|
+
*/
|
|
257
|
+
withMeta(
|
|
258
|
+
_meta: Partial<MetaOptions.InferedSchema<TOptions>>
|
|
259
|
+
): ResponseHandler<TMeta, TError, TJson, TBinary, TOptions, TErrors> {
|
|
260
|
+
const metaOptions = this.options.meta;
|
|
261
|
+
const meta = metaOptions?.schema
|
|
262
|
+
? checkSchema(metaOptions.schema.partial(), _meta, {
|
|
263
|
+
validationType: metaOptions.validationType ?? "parse",
|
|
264
|
+
})
|
|
265
|
+
: _meta;
|
|
266
|
+
|
|
267
|
+
return new ResponseHandler<
|
|
268
|
+
TMeta,
|
|
269
|
+
TError,
|
|
270
|
+
TJson,
|
|
271
|
+
TBinary,
|
|
272
|
+
TOptions,
|
|
273
|
+
TErrors
|
|
274
|
+
>({
|
|
275
|
+
options: this.options,
|
|
276
|
+
errors: this.errors,
|
|
277
|
+
preasignedMeta: meta as Partial<MetaOptions.InferedSchema<TOptions>>,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Define a named error handler.
|
|
283
|
+
*
|
|
284
|
+
* Returns a new `ResponseHandler` instance with the extended error registry.
|
|
285
|
+
*
|
|
286
|
+
* @typeParam TName - Error name.
|
|
287
|
+
* @typeParam THandlerOptions - Handler options type.
|
|
288
|
+
* @param name - Error name to register.
|
|
289
|
+
* @param handler - Error handler function or static error output.
|
|
290
|
+
* @param options - Error handler options (e.g. input schema).
|
|
291
|
+
* @returns New `ResponseHandler` instance with the new error registered.
|
|
292
|
+
*/
|
|
293
|
+
defineError<
|
|
294
|
+
TName extends string,
|
|
295
|
+
THandlerOptions extends ErrorHandlerOptions | undefined,
|
|
296
|
+
>(
|
|
297
|
+
name: TName,
|
|
298
|
+
handler:
|
|
299
|
+
| ErrorHandler<TOptions, THandlerOptions>
|
|
300
|
+
| ErrorOptions.InferedSchema<TOptions>,
|
|
301
|
+
options?: THandlerOptions
|
|
302
|
+
): ResponseHandler<
|
|
303
|
+
TMeta,
|
|
304
|
+
TError,
|
|
305
|
+
TJson,
|
|
306
|
+
TBinary,
|
|
307
|
+
TOptions,
|
|
308
|
+
TErrors & Record<TName, ErrorDefinition<TOptions, THandlerOptions>>
|
|
309
|
+
> {
|
|
310
|
+
const nextErrors = {
|
|
311
|
+
...(this.errors as Record<string, unknown>),
|
|
312
|
+
[name]: {
|
|
313
|
+
handler,
|
|
314
|
+
options,
|
|
315
|
+
},
|
|
316
|
+
} as TErrors & Record<TName, ErrorDefinition<TOptions, THandlerOptions>>;
|
|
317
|
+
|
|
318
|
+
const instance = new ResponseHandler({
|
|
319
|
+
options: this.options,
|
|
320
|
+
errors: nextErrors,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return instance;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
mapResponse(raw: {
|
|
327
|
+
data:
|
|
328
|
+
| JsonOptions.InferedSchemaFromBase<TJson>
|
|
329
|
+
| Binary
|
|
330
|
+
| string
|
|
331
|
+
| undefined;
|
|
332
|
+
error: ErrorOptions.InferedSchemaFromBase<TError> | undefined;
|
|
333
|
+
}): PromiseOr<Response> {
|
|
334
|
+
const mapResponse = this.options.mapResponse;
|
|
335
|
+
if (this.isBinaryData(raw.data)) {
|
|
336
|
+
return this.defaultMapResponse(raw);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const response = this.defaultMapResponse(
|
|
340
|
+
raw
|
|
341
|
+
) as BaseResponse.Base<DefaultResponse>;
|
|
342
|
+
|
|
343
|
+
return mapResponse
|
|
344
|
+
? mapResponse({
|
|
345
|
+
data: raw.data as
|
|
346
|
+
| JsonOptions.InferedSchemaFromBase<TJson>
|
|
347
|
+
| Binary
|
|
348
|
+
| string,
|
|
349
|
+
error: raw.error as ErrorOptions.InferedSchemaFromBase<TError>,
|
|
350
|
+
response,
|
|
351
|
+
})
|
|
352
|
+
: response;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Returns response that mapped into `DefaultResponse`:
|
|
357
|
+
* ```
|
|
358
|
+
* export interface DefaultResponse<TData = unknown> {
|
|
359
|
+
* success: boolean;
|
|
360
|
+
* error: ErrorResponse.Base<any, any, any> | null;
|
|
361
|
+
* data: TData | null;
|
|
362
|
+
* metadata: Record<string, unknown>;
|
|
363
|
+
* }
|
|
364
|
+
* ```
|
|
365
|
+
* @param raw
|
|
366
|
+
* @returns Response
|
|
367
|
+
*/
|
|
368
|
+
defaultMapResponse(raw: {
|
|
369
|
+
data:
|
|
370
|
+
| JsonOptions.InferedSchemaFromBase<TJson>
|
|
371
|
+
| Binary
|
|
372
|
+
| string
|
|
373
|
+
| undefined;
|
|
374
|
+
error: ErrorOptions.InferedSchemaFromBase<TError> | undefined;
|
|
375
|
+
}): BaseResponse.Base<DefaultResponse> | BaseResponse.Base<Binary> {
|
|
376
|
+
const options = this.options;
|
|
377
|
+
const meta = this.prepareMeta();
|
|
378
|
+
// DefaultError or CustomError (ErrorResponse.Base) or undefined.
|
|
379
|
+
const error = options.error?.mapError
|
|
380
|
+
? options.error?.mapError?.({
|
|
381
|
+
error: raw.error && raw.error instanceof Error ? raw.error : null,
|
|
382
|
+
meta,
|
|
383
|
+
parsedError:
|
|
384
|
+
raw.error instanceof ErrorResponse.Base ? raw.error : null,
|
|
385
|
+
})
|
|
386
|
+
: raw.error;
|
|
387
|
+
|
|
388
|
+
if (error) {
|
|
389
|
+
const status =
|
|
390
|
+
(error as ErrorResponse.Base<any, any, any>)?.status ?? 500;
|
|
391
|
+
const statusText =
|
|
392
|
+
(error as ErrorResponse.Base<any, any, any>)?.statusText ??
|
|
393
|
+
"Internal Server Error";
|
|
394
|
+
|
|
395
|
+
const payload = {
|
|
396
|
+
error,
|
|
397
|
+
data: null,
|
|
398
|
+
success: false,
|
|
399
|
+
metadata: meta,
|
|
400
|
+
} as DefaultResponse;
|
|
401
|
+
|
|
402
|
+
const headers: Record<string, string> = {
|
|
403
|
+
"Content-Type": "application/json",
|
|
404
|
+
...(resolveHeaders(options.headers, {
|
|
405
|
+
type: "error",
|
|
406
|
+
data: error,
|
|
407
|
+
}) ?? {}),
|
|
408
|
+
...(resolveHeaders(options.error?.headers, error) ?? {}),
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
return new BaseResponse.Base<DefaultResponse>(JSON.stringify(payload), {
|
|
412
|
+
status,
|
|
413
|
+
statusText,
|
|
414
|
+
headers,
|
|
415
|
+
payload,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (this.isBinaryData(raw.data)) {
|
|
420
|
+
const headers: Record<string, string> = {
|
|
421
|
+
...(resolveHeaders(options.headers, {
|
|
422
|
+
type: "binary",
|
|
423
|
+
data: raw.data,
|
|
424
|
+
}) ?? {}),
|
|
425
|
+
...(resolveHeaders(options.binary?.headers, raw.data) ?? {}),
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
return new BaseResponse.Base<Binary>(raw.data as Binary, {
|
|
429
|
+
status: 200,
|
|
430
|
+
headers,
|
|
431
|
+
payload: raw.data as Binary,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const payload = {
|
|
436
|
+
error: null,
|
|
437
|
+
data: raw.data ?? null,
|
|
438
|
+
success: true,
|
|
439
|
+
metadata: meta,
|
|
440
|
+
} as DefaultResponse;
|
|
441
|
+
|
|
442
|
+
const headers: Record<string, string> = {
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
...(resolveHeaders(options.headers, {
|
|
445
|
+
type: "json",
|
|
446
|
+
data: raw.data,
|
|
447
|
+
}) ?? {}),
|
|
448
|
+
...(resolveHeaders(options.json?.headers, {
|
|
449
|
+
output: raw.data,
|
|
450
|
+
}) ?? {}),
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
return new BaseResponse.Base<DefaultResponse>(JSON.stringify(payload), {
|
|
454
|
+
status: 200,
|
|
455
|
+
headers,
|
|
456
|
+
payload,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Prepare `meta` by merging preassigned meta (from `withMeta`) with default meta (if any).
|
|
462
|
+
*
|
|
463
|
+
* @returns Resolved meta object.
|
|
464
|
+
*/
|
|
465
|
+
private prepareMeta(): MetaOptions.InferedSchema<TOptions> {
|
|
466
|
+
const objOrFn = this.options.meta?.default;
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
...(typeof objOrFn === "function" ? objOrFn() : objOrFn),
|
|
470
|
+
...(this.preasignedMeta ?? {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private isBinaryData(data: unknown): data is Binary {
|
|
475
|
+
return (
|
|
476
|
+
data instanceof Blob ||
|
|
477
|
+
data instanceof ArrayBuffer ||
|
|
478
|
+
data instanceof Uint8Array ||
|
|
479
|
+
(typeof ReadableStream !== "undefined" && data instanceof ReadableStream)
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
package/src/headers.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FunctionObject } from "@/types";
|
|
2
|
+
|
|
3
|
+
export type RawHeaders = Record<string, string>;
|
|
4
|
+
export type Headers<T> = FunctionObject<RawHeaders, T>;
|
|
5
|
+
|
|
6
|
+
export function resolveHeaders(
|
|
7
|
+
headers: Headers<any> | undefined,
|
|
8
|
+
input: unknown
|
|
9
|
+
): RawHeaders | null {
|
|
10
|
+
const result =
|
|
11
|
+
typeof headers === "function"
|
|
12
|
+
? (headers as (arg: unknown) => RawHeaders)(input)
|
|
13
|
+
: headers;
|
|
14
|
+
|
|
15
|
+
if (!headers) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
return result;
|
|
21
|
+
}
|