@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/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MetaOptions } from "@/options/meta";
|
|
2
|
+
import type { ErrorOptions } from "@/options/error";
|
|
3
|
+
import type { JsonOptions } from "@/options/json";
|
|
4
|
+
import type { BinaryOptions } from "@/options/binary";
|
|
5
|
+
import type { Headers } from "@/headers";
|
|
6
|
+
import type { BaseResponse, ResponseTypes } from "@/response/base";
|
|
7
|
+
import type { Binary } from "@/response/binary";
|
|
8
|
+
import type { PromiseOr } from "@/types";
|
|
9
|
+
import type { DefaultResponse } from "@/response/default";
|
|
10
|
+
|
|
11
|
+
export interface Options<
|
|
12
|
+
TMeta extends MetaOptions.Base = MetaOptions.Base,
|
|
13
|
+
TError extends ErrorOptions.Base = ErrorOptions.Base,
|
|
14
|
+
TJson extends JsonOptions.Base = JsonOptions.Base,
|
|
15
|
+
TBinary extends BinaryOptions.Base = BinaryOptions.Base
|
|
16
|
+
> {
|
|
17
|
+
headers?: Headers<{
|
|
18
|
+
type: ResponseTypes;
|
|
19
|
+
data: any;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
meta?: TMeta;
|
|
23
|
+
error?: TError;
|
|
24
|
+
json?: TJson;
|
|
25
|
+
|
|
26
|
+
binary?: TBinary;
|
|
27
|
+
|
|
28
|
+
mapResponse?(data: {
|
|
29
|
+
data: JsonOptions.InferedSchemaFromBase<TJson> | Binary | string;
|
|
30
|
+
error: ErrorOptions.InferedSchemaFromBase<TError>;
|
|
31
|
+
// headers: RawHeaders;
|
|
32
|
+
// status: number | undefined;
|
|
33
|
+
// statusText: string | undefined;
|
|
34
|
+
response: BaseResponse.Base<DefaultResponse>;
|
|
35
|
+
}): PromiseOr<Response>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Headers } from "@/headers";
|
|
2
|
+
import type { Binary } from "@/response/binary";
|
|
3
|
+
|
|
4
|
+
export namespace BinaryOptions {
|
|
5
|
+
export interface Base {
|
|
6
|
+
headers?: Headers<Binary>;
|
|
7
|
+
|
|
8
|
+
mapData?: (data: Binary) => Binary;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function binary<T extends BinaryOptions.Base>(opts: T): T {
|
|
13
|
+
return opts;
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtractSchema,
|
|
3
|
+
Infer,
|
|
4
|
+
InferOr,
|
|
5
|
+
Schema,
|
|
6
|
+
ValidationType,
|
|
7
|
+
} from "@apisr/schema";
|
|
8
|
+
import type { DefaultError } from "@/error";
|
|
9
|
+
import type { Headers } from "@/headers";
|
|
10
|
+
import type { Options } from "@/options/base";
|
|
11
|
+
import type { PromiseOr } from "@/types";
|
|
12
|
+
|
|
13
|
+
export namespace ErrorOptions {
|
|
14
|
+
export interface Base<TSchema extends Schema = Schema> {
|
|
15
|
+
headers?: Headers<Infer<TSchema>>;
|
|
16
|
+
|
|
17
|
+
mapDefaultError?: (
|
|
18
|
+
error: DefaultError
|
|
19
|
+
) => TSchema extends undefined ? DefaultError : Infer<TSchema>;
|
|
20
|
+
|
|
21
|
+
mapError?: (ctx: {
|
|
22
|
+
error: Error | null;
|
|
23
|
+
meta: unknown;
|
|
24
|
+
parsedError?: TSchema extends undefined ? DefaultError : Infer<TSchema>;
|
|
25
|
+
}) => PromiseOr<TSchema extends undefined ? DefaultError : Infer<TSchema>>;
|
|
26
|
+
|
|
27
|
+
onFailedSchemaValidation?: (ctx: {
|
|
28
|
+
data: unknown;
|
|
29
|
+
}) => PromiseOr<TSchema extends undefined ? DefaultError : Infer<TSchema>>;
|
|
30
|
+
schema?: TSchema;
|
|
31
|
+
validationType?: ValidationType;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type InferedSchemaFromBase<TError extends Base> = InferOr<
|
|
35
|
+
ExtractSchema<TError>,
|
|
36
|
+
DefaultError
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export type InferedSchema<TOptions extends Options> =
|
|
40
|
+
TOptions["error"] extends undefined
|
|
41
|
+
? DefaultError
|
|
42
|
+
: InferOr<ExtractSchema<TOptions["error"]>, DefaultError>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function error<TSchema extends Schema>(
|
|
46
|
+
opts: ErrorOptions.Base<TSchema> & {
|
|
47
|
+
schema?: TSchema;
|
|
48
|
+
}
|
|
49
|
+
): ErrorOptions.Base<TSchema> & {
|
|
50
|
+
schema?: TSchema;
|
|
51
|
+
} {
|
|
52
|
+
return opts;
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./base";
|
|
2
|
+
export * from "./json"
|
|
3
|
+
export * from "./meta"
|
|
4
|
+
export * from "./error"
|
|
5
|
+
export * from "./binary"
|
|
6
|
+
|
|
7
|
+
import { json } from "./json"
|
|
8
|
+
import { meta } from "./meta"
|
|
9
|
+
import { error } from "./error"
|
|
10
|
+
import { binary } from "./binary"
|
|
11
|
+
|
|
12
|
+
export const options = {
|
|
13
|
+
json,
|
|
14
|
+
meta,
|
|
15
|
+
error,
|
|
16
|
+
binary
|
|
17
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtractSchema,
|
|
3
|
+
Infer,
|
|
4
|
+
InferOr,
|
|
5
|
+
Schema,
|
|
6
|
+
ValidationType,
|
|
7
|
+
} from "@apisr/schema";
|
|
8
|
+
import type { Headers } from "@/headers";
|
|
9
|
+
import type { JsonResponse } from "@/response/json";
|
|
10
|
+
import type { PromiseOr } from "@/types";
|
|
11
|
+
import type { Options } from "./base";
|
|
12
|
+
|
|
13
|
+
export namespace JsonOptions {
|
|
14
|
+
export interface Base<TSchema extends Schema = Schema> {
|
|
15
|
+
headers?: Headers<Infer<TSchema>>;
|
|
16
|
+
|
|
17
|
+
mapData?: (data: Infer<TSchema>) => PromiseOr<Infer<TSchema>>;
|
|
18
|
+
|
|
19
|
+
schema?: TSchema;
|
|
20
|
+
validationType?: ValidationType;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type InferedSchemaFromBase<TJson extends Base> = InferOr<
|
|
24
|
+
ExtractSchema<TJson>,
|
|
25
|
+
JsonResponse.DefaultSchema
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
// export type InferedInputSchema<TOptions extends Options> = (TOptions["json"] extends undefined
|
|
29
|
+
// ? JsonResponse.DefaultInputSchema
|
|
30
|
+
// : InferOr<Exclude<ExtractSchemaFromKey<TOptions["json"], "inputSchema">, undefined>, JsonResponse.DefaultInputSchema>);
|
|
31
|
+
|
|
32
|
+
export type InferedSchema<TOptions extends Options> =
|
|
33
|
+
TOptions["json"] extends undefined
|
|
34
|
+
? JsonResponse.DefaultSchema
|
|
35
|
+
: InferOr<
|
|
36
|
+
Exclude<ExtractSchema<TOptions["json"]>, undefined>,
|
|
37
|
+
JsonResponse.DefaultSchema
|
|
38
|
+
>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function json<TSchema extends Schema>(
|
|
42
|
+
opts: JsonOptions.Base<TSchema>
|
|
43
|
+
): JsonOptions.Base<TSchema> {
|
|
44
|
+
return opts;
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtractSchema,
|
|
3
|
+
Infer,
|
|
4
|
+
InferOr,
|
|
5
|
+
Schema,
|
|
6
|
+
ValidationType,
|
|
7
|
+
} from "@apisr/schema";
|
|
8
|
+
import type { DefaultMeta } from "@/response/meta";
|
|
9
|
+
import type { FunctionObject } from "@/types";
|
|
10
|
+
import type { Options } from "./base";
|
|
11
|
+
|
|
12
|
+
export namespace MetaOptions {
|
|
13
|
+
export interface Base<TSchema extends Schema = Schema> {
|
|
14
|
+
default?: FunctionObject<Infer<TSchema>, never>;
|
|
15
|
+
schema?: TSchema;
|
|
16
|
+
validationType?: ValidationType;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type InferedSchema<TOptions extends Options> =
|
|
20
|
+
TOptions["meta"] extends undefined
|
|
21
|
+
? DefaultMeta
|
|
22
|
+
: InferOr<ExtractSchema<TOptions["meta"]>, DefaultMeta>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function meta<TSchema extends Schema>(
|
|
26
|
+
opts: MetaOptions.Base<TSchema> & {
|
|
27
|
+
schema?: TSchema;
|
|
28
|
+
}
|
|
29
|
+
): MetaOptions.Base<TSchema> & {
|
|
30
|
+
schema?: TSchema;
|
|
31
|
+
} {
|
|
32
|
+
return opts;
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RawHeaders } from "@/headers";
|
|
2
|
+
|
|
3
|
+
export type ResponseTypes = "json" | "binary" | "text" | "error";
|
|
4
|
+
|
|
5
|
+
export namespace BaseResponse {
|
|
6
|
+
export interface Options {
|
|
7
|
+
status?: number;
|
|
8
|
+
statusText?: string;
|
|
9
|
+
headers?: RawHeaders;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Base<TPayload> extends Response {
|
|
13
|
+
public payload: TPayload | undefined;
|
|
14
|
+
|
|
15
|
+
constructor(body?: ConstructorParameters<typeof Response>[0], init?: (ResponseInit & { payload: TPayload }) | undefined) {
|
|
16
|
+
const { payload, ..._init } = init ?? {};
|
|
17
|
+
|
|
18
|
+
super(body, _init);
|
|
19
|
+
|
|
20
|
+
this.payload = payload;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BaseResponse } from "../base";
|
|
2
|
+
|
|
3
|
+
export type Binary = Blob | ArrayBuffer | Uint8Array | ReadableStream;
|
|
4
|
+
|
|
5
|
+
export namespace BinaryResponse {
|
|
6
|
+
export class Base extends Response {
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Options extends BaseResponse.Options {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BaseResponse } from "../base";
|
|
2
|
+
|
|
3
|
+
export namespace ErrorResponse {
|
|
4
|
+
export class Base<
|
|
5
|
+
TName extends string,
|
|
6
|
+
TMeta extends Record<string, any>,
|
|
7
|
+
TOutput extends Record<string, any>
|
|
8
|
+
> extends Error {
|
|
9
|
+
public override name: TName;
|
|
10
|
+
public meta: TMeta;
|
|
11
|
+
public output: TOutput;
|
|
12
|
+
public status: number;
|
|
13
|
+
public statusText: string;
|
|
14
|
+
|
|
15
|
+
constructor({ meta, name, output, status, statusText }: {
|
|
16
|
+
// Name of error in response handler
|
|
17
|
+
name: TName;
|
|
18
|
+
|
|
19
|
+
// Meta of response
|
|
20
|
+
meta: TMeta;
|
|
21
|
+
|
|
22
|
+
// Output of handler
|
|
23
|
+
output: TOutput;
|
|
24
|
+
|
|
25
|
+
status: number;
|
|
26
|
+
statusText: string;
|
|
27
|
+
}) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.statusText = statusText;
|
|
32
|
+
|
|
33
|
+
this.name = name;
|
|
34
|
+
this.meta = meta;
|
|
35
|
+
this.output = output;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export interface Options extends BaseResponse.Options {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BaseResponse } from "../base";
|
|
2
|
+
|
|
3
|
+
export namespace JsonResponse {
|
|
4
|
+
export class Base<TOutput> extends Response {
|
|
5
|
+
private _output: TOutput | undefined;
|
|
6
|
+
|
|
7
|
+
constructor(body: ConstructorParameters<typeof Response>[0], _init?: ConstructorParameters<typeof Response>[1] & {
|
|
8
|
+
output: TOutput;
|
|
9
|
+
}) {
|
|
10
|
+
const { output, ...init } = _init ?? {};
|
|
11
|
+
|
|
12
|
+
super(body, init);
|
|
13
|
+
|
|
14
|
+
this._output = output;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get output(): TOutput | undefined {
|
|
18
|
+
return this._output;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override json: () => Promise<TOutput> = () => {
|
|
22
|
+
return Response.prototype.json.call(this) as Promise<TOutput>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// export type Base = Record<string, any> & {
|
|
26
|
+
// [responseSymbol]: () => Response;
|
|
27
|
+
// }
|
|
28
|
+
|
|
29
|
+
export interface Options extends BaseResponse.Options {
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type DefaultInputSchema = Record<string, any>;
|
|
33
|
+
export type DefaultSchema = {
|
|
34
|
+
data: Record<string, any>;
|
|
35
|
+
success: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function defaultOnDataOutput(input: DefaultInputSchema): DefaultSchema {
|
|
39
|
+
return {
|
|
40
|
+
data: input,
|
|
41
|
+
success: true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/symbol.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const responseSymbol = Symbol("_response");
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type AnyObject = Record<string, any>;
|
|
2
|
+
export type FunctionObject<TResult extends any, TFnArg = never> = TResult | (
|
|
3
|
+
(TFnArg extends never
|
|
4
|
+
? (() => TResult)
|
|
5
|
+
: ((arg: TFnArg) => TResult))
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export type PromiseOr<T> = PromiseLike<T> | T;
|
|
9
|
+
// export type IfUndefined<> =
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "@apisr/zod";
|
|
3
|
+
import { createResponseHandler } from "@/index";
|
|
4
|
+
|
|
5
|
+
type TestErrorOutput = {
|
|
6
|
+
name: string;
|
|
7
|
+
details: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("@apisr/response ResponseHandler", () => {
|
|
11
|
+
test("json() validates output schema, sets content-type and merges headers", () => {
|
|
12
|
+
const handler = createResponseHandler((options) => ({
|
|
13
|
+
json: options.json({
|
|
14
|
+
schema: z.object({
|
|
15
|
+
msg: z.string(),
|
|
16
|
+
}),
|
|
17
|
+
mapData: (input) => ({
|
|
18
|
+
msg: input?.msg,
|
|
19
|
+
}),
|
|
20
|
+
headers: (data) => ({
|
|
21
|
+
"x-data": data?.msg,
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const res = handler.json({ msg: "hello" }, { headers: { "x-opt": "1" } });
|
|
27
|
+
|
|
28
|
+
expect(res).toBeInstanceOf(Response);
|
|
29
|
+
expect(res.status).toBe(200);
|
|
30
|
+
expect(res.headers.get("content-type")).toBe("application/json");
|
|
31
|
+
expect(res.headers.get("x-opt")).toBe("1");
|
|
32
|
+
expect(res.headers.get("x-data")).toBe("hello");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("text() returns Response with provided body and init options", async () => {
|
|
36
|
+
const handler = createResponseHandler({});
|
|
37
|
+
const res = handler.text("ok", { status: 201, headers: { "x-t": "1" } });
|
|
38
|
+
|
|
39
|
+
expect(res).toBeInstanceOf(Response);
|
|
40
|
+
expect(res.status).toBe(201);
|
|
41
|
+
expect(res.headers.get("x-t")).toBe("1");
|
|
42
|
+
expect(await res.text()).toBe("ok");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("binary() applies onData, merges headers and returns Response", async () => {
|
|
46
|
+
const handler = createResponseHandler({
|
|
47
|
+
binary: {
|
|
48
|
+
mapData: (data) => data,
|
|
49
|
+
headers: () => ({
|
|
50
|
+
"content-type": "application/octet-stream",
|
|
51
|
+
"x-bin": "1",
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const body = new Uint8Array([1, 2, 3]);
|
|
57
|
+
const res = handler.binary(body, {
|
|
58
|
+
status: 202,
|
|
59
|
+
headers: { "x-opt": "1" },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(res).toBeInstanceOf(Response);
|
|
63
|
+
expect(res.status).toBe(202);
|
|
64
|
+
expect(res.headers.get("x-opt")).toBe("1");
|
|
65
|
+
expect(res.headers.get("x-bin")).toBe("1");
|
|
66
|
+
expect(res.headers.get("content-type")).toBe("application/octet-stream");
|
|
67
|
+
|
|
68
|
+
const ab = await res.arrayBuffer();
|
|
69
|
+
expect(new Uint8Array(ab)).toEqual(body);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("withMeta() validates meta and fail() includes prepared meta", () => {
|
|
73
|
+
const handler = createResponseHandler({
|
|
74
|
+
meta: {
|
|
75
|
+
schema: z.object({
|
|
76
|
+
raw: z.boolean(),
|
|
77
|
+
version: z.number().optional(),
|
|
78
|
+
}),
|
|
79
|
+
default: { raw: false },
|
|
80
|
+
},
|
|
81
|
+
error: {
|
|
82
|
+
schema: z.object({
|
|
83
|
+
name: z.string(),
|
|
84
|
+
details: z.string(),
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
.defineError("E_META", ({ meta }) => ({
|
|
89
|
+
name: "E_META",
|
|
90
|
+
details: String(meta.raw),
|
|
91
|
+
}))
|
|
92
|
+
.withMeta({ raw: true });
|
|
93
|
+
|
|
94
|
+
const err = handler.fail("E_META");
|
|
95
|
+
const output = err.output as TestErrorOutput;
|
|
96
|
+
|
|
97
|
+
expect(err.name).toBe("E_META");
|
|
98
|
+
expect(err.meta.raw).toBe(true);
|
|
99
|
+
expect(output.name).toBe("E_META");
|
|
100
|
+
expect(output.details).toBe("true");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("defineError() supports static handler and fail() returns its output", () => {
|
|
104
|
+
const handler = createResponseHandler({
|
|
105
|
+
error: {
|
|
106
|
+
schema: z.object({
|
|
107
|
+
name: z.string(),
|
|
108
|
+
details: z.string(),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
}).defineError("STATIC", { name: "STATIC", details: "d" });
|
|
112
|
+
|
|
113
|
+
const err = handler.fail("STATIC");
|
|
114
|
+
const output = err.output as TestErrorOutput;
|
|
115
|
+
expect(err.name).toBe("STATIC");
|
|
116
|
+
expect(output).toEqual({ name: "STATIC", details: "d" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("fail() validates error input schema", () => {
|
|
120
|
+
const handler = createResponseHandler({
|
|
121
|
+
error: {
|
|
122
|
+
schema: z.object({
|
|
123
|
+
name: z.string(),
|
|
124
|
+
details: z.string(),
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
}).defineError(
|
|
128
|
+
"E_INPUT",
|
|
129
|
+
({ input }) => ({
|
|
130
|
+
name: "E_INPUT",
|
|
131
|
+
details: String(input?.raw2),
|
|
132
|
+
}),
|
|
133
|
+
{
|
|
134
|
+
input: z.object({
|
|
135
|
+
raw2: z.boolean(),
|
|
136
|
+
}),
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const err = handler.fail("E_INPUT", { raw2: true });
|
|
141
|
+
const output = err.output as unknown as TestErrorOutput;
|
|
142
|
+
expect(output.details).toBe("true");
|
|
143
|
+
|
|
144
|
+
expect(() => handler.fail("E_INPUT", { raw2: "nope" } as any)).toThrow();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("defineError() supports dynamic handler and fail() returns its output", () => {
|
|
148
|
+
const handler = createResponseHandler({
|
|
149
|
+
error: {
|
|
150
|
+
schema: z.object({
|
|
151
|
+
name: z.string(),
|
|
152
|
+
details: z.string(),
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
}).defineError(
|
|
156
|
+
"DYNAMIC",
|
|
157
|
+
({ input, meta }) => {
|
|
158
|
+
return {
|
|
159
|
+
name: "DYNAMIC",
|
|
160
|
+
details: String(input.raw2),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
input: z.object({
|
|
165
|
+
raw2: z.boolean(),
|
|
166
|
+
}),
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const err = handler.fail("DYNAMIC", { raw2: true });
|
|
171
|
+
const output = err.output as unknown as TestErrorOutput;
|
|
172
|
+
expect(err.name).toBe("DYNAMIC");
|
|
173
|
+
expect(output.details).toBe("true");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("withMeta() affects meta for subsequent fail() calls", () => {
|
|
177
|
+
const handler = createResponseHandler({
|
|
178
|
+
meta: {
|
|
179
|
+
schema: z.object({
|
|
180
|
+
raw: z.boolean(),
|
|
181
|
+
}),
|
|
182
|
+
default: { raw: false },
|
|
183
|
+
},
|
|
184
|
+
error: {
|
|
185
|
+
schema: z.object({
|
|
186
|
+
name: z.string(),
|
|
187
|
+
details: z.string(),
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
.defineError("META", ({ meta }) => {
|
|
192
|
+
return {
|
|
193
|
+
name: "META",
|
|
194
|
+
details: String(meta.raw),
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
.withMeta({ raw: true });
|
|
198
|
+
|
|
199
|
+
const err = handler.fail("META");
|
|
200
|
+
const output = err.output as TestErrorOutput;
|
|
201
|
+
expect(err.name).toBe("META");
|
|
202
|
+
expect(output.details).toBe("true");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const jsonSymbol = Symbol("_json");
|
|
4
|
+
|
|
5
|
+
test("json symbol stringify", () => {
|
|
6
|
+
const obj = {
|
|
7
|
+
id: 123,
|
|
8
|
+
[jsonSymbol]: () => new Response()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const str = JSON.stringify(obj, null, 2);
|
|
12
|
+
|
|
13
|
+
console.log(str);
|
|
14
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
"baseUrl": ".",
|
|
12
|
+
"paths": {
|
|
13
|
+
"@/*": ["./src/*"]
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// Bundler mode
|
|
17
|
+
"moduleResolution": "bundler",
|
|
18
|
+
"allowImportingTsExtensions": true,
|
|
19
|
+
"verbatimModuleSyntax": true,
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
|
|
22
|
+
// Best practices
|
|
23
|
+
"strict": true,
|
|
24
|
+
"skipLibCheck": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": true,
|
|
26
|
+
"noUncheckedIndexedAccess": true,
|
|
27
|
+
"noImplicitOverride": true,
|
|
28
|
+
|
|
29
|
+
// Some stricter flags (disabled by default)
|
|
30
|
+
"noUnusedLocals": false,
|
|
31
|
+
"noUnusedParameters": false,
|
|
32
|
+
"noPropertyAccessFromIndexSignature": false
|
|
33
|
+
}
|
|
34
|
+
}
|
package/tsdown.config.ts
ADDED