@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/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./handler";
2
+ export * from "./headers";
3
+ export * from "./symbol";
4
+ export * from "./types";
5
+ export * from "./response";
6
+ export * from "./options";
7
+ export * from "./error";
@@ -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,8 @@
1
+ import type { ErrorResponse } from "./error";
2
+
3
+ export interface DefaultResponse<TData = unknown> {
4
+ success: boolean;
5
+ error: ErrorResponse.Base<any, any, any> | null;
6
+ data: TData | null;
7
+ metadata: Record<string, unknown>;
8
+ }
@@ -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,7 @@
1
+ export * from "./base";
2
+ export * from "./default";
3
+ export * from "./error";
4
+ export * from "./json";
5
+ export * from "./binary";
6
+ export * from "./meta";
7
+ export * from "./text";
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export type DefaultMeta = {
2
+ requestId: string;
3
+ timestamp: number;
4
+ }
@@ -0,0 +1,9 @@
1
+ import type { BaseResponse } from "../base";
2
+
3
+ export namespace TextResponse {
4
+ export class Base extends Response {
5
+ }
6
+
7
+ export interface Options extends BaseResponse.Options {
8
+ }
9
+ }
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
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: "./src/index.ts",
5
+ dts: true,
6
+ format: "esm",
7
+ outDir: "./dist",
8
+ clean: true,
9
+ unbundle: true,
10
+ noExternal: [/@apisr\/.*/],
11
+ external: ["zod"],
12
+ });