@mhbdev/next-safe-route 0.0.31
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 +21 -0
- package/README.md +101 -0
- package/dist/index.d.mts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Richard Solomou
|
|
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
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<h1 align="center">@mhbdev/next-safe-route</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/@mhbdev/next-safe-route"><img src="https://img.shields.io/npm/v/%40mhbdev%2Fnext-safe-route?style=for-the-badge&logo=npm" /></a>
|
|
5
|
+
<a href="https://github.com/richardsolomou/next-safe-route/actions/workflows/test.yaml"><img src="https://img.shields.io/github/actions/workflow/status/richardsolomou/next-safe-route/test.yaml?style=for-the-badge&logo=vitest" /></a>
|
|
6
|
+
<a href="https://github.com/richardsolomou/next-safe-route/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/%40mhbdev%2Fnext-safe-route?style=for-the-badge" /></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
`next-safe-route` is a utility library for Next.js that provides type-safety and schema validation for [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)/API Routes. It is compatible with Next.js 15+ (including 16) route handler signatures.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **✅ Schema Validation:** Automatically validate request parameters, query strings, and body content with built-in JSON error responses.
|
|
14
|
+
- **🧷 Type-Safe:** Work with full TypeScript type safety for parameters, query strings, and body content, including transformation results.
|
|
15
|
+
- **🔗 Adapter-Friendly:** Ships with a zod (v4+) adapter by default and exports adapters for valibot and yup.
|
|
16
|
+
- **📦 Next-Ready:** Matches the Next.js Route Handler signature (including Next 15/16) and supports middleware-style context extensions.
|
|
17
|
+
- **🧪 Fully Tested:** Extensive test suite to ensure everything works reliably.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @mhbdev/next-safe-route zod
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The library uses [zod](https://zod.dev) v4+ by default. If you want to use another validation library supported by [TypeSchema](https://typeschema.com), install it alongside this package and pass its adapter to `createSafeRoute`.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// app/api/hello/route.ts
|
|
31
|
+
import { createSafeRoute } from '@mhbdev/next-safe-route';
|
|
32
|
+
import { z } from 'zod';
|
|
33
|
+
|
|
34
|
+
const paramsSchema = z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const querySchema = z.object({
|
|
39
|
+
search: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const bodySchema = z.object({
|
|
43
|
+
field: z.string(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const GET = createSafeRoute()
|
|
47
|
+
.params(paramsSchema)
|
|
48
|
+
.query(querySchema)
|
|
49
|
+
.body(bodySchema)
|
|
50
|
+
.handler((request, context) => {
|
|
51
|
+
const { id } = context.params;
|
|
52
|
+
const { search } = context.query;
|
|
53
|
+
const { field } = context.body;
|
|
54
|
+
|
|
55
|
+
return Response.json({ id, search, field }, { status: 200 });
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To define a route handler in Next.js:
|
|
60
|
+
|
|
61
|
+
1. Import `createSafeRoute` and your validation library (e.g., `zod`).
|
|
62
|
+
2. Define validation schemas for params, query, and body as needed.
|
|
63
|
+
3. Use `createSafeRoute()` to create a route handler, chaining `params`, `query`, and `body` methods.
|
|
64
|
+
4. Implement your handler function, accessing validated and type-safe params, query, and body through `context`. Body validation expects `Content-Type: application/json`.
|
|
65
|
+
|
|
66
|
+
### Using other validation libraries
|
|
67
|
+
|
|
68
|
+
The package exports adapters so you can bring your own schema library:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createSafeRoute, valibotAdapter } from '@mhbdev/next-safe-route';
|
|
72
|
+
import { object, string } from 'valibot';
|
|
73
|
+
|
|
74
|
+
const querySchema = object({
|
|
75
|
+
search: string(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const GET = createSafeRoute({
|
|
79
|
+
validationAdapter: valibotAdapter(),
|
|
80
|
+
})
|
|
81
|
+
.query(querySchema)
|
|
82
|
+
.handler((request, context) => {
|
|
83
|
+
return Response.json({ search: context.query.search });
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
Tests are written using [Vitest](https://vitest.dev). To run the tests, use the following command:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pnpm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Contributing
|
|
96
|
+
|
|
97
|
+
Contributions are welcome! For major changes, please open an issue first to discuss what you would like to change.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
+
import { GenericSchema, GenericSchemaAsync, InferOutput } from 'valibot';
|
|
3
|
+
import { Schema as Schema$1, InferType } from 'yup';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
type IfInstalled<T> = any extends T ? never : T;
|
|
7
|
+
type Schema = IfInstalled<z.ZodTypeAny> | IfInstalled<GenericSchema> | IfInstalled<GenericSchemaAsync> | IfInstalled<Schema$1> | IfInstalled<TSchema>;
|
|
8
|
+
type Infer<S extends Schema> = S extends IfInstalled<z.ZodTypeAny> ? z.infer<S> : S extends IfInstalled<GenericSchema> ? InferOutput<S> : S extends IfInstalled<GenericSchemaAsync> ? InferOutput<S> : S extends IfInstalled<Schema$1> ? InferType<S> : S extends IfInstalled<TSchema> ? Static<S> : never;
|
|
9
|
+
type ValidationIssue = {
|
|
10
|
+
message: string;
|
|
11
|
+
path?: Array<string | number | symbol>;
|
|
12
|
+
};
|
|
13
|
+
interface ValidationAdapter {
|
|
14
|
+
validate<S extends Schema>(schema: S, data: unknown): Promise<{
|
|
15
|
+
success: true;
|
|
16
|
+
data: Infer<S>;
|
|
17
|
+
} | {
|
|
18
|
+
success: false;
|
|
19
|
+
issues: ValidationIssue[];
|
|
20
|
+
}>;
|
|
21
|
+
validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown): Promise<{
|
|
22
|
+
success: true;
|
|
23
|
+
data: Infer<S>;
|
|
24
|
+
} | {
|
|
25
|
+
success: false;
|
|
26
|
+
issues: ValidationIssue[];
|
|
27
|
+
}>;
|
|
28
|
+
validate<S extends IfInstalled<GenericSchema>>(schema: S, data: unknown): Promise<{
|
|
29
|
+
success: true;
|
|
30
|
+
data: Infer<S>;
|
|
31
|
+
} | {
|
|
32
|
+
success: false;
|
|
33
|
+
issues: ValidationIssue[];
|
|
34
|
+
}>;
|
|
35
|
+
validate<S extends IfInstalled<GenericSchemaAsync>>(schema: S, data: unknown): Promise<{
|
|
36
|
+
success: true;
|
|
37
|
+
data: Infer<S>;
|
|
38
|
+
} | {
|
|
39
|
+
success: false;
|
|
40
|
+
issues: ValidationIssue[];
|
|
41
|
+
}>;
|
|
42
|
+
validate<S extends IfInstalled<Schema$1>>(schema: S, data: unknown): Promise<{
|
|
43
|
+
success: true;
|
|
44
|
+
data: Infer<S>;
|
|
45
|
+
} | {
|
|
46
|
+
success: false;
|
|
47
|
+
issues: ValidationIssue[];
|
|
48
|
+
}>;
|
|
49
|
+
validate<S extends IfInstalled<TSchema>>(schema: S, data: unknown): Promise<{
|
|
50
|
+
success: true;
|
|
51
|
+
data: Infer<S>;
|
|
52
|
+
} | {
|
|
53
|
+
success: false;
|
|
54
|
+
issues: ValidationIssue[];
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type InferMaybe<TSchema extends Schema | undefined> = TSchema extends Schema ? Infer<TSchema> : Record<string, unknown>;
|
|
59
|
+
type RouteContext<TRawParams extends Record<string, unknown> = Record<string, string | string[]>> = {
|
|
60
|
+
params: TRawParams;
|
|
61
|
+
};
|
|
62
|
+
type HandlerFunction<TParams, TQuery, TBody, TContext> = (request: Request, context: {
|
|
63
|
+
params: TParams;
|
|
64
|
+
query: TQuery;
|
|
65
|
+
body: TBody;
|
|
66
|
+
data: TContext;
|
|
67
|
+
}) => Response | Promise<Response>;
|
|
68
|
+
type OriginalRouteHandler = (request: Request, context: RouteContext) => Response | Promise<Response>;
|
|
69
|
+
type HandlerServerErrorFn = (error: Error) => Response;
|
|
70
|
+
|
|
71
|
+
type Middleware<T = Record<string, unknown>> = (request: Request) => Promise<T>;
|
|
72
|
+
type BuilderConfig<TParams extends Schema | undefined, TQuery extends Schema | undefined, TBody extends Schema | undefined> = {
|
|
73
|
+
paramsSchema: TParams;
|
|
74
|
+
querySchema: TQuery;
|
|
75
|
+
bodySchema: TBody;
|
|
76
|
+
};
|
|
77
|
+
declare class RouteHandlerBuilder<TParams extends Schema | undefined = undefined, TQuery extends Schema | undefined = undefined, TBody extends Schema | undefined = undefined, TContext extends Record<string, unknown> = Record<string, unknown>> {
|
|
78
|
+
private config;
|
|
79
|
+
private middlewares;
|
|
80
|
+
private handleServerError?;
|
|
81
|
+
private validationAdapter;
|
|
82
|
+
private baseContext;
|
|
83
|
+
constructor({ config, validationAdapter, middlewares, handleServerError, baseContext, }: {
|
|
84
|
+
config?: BuilderConfig<TParams, TQuery, TBody>;
|
|
85
|
+
middlewares?: Middleware[];
|
|
86
|
+
handleServerError?: HandlerServerErrorFn;
|
|
87
|
+
validationAdapter?: ValidationAdapter;
|
|
88
|
+
baseContext?: TContext;
|
|
89
|
+
});
|
|
90
|
+
params<T extends Schema>(schema: T): RouteHandlerBuilder<T, TQuery, TBody, TContext>;
|
|
91
|
+
query<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, T, TBody, TContext>;
|
|
92
|
+
body<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, TQuery, T, TContext>;
|
|
93
|
+
use<TReturnType extends Record<string, unknown>>(middleware: Middleware<TReturnType>): RouteHandlerBuilder<TParams, TQuery, TBody, TContext & TReturnType>;
|
|
94
|
+
handler(handler: HandlerFunction<InferMaybe<TParams>, InferMaybe<TQuery>, InferMaybe<TBody>, TContext>): OriginalRouteHandler;
|
|
95
|
+
private validateInput;
|
|
96
|
+
private getQueryParams;
|
|
97
|
+
private parseRequestBody;
|
|
98
|
+
private buildErrorResponse;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type CreateSafeRouteParams<TContext extends Record<string, unknown>> = {
|
|
102
|
+
handleServerError?: HandlerServerErrorFn;
|
|
103
|
+
validationAdapter?: ValidationAdapter;
|
|
104
|
+
baseContext?: TContext;
|
|
105
|
+
};
|
|
106
|
+
declare function createSafeRoute<TContext extends Record<string, unknown> = Record<string, unknown>>(params?: CreateSafeRouteParams<TContext>): RouteHandlerBuilder<undefined, undefined, undefined, TContext>;
|
|
107
|
+
|
|
108
|
+
declare class ZodAdapter implements ValidationAdapter {
|
|
109
|
+
validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown): Promise<{
|
|
110
|
+
readonly success: true;
|
|
111
|
+
readonly data: Infer<S>;
|
|
112
|
+
readonly issues?: undefined;
|
|
113
|
+
} | {
|
|
114
|
+
readonly success: false;
|
|
115
|
+
readonly issues: {
|
|
116
|
+
message: string;
|
|
117
|
+
path: PropertyKey[];
|
|
118
|
+
}[];
|
|
119
|
+
readonly data?: undefined;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
declare function zodAdapter(): ZodAdapter;
|
|
123
|
+
|
|
124
|
+
declare class ValibotAdapter implements ValidationAdapter {
|
|
125
|
+
validate<S extends IfInstalled<GenericSchema | GenericSchemaAsync>>(schema: S, data: unknown): Promise<{
|
|
126
|
+
readonly success: true;
|
|
127
|
+
readonly data: Infer<S>;
|
|
128
|
+
readonly issues?: undefined;
|
|
129
|
+
} | {
|
|
130
|
+
readonly success: false;
|
|
131
|
+
readonly issues: {
|
|
132
|
+
message: string;
|
|
133
|
+
path: string[] | undefined;
|
|
134
|
+
}[];
|
|
135
|
+
readonly data?: undefined;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
declare function valibotAdapter(): ValibotAdapter;
|
|
139
|
+
|
|
140
|
+
declare class YupAdapter implements ValidationAdapter {
|
|
141
|
+
validate<S extends IfInstalled<Schema$1>>(schema: S, data: unknown): Promise<{
|
|
142
|
+
readonly success: true;
|
|
143
|
+
readonly data: Infer<S>;
|
|
144
|
+
readonly issues?: undefined;
|
|
145
|
+
} | {
|
|
146
|
+
readonly success: false;
|
|
147
|
+
readonly issues: ValidationIssue[];
|
|
148
|
+
readonly data?: undefined;
|
|
149
|
+
}>;
|
|
150
|
+
}
|
|
151
|
+
declare function yupAdapter(): YupAdapter;
|
|
152
|
+
|
|
153
|
+
export { type HandlerFunction, type HandlerServerErrorFn, type IfInstalled, type Infer, type InferMaybe, type OriginalRouteHandler, type RouteContext, RouteHandlerBuilder, type Schema, type ValidationAdapter, type ValidationIssue, createSafeRoute, valibotAdapter, yupAdapter, zodAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
+
import { GenericSchema, GenericSchemaAsync, InferOutput } from 'valibot';
|
|
3
|
+
import { Schema as Schema$1, InferType } from 'yup';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
type IfInstalled<T> = any extends T ? never : T;
|
|
7
|
+
type Schema = IfInstalled<z.ZodTypeAny> | IfInstalled<GenericSchema> | IfInstalled<GenericSchemaAsync> | IfInstalled<Schema$1> | IfInstalled<TSchema>;
|
|
8
|
+
type Infer<S extends Schema> = S extends IfInstalled<z.ZodTypeAny> ? z.infer<S> : S extends IfInstalled<GenericSchema> ? InferOutput<S> : S extends IfInstalled<GenericSchemaAsync> ? InferOutput<S> : S extends IfInstalled<Schema$1> ? InferType<S> : S extends IfInstalled<TSchema> ? Static<S> : never;
|
|
9
|
+
type ValidationIssue = {
|
|
10
|
+
message: string;
|
|
11
|
+
path?: Array<string | number | symbol>;
|
|
12
|
+
};
|
|
13
|
+
interface ValidationAdapter {
|
|
14
|
+
validate<S extends Schema>(schema: S, data: unknown): Promise<{
|
|
15
|
+
success: true;
|
|
16
|
+
data: Infer<S>;
|
|
17
|
+
} | {
|
|
18
|
+
success: false;
|
|
19
|
+
issues: ValidationIssue[];
|
|
20
|
+
}>;
|
|
21
|
+
validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown): Promise<{
|
|
22
|
+
success: true;
|
|
23
|
+
data: Infer<S>;
|
|
24
|
+
} | {
|
|
25
|
+
success: false;
|
|
26
|
+
issues: ValidationIssue[];
|
|
27
|
+
}>;
|
|
28
|
+
validate<S extends IfInstalled<GenericSchema>>(schema: S, data: unknown): Promise<{
|
|
29
|
+
success: true;
|
|
30
|
+
data: Infer<S>;
|
|
31
|
+
} | {
|
|
32
|
+
success: false;
|
|
33
|
+
issues: ValidationIssue[];
|
|
34
|
+
}>;
|
|
35
|
+
validate<S extends IfInstalled<GenericSchemaAsync>>(schema: S, data: unknown): Promise<{
|
|
36
|
+
success: true;
|
|
37
|
+
data: Infer<S>;
|
|
38
|
+
} | {
|
|
39
|
+
success: false;
|
|
40
|
+
issues: ValidationIssue[];
|
|
41
|
+
}>;
|
|
42
|
+
validate<S extends IfInstalled<Schema$1>>(schema: S, data: unknown): Promise<{
|
|
43
|
+
success: true;
|
|
44
|
+
data: Infer<S>;
|
|
45
|
+
} | {
|
|
46
|
+
success: false;
|
|
47
|
+
issues: ValidationIssue[];
|
|
48
|
+
}>;
|
|
49
|
+
validate<S extends IfInstalled<TSchema>>(schema: S, data: unknown): Promise<{
|
|
50
|
+
success: true;
|
|
51
|
+
data: Infer<S>;
|
|
52
|
+
} | {
|
|
53
|
+
success: false;
|
|
54
|
+
issues: ValidationIssue[];
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type InferMaybe<TSchema extends Schema | undefined> = TSchema extends Schema ? Infer<TSchema> : Record<string, unknown>;
|
|
59
|
+
type RouteContext<TRawParams extends Record<string, unknown> = Record<string, string | string[]>> = {
|
|
60
|
+
params: TRawParams;
|
|
61
|
+
};
|
|
62
|
+
type HandlerFunction<TParams, TQuery, TBody, TContext> = (request: Request, context: {
|
|
63
|
+
params: TParams;
|
|
64
|
+
query: TQuery;
|
|
65
|
+
body: TBody;
|
|
66
|
+
data: TContext;
|
|
67
|
+
}) => Response | Promise<Response>;
|
|
68
|
+
type OriginalRouteHandler = (request: Request, context: RouteContext) => Response | Promise<Response>;
|
|
69
|
+
type HandlerServerErrorFn = (error: Error) => Response;
|
|
70
|
+
|
|
71
|
+
type Middleware<T = Record<string, unknown>> = (request: Request) => Promise<T>;
|
|
72
|
+
type BuilderConfig<TParams extends Schema | undefined, TQuery extends Schema | undefined, TBody extends Schema | undefined> = {
|
|
73
|
+
paramsSchema: TParams;
|
|
74
|
+
querySchema: TQuery;
|
|
75
|
+
bodySchema: TBody;
|
|
76
|
+
};
|
|
77
|
+
declare class RouteHandlerBuilder<TParams extends Schema | undefined = undefined, TQuery extends Schema | undefined = undefined, TBody extends Schema | undefined = undefined, TContext extends Record<string, unknown> = Record<string, unknown>> {
|
|
78
|
+
private config;
|
|
79
|
+
private middlewares;
|
|
80
|
+
private handleServerError?;
|
|
81
|
+
private validationAdapter;
|
|
82
|
+
private baseContext;
|
|
83
|
+
constructor({ config, validationAdapter, middlewares, handleServerError, baseContext, }: {
|
|
84
|
+
config?: BuilderConfig<TParams, TQuery, TBody>;
|
|
85
|
+
middlewares?: Middleware[];
|
|
86
|
+
handleServerError?: HandlerServerErrorFn;
|
|
87
|
+
validationAdapter?: ValidationAdapter;
|
|
88
|
+
baseContext?: TContext;
|
|
89
|
+
});
|
|
90
|
+
params<T extends Schema>(schema: T): RouteHandlerBuilder<T, TQuery, TBody, TContext>;
|
|
91
|
+
query<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, T, TBody, TContext>;
|
|
92
|
+
body<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, TQuery, T, TContext>;
|
|
93
|
+
use<TReturnType extends Record<string, unknown>>(middleware: Middleware<TReturnType>): RouteHandlerBuilder<TParams, TQuery, TBody, TContext & TReturnType>;
|
|
94
|
+
handler(handler: HandlerFunction<InferMaybe<TParams>, InferMaybe<TQuery>, InferMaybe<TBody>, TContext>): OriginalRouteHandler;
|
|
95
|
+
private validateInput;
|
|
96
|
+
private getQueryParams;
|
|
97
|
+
private parseRequestBody;
|
|
98
|
+
private buildErrorResponse;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type CreateSafeRouteParams<TContext extends Record<string, unknown>> = {
|
|
102
|
+
handleServerError?: HandlerServerErrorFn;
|
|
103
|
+
validationAdapter?: ValidationAdapter;
|
|
104
|
+
baseContext?: TContext;
|
|
105
|
+
};
|
|
106
|
+
declare function createSafeRoute<TContext extends Record<string, unknown> = Record<string, unknown>>(params?: CreateSafeRouteParams<TContext>): RouteHandlerBuilder<undefined, undefined, undefined, TContext>;
|
|
107
|
+
|
|
108
|
+
declare class ZodAdapter implements ValidationAdapter {
|
|
109
|
+
validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown): Promise<{
|
|
110
|
+
readonly success: true;
|
|
111
|
+
readonly data: Infer<S>;
|
|
112
|
+
readonly issues?: undefined;
|
|
113
|
+
} | {
|
|
114
|
+
readonly success: false;
|
|
115
|
+
readonly issues: {
|
|
116
|
+
message: string;
|
|
117
|
+
path: PropertyKey[];
|
|
118
|
+
}[];
|
|
119
|
+
readonly data?: undefined;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
declare function zodAdapter(): ZodAdapter;
|
|
123
|
+
|
|
124
|
+
declare class ValibotAdapter implements ValidationAdapter {
|
|
125
|
+
validate<S extends IfInstalled<GenericSchema | GenericSchemaAsync>>(schema: S, data: unknown): Promise<{
|
|
126
|
+
readonly success: true;
|
|
127
|
+
readonly data: Infer<S>;
|
|
128
|
+
readonly issues?: undefined;
|
|
129
|
+
} | {
|
|
130
|
+
readonly success: false;
|
|
131
|
+
readonly issues: {
|
|
132
|
+
message: string;
|
|
133
|
+
path: string[] | undefined;
|
|
134
|
+
}[];
|
|
135
|
+
readonly data?: undefined;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
declare function valibotAdapter(): ValibotAdapter;
|
|
139
|
+
|
|
140
|
+
declare class YupAdapter implements ValidationAdapter {
|
|
141
|
+
validate<S extends IfInstalled<Schema$1>>(schema: S, data: unknown): Promise<{
|
|
142
|
+
readonly success: true;
|
|
143
|
+
readonly data: Infer<S>;
|
|
144
|
+
readonly issues?: undefined;
|
|
145
|
+
} | {
|
|
146
|
+
readonly success: false;
|
|
147
|
+
readonly issues: ValidationIssue[];
|
|
148
|
+
readonly data?: undefined;
|
|
149
|
+
}>;
|
|
150
|
+
}
|
|
151
|
+
declare function yupAdapter(): YupAdapter;
|
|
152
|
+
|
|
153
|
+
export { type HandlerFunction, type HandlerServerErrorFn, type IfInstalled, type Infer, type InferMaybe, type OriginalRouteHandler, type RouteContext, RouteHandlerBuilder, type Schema, type ValidationAdapter, type ValidationIssue, createSafeRoute, valibotAdapter, yupAdapter, zodAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var f=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var P=(n,e)=>{for(var r in e)f(n,r,{get:e[r],enumerable:!0})},B=(n,e,r,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of A(e))!E.call(n,a)&&a!==r&&f(n,a,{get:()=>e[a],enumerable:!(t=R(e,a))||t.enumerable});return n};var V=n=>B(f({},"__esModule",{value:!0}),n);var H={};P(H,{RouteHandlerBuilder:()=>i,createSafeRoute:()=>S,valibotAdapter:()=>x,yupAdapter:()=>w,zodAdapter:()=>o});module.exports=V(H);var y=class{async validate(e,r){let t=await e.safeParseAsync(r);return t.success?{success:!0,data:t.data}:{success:!1,issues:t.error.issues.map(({message:a,path:s})=>({message:a,path:s}))}}};function o(){return new y}var d=class extends Error{constructor(e){super(e),this.name="BodyParsingError"}},i=class n{config;middlewares;handleServerError;validationAdapter;baseContext;constructor({config:e={paramsSchema:void 0,querySchema:void 0,bodySchema:void 0},validationAdapter:r=o(),middlewares:t=[],handleServerError:a,baseContext:s}){this.config=e,this.middlewares=t,this.handleServerError=a,this.validationAdapter=r,this.baseContext=s??{}}params(e){return new n({config:{...this.config,paramsSchema:e},middlewares:this.middlewares,handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}query(e){return new n({config:{...this.config,querySchema:e},middlewares:this.middlewares,handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}body(e){return new n({config:{...this.config,bodySchema:e},middlewares:this.middlewares,handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}use(e){return new n({config:this.config,middlewares:[...this.middlewares,e],handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}handler(e){return async(r,t)=>{try{let s=(t??{params:{}}).params??{},I=this.getQueryParams(r),C=await this.parseRequestBody(r),l=await this.validateInput(this.config.paramsSchema,s,"Invalid params");if(l.success===!1)return l.response;let c=await this.validateInput(this.config.querySchema,I,"Invalid query");if(c.success===!1)return c.response;let p=await this.validateInput(this.config.bodySchema,C,"Invalid body");if(p.success===!1)return p.response;let m={...this.baseContext};for(let g of this.middlewares){let b=await g(r);m={...m,...b}}return e(r,{params:l.data,query:c.data,body:p.data,data:m})}catch(a){return a instanceof d?this.buildErrorResponse(a.message,void 0,400):this.handleServerError?this.handleServerError(a):this.buildErrorResponse("Internal server error",void 0,500)}}}async validateInput(e,r,t){if(!e)return{success:!0,data:r??{}};let a=await this.validationAdapter.validate(e,r);return a.success===!0?{success:!0,data:a.data}:{success:!1,response:this.buildErrorResponse(t,a.issues)}}getQueryParams(e){let r=new URL(e.url);return Object.fromEntries(r.searchParams.entries())}async parseRequestBody(e){if(!this.config.bodySchema)return{};let r=e.headers.get("content-type")??"",t=await e.text();if(t.length===0)return{};if(!r.includes("application/json"))throw new d("Unsupported content type. Expected application/json.");try{return JSON.parse(t)}catch{throw new d("Invalid JSON body.")}}buildErrorResponse(e,r,t=400){return new Response(JSON.stringify({message:e,issues:r}),{status:t,headers:{"content-type":"application/json"}})}};function S(n){return new i({handleServerError:n?.handleServerError,validationAdapter:n?.validationAdapter??o(),baseContext:n?.baseContext})}var u=require("valibot"),h=class{async validate(e,r){let t=await(0,u.safeParseAsync)(e,r);return t.success?{success:!0,data:t.output}:{success:!1,issues:t.issues.map(a=>({message:a.message,path:(0,u.getDotPath)(a)?.split(".")}))}}};function x(){return new h}var v=require("yup"),T=class{async validate(e,r){try{return{success:!0,data:await e.validate(r,{strict:!0})}}catch(t){if(t instanceof v.ValidationError){let{message:a,path:s}=t;return{success:!1,issues:[{message:a,path:s&&s.length>0?s.split("."):void 0}]}}throw t}}};function w(){return new T}0&&(module.exports={RouteHandlerBuilder,createSafeRoute,valibotAdapter,yupAdapter,zodAdapter});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/adapters/zod.ts","../src/routeHandlerBuilder.ts","../src/createSafeRoute.ts","../src/adapters/valibot.ts","../src/adapters/yup.ts"],"sourcesContent":["export { createSafeRoute } from './createSafeRoute';\nexport { RouteHandlerBuilder } from './routeHandlerBuilder';\nexport {\n type HandlerFunction,\n type HandlerServerErrorFn,\n type InferMaybe,\n type OriginalRouteHandler,\n type RouteContext,\n} from './types';\nexport {\n type IfInstalled,\n type Infer,\n type Schema,\n type ValidationAdapter,\n type ValidationIssue,\n} from './adapters/types';\nexport { zodAdapter } from './adapters/zod';\nexport { valibotAdapter } from './adapters/valibot';\nexport { yupAdapter } from './adapters/yup';\n","// Code based on https://github.com/decs/typeschema/blob/main/packages/zod/src/validation.ts\n// MIT License\n// Copyright (c) 2023 André Costa\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nimport type { z } from 'zod';\n\nimport type { IfInstalled, Infer, ValidationAdapter } from './types';\n\nclass ZodAdapter implements ValidationAdapter {\n async validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown) {\n const result = await schema.safeParseAsync(data);\n\n if (result.success) {\n return {\n success: true,\n data: result.data as Infer<S>,\n } as const;\n }\n\n return {\n success: false,\n issues: result.error.issues.map(({ message, path }) => ({ message, path })),\n } as const;\n }\n}\n\nexport function zodAdapter() {\n return new ZodAdapter();\n}\n","import { Schema, ValidationAdapter, ValidationIssue } from './adapters/types';\nimport { zodAdapter } from './adapters/zod';\nimport { HandlerFunction, HandlerServerErrorFn, InferMaybe, OriginalRouteHandler, RouteContext } from './types';\n\ntype Middleware<T = Record<string, unknown>> = (request: Request) => Promise<T>;\n\ntype BuilderConfig<\n TParams extends Schema | undefined,\n TQuery extends Schema | undefined,\n TBody extends Schema | undefined,\n> = {\n paramsSchema: TParams;\n querySchema: TQuery;\n bodySchema: TBody;\n};\n\nclass BodyParsingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BodyParsingError';\n }\n}\n\nexport class RouteHandlerBuilder<\n TParams extends Schema | undefined = undefined,\n TQuery extends Schema | undefined = undefined,\n TBody extends Schema | undefined = undefined,\n TContext extends Record<string, unknown> = Record<string, unknown>,\n> {\n private config: BuilderConfig<TParams, TQuery, TBody>;\n private middlewares: Middleware[];\n private handleServerError?: HandlerServerErrorFn;\n private validationAdapter: ValidationAdapter;\n private baseContext: TContext;\n\n constructor({\n config = {\n paramsSchema: undefined as TParams,\n querySchema: undefined as TQuery,\n bodySchema: undefined as TBody,\n },\n validationAdapter = zodAdapter(),\n middlewares = [],\n handleServerError,\n baseContext,\n }: {\n config?: BuilderConfig<TParams, TQuery, TBody>;\n middlewares?: Middleware[];\n handleServerError?: HandlerServerErrorFn;\n validationAdapter?: ValidationAdapter;\n baseContext?: TContext;\n }) {\n this.config = config;\n this.middlewares = middlewares;\n this.handleServerError = handleServerError;\n this.validationAdapter = validationAdapter;\n this.baseContext = (baseContext ?? {}) as TContext;\n }\n\n params<T extends Schema>(schema: T): RouteHandlerBuilder<T, TQuery, TBody, TContext> {\n return new RouteHandlerBuilder<T, TQuery, TBody, TContext>({\n config: { ...this.config, paramsSchema: schema },\n middlewares: this.middlewares,\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext,\n });\n }\n\n query<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, T, TBody, TContext> {\n return new RouteHandlerBuilder<TParams, T, TBody, TContext>({\n config: { ...this.config, querySchema: schema },\n middlewares: this.middlewares,\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext,\n });\n }\n\n body<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, TQuery, T, TContext> {\n return new RouteHandlerBuilder<TParams, TQuery, T, TContext>({\n config: { ...this.config, bodySchema: schema },\n middlewares: this.middlewares,\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext,\n });\n }\n\n use<TReturnType extends Record<string, unknown>>(middleware: Middleware<TReturnType>) {\n return new RouteHandlerBuilder<TParams, TQuery, TBody, TContext & TReturnType>({\n config: this.config,\n middlewares: [...this.middlewares, middleware],\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext as unknown as TContext & TReturnType,\n });\n }\n\n handler(\n handler: HandlerFunction<InferMaybe<TParams>, InferMaybe<TQuery>, InferMaybe<TBody>, TContext>,\n ): OriginalRouteHandler {\n return async (request, context): Promise<Response> => {\n try {\n const routeContext = context ?? ({ params: {} } as RouteContext);\n const paramsInput = routeContext.params ?? {};\n const queryInput = this.getQueryParams(request);\n const bodyInput = await this.parseRequestBody(request);\n\n const paramsValidation = await this.validateInput(this.config.paramsSchema, paramsInput, 'Invalid params');\n if (paramsValidation.success === false) {\n return paramsValidation.response;\n }\n\n const queryValidation = await this.validateInput(this.config.querySchema, queryInput, 'Invalid query');\n if (queryValidation.success === false) {\n return queryValidation.response;\n }\n\n const bodyValidation = await this.validateInput(this.config.bodySchema, bodyInput, 'Invalid body');\n if (bodyValidation.success === false) {\n return bodyValidation.response;\n }\n\n let middlewareContext: TContext = { ...this.baseContext };\n for (const middleware of this.middlewares) {\n const result = await middleware(request);\n middlewareContext = { ...middlewareContext, ...result };\n }\n\n return handler(request, {\n params: paramsValidation.data,\n query: queryValidation.data,\n body: bodyValidation.data,\n data: middlewareContext,\n });\n } catch (error) {\n if (error instanceof BodyParsingError) {\n return this.buildErrorResponse(error.message, undefined, 400);\n }\n\n if (this.handleServerError) {\n return this.handleServerError(error as Error);\n }\n\n return this.buildErrorResponse('Internal server error', undefined, 500);\n }\n };\n }\n\n private async validateInput<S extends Schema | undefined>(\n schema: S,\n data: unknown,\n errorMessage: string,\n ): Promise<{ success: true; data: InferMaybe<S> } | { success: false; response: Response }> {\n if (!schema) {\n return { success: true, data: (data ?? {}) as InferMaybe<S> };\n }\n\n const result = await this.validationAdapter.validate(schema, data);\n if (result.success === true) {\n return { success: true, data: result.data as InferMaybe<S> };\n }\n\n return { success: false, response: this.buildErrorResponse(errorMessage, result.issues) };\n }\n\n private getQueryParams(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n }\n\n private async parseRequestBody(request: Request): Promise<unknown> {\n if (!this.config.bodySchema) {\n return {};\n }\n\n const contentType = request.headers.get('content-type') ?? '';\n const rawBody = await request.text();\n\n if (rawBody.length === 0) {\n return {};\n }\n\n if (!contentType.includes('application/json')) {\n throw new BodyParsingError('Unsupported content type. Expected application/json.');\n }\n\n try {\n return JSON.parse(rawBody);\n } catch (error) {\n throw new BodyParsingError('Invalid JSON body.');\n }\n }\n\n private buildErrorResponse(message: string, issues?: ValidationIssue[], status = 400) {\n return new Response(JSON.stringify({ message, issues }), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n }\n}\n","import { ValidationAdapter } from './adapters/types';\nimport { zodAdapter } from './adapters/zod';\nimport { RouteHandlerBuilder } from './routeHandlerBuilder';\nimport { HandlerServerErrorFn } from './types';\n\ntype CreateSafeRouteParams<TContext extends Record<string, unknown>> = {\n handleServerError?: HandlerServerErrorFn;\n validationAdapter?: ValidationAdapter;\n baseContext?: TContext;\n};\n\nexport function createSafeRoute<TContext extends Record<string, unknown> = Record<string, unknown>>(\n params?: CreateSafeRouteParams<TContext>,\n) {\n return new RouteHandlerBuilder<undefined, undefined, undefined, TContext>({\n handleServerError: params?.handleServerError,\n validationAdapter: params?.validationAdapter ?? zodAdapter(),\n baseContext: params?.baseContext,\n });\n}\n","// Code based on https://github.com/decs/typeschema/blob/main/packages/valibot/src/validation.ts\n// MIT License\n// Copyright (c) 2023 André Costa\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nimport { type GenericSchema, type GenericSchemaAsync, getDotPath, safeParseAsync } from 'valibot';\n\nimport type { IfInstalled, Infer, ValidationAdapter } from './types';\n\nclass ValibotAdapter implements ValidationAdapter {\n async validate<S extends IfInstalled<GenericSchema | GenericSchemaAsync>>(schema: S, data: unknown) {\n const result = await safeParseAsync(schema, data);\n\n if (result.success) {\n return {\n success: true,\n data: result.output as Infer<S>,\n } as const;\n }\n\n return {\n success: false,\n issues: result.issues.map((issue) => ({\n message: issue.message,\n path: getDotPath(issue)?.split('.'),\n })),\n } as const;\n }\n}\n\nexport function valibotAdapter() {\n return new ValibotAdapter();\n}\n","// Code based on https://github.com/decs/typeschema/blob/main/packages/yup/src/validation.ts\n// MIT License\n// Copyright (c) 2023 André Costa\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nimport type { Schema as YupSchema } from 'yup';\nimport { ValidationError } from 'yup';\n\nimport type { IfInstalled, Infer, ValidationAdapter, ValidationIssue } from './types';\n\nclass YupAdapter implements ValidationAdapter {\n async validate<S extends IfInstalled<YupSchema>>(schema: S, data: unknown) {\n try {\n const result = await schema.validate(data, { strict: true });\n\n return {\n success: true,\n data: result as Infer<S>,\n } as const;\n } catch (e) {\n if (e instanceof ValidationError) {\n const { message, path } = e;\n\n return {\n success: false,\n issues: [\n {\n message,\n path: path && path.length > 0 ? path.split('.') : undefined,\n },\n ] as ValidationIssue[],\n } as const;\n }\n\n throw e;\n }\n }\n}\n\nexport function yupAdapter() {\n return new YupAdapter();\n}\n"],"mappings":"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","names":["src_exports","__export","RouteHandlerBuilder","createSafeRoute","valibotAdapter","yupAdapter","zodAdapter","__toCommonJS","ZodAdapter","schema","data","result","message","path","zodAdapter","BodyParsingError","message","RouteHandlerBuilder","_RouteHandlerBuilder","config","validationAdapter","zodAdapter","middlewares","handleServerError","baseContext","schema","middleware","handler","request","context","paramsInput","queryInput","bodyInput","paramsValidation","queryValidation","bodyValidation","middlewareContext","result","error","data","errorMessage","url","contentType","rawBody","issues","status","createSafeRoute","params","RouteHandlerBuilder","zodAdapter","import_valibot","ValibotAdapter","schema","data","result","issue","valibotAdapter","import_yup","YupAdapter","schema","data","e","message","path","yupAdapter"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var m=class{async validate(e,t){let r=await e.safeParseAsync(t);return r.success?{success:!0,data:r.data}:{success:!1,issues:r.error.issues.map(({message:a,path:s})=>({message:a,path:s}))}}};function o(){return new m}var i=class extends Error{constructor(e){super(e),this.name="BodyParsingError"}},d=class n{config;middlewares;handleServerError;validationAdapter;baseContext;constructor({config:e={paramsSchema:void 0,querySchema:void 0,bodySchema:void 0},validationAdapter:t=o(),middlewares:r=[],handleServerError:a,baseContext:s}){this.config=e,this.middlewares=r,this.handleServerError=a,this.validationAdapter=t,this.baseContext=s??{}}params(e){return new n({config:{...this.config,paramsSchema:e},middlewares:this.middlewares,handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}query(e){return new n({config:{...this.config,querySchema:e},middlewares:this.middlewares,handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}body(e){return new n({config:{...this.config,bodySchema:e},middlewares:this.middlewares,handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}use(e){return new n({config:this.config,middlewares:[...this.middlewares,e],handleServerError:this.handleServerError,validationAdapter:this.validationAdapter,baseContext:this.baseContext})}handler(e){return async(t,r)=>{try{let s=(r??{params:{}}).params??{},h=this.getQueryParams(t),T=await this.parseRequestBody(t),u=await this.validateInput(this.config.paramsSchema,s,"Invalid params");if(u.success===!1)return u.response;let l=await this.validateInput(this.config.querySchema,h,"Invalid query");if(l.success===!1)return l.response;let c=await this.validateInput(this.config.bodySchema,T,"Invalid body");if(c.success===!1)return c.response;let p={...this.baseContext};for(let S of this.middlewares){let x=await S(t);p={...p,...x}}return e(t,{params:u.data,query:l.data,body:c.data,data:p})}catch(a){return a instanceof i?this.buildErrorResponse(a.message,void 0,400):this.handleServerError?this.handleServerError(a):this.buildErrorResponse("Internal server error",void 0,500)}}}async validateInput(e,t,r){if(!e)return{success:!0,data:t??{}};let a=await this.validationAdapter.validate(e,t);return a.success===!0?{success:!0,data:a.data}:{success:!1,response:this.buildErrorResponse(r,a.issues)}}getQueryParams(e){let t=new URL(e.url);return Object.fromEntries(t.searchParams.entries())}async parseRequestBody(e){if(!this.config.bodySchema)return{};let t=e.headers.get("content-type")??"",r=await e.text();if(r.length===0)return{};if(!t.includes("application/json"))throw new i("Unsupported content type. Expected application/json.");try{return JSON.parse(r)}catch{throw new i("Invalid JSON body.")}}buildErrorResponse(e,t,r=400){return new Response(JSON.stringify({message:e,issues:t}),{status:r,headers:{"content-type":"application/json"}})}};function v(n){return new d({handleServerError:n?.handleServerError,validationAdapter:n?.validationAdapter??o(),baseContext:n?.baseContext})}import{getDotPath as w,safeParseAsync as I}from"valibot";var f=class{async validate(e,t){let r=await I(e,t);return r.success?{success:!0,data:r.output}:{success:!1,issues:r.issues.map(a=>({message:a.message,path:w(a)?.split(".")}))}}};function C(){return new f}import{ValidationError as g}from"yup";var y=class{async validate(e,t){try{return{success:!0,data:await e.validate(t,{strict:!0})}}catch(r){if(r instanceof g){let{message:a,path:s}=r;return{success:!1,issues:[{message:a,path:s&&s.length>0?s.split("."):void 0}]}}throw r}}};function b(){return new y}export{d as RouteHandlerBuilder,v as createSafeRoute,C as valibotAdapter,b as yupAdapter,o as zodAdapter};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/zod.ts","../src/routeHandlerBuilder.ts","../src/createSafeRoute.ts","../src/adapters/valibot.ts","../src/adapters/yup.ts"],"sourcesContent":["// Code based on https://github.com/decs/typeschema/blob/main/packages/zod/src/validation.ts\n// MIT License\n// Copyright (c) 2023 André Costa\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nimport type { z } from 'zod';\n\nimport type { IfInstalled, Infer, ValidationAdapter } from './types';\n\nclass ZodAdapter implements ValidationAdapter {\n async validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown) {\n const result = await schema.safeParseAsync(data);\n\n if (result.success) {\n return {\n success: true,\n data: result.data as Infer<S>,\n } as const;\n }\n\n return {\n success: false,\n issues: result.error.issues.map(({ message, path }) => ({ message, path })),\n } as const;\n }\n}\n\nexport function zodAdapter() {\n return new ZodAdapter();\n}\n","import { Schema, ValidationAdapter, ValidationIssue } from './adapters/types';\nimport { zodAdapter } from './adapters/zod';\nimport { HandlerFunction, HandlerServerErrorFn, InferMaybe, OriginalRouteHandler, RouteContext } from './types';\n\ntype Middleware<T = Record<string, unknown>> = (request: Request) => Promise<T>;\n\ntype BuilderConfig<\n TParams extends Schema | undefined,\n TQuery extends Schema | undefined,\n TBody extends Schema | undefined,\n> = {\n paramsSchema: TParams;\n querySchema: TQuery;\n bodySchema: TBody;\n};\n\nclass BodyParsingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BodyParsingError';\n }\n}\n\nexport class RouteHandlerBuilder<\n TParams extends Schema | undefined = undefined,\n TQuery extends Schema | undefined = undefined,\n TBody extends Schema | undefined = undefined,\n TContext extends Record<string, unknown> = Record<string, unknown>,\n> {\n private config: BuilderConfig<TParams, TQuery, TBody>;\n private middlewares: Middleware[];\n private handleServerError?: HandlerServerErrorFn;\n private validationAdapter: ValidationAdapter;\n private baseContext: TContext;\n\n constructor({\n config = {\n paramsSchema: undefined as TParams,\n querySchema: undefined as TQuery,\n bodySchema: undefined as TBody,\n },\n validationAdapter = zodAdapter(),\n middlewares = [],\n handleServerError,\n baseContext,\n }: {\n config?: BuilderConfig<TParams, TQuery, TBody>;\n middlewares?: Middleware[];\n handleServerError?: HandlerServerErrorFn;\n validationAdapter?: ValidationAdapter;\n baseContext?: TContext;\n }) {\n this.config = config;\n this.middlewares = middlewares;\n this.handleServerError = handleServerError;\n this.validationAdapter = validationAdapter;\n this.baseContext = (baseContext ?? {}) as TContext;\n }\n\n params<T extends Schema>(schema: T): RouteHandlerBuilder<T, TQuery, TBody, TContext> {\n return new RouteHandlerBuilder<T, TQuery, TBody, TContext>({\n config: { ...this.config, paramsSchema: schema },\n middlewares: this.middlewares,\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext,\n });\n }\n\n query<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, T, TBody, TContext> {\n return new RouteHandlerBuilder<TParams, T, TBody, TContext>({\n config: { ...this.config, querySchema: schema },\n middlewares: this.middlewares,\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext,\n });\n }\n\n body<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, TQuery, T, TContext> {\n return new RouteHandlerBuilder<TParams, TQuery, T, TContext>({\n config: { ...this.config, bodySchema: schema },\n middlewares: this.middlewares,\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext,\n });\n }\n\n use<TReturnType extends Record<string, unknown>>(middleware: Middleware<TReturnType>) {\n return new RouteHandlerBuilder<TParams, TQuery, TBody, TContext & TReturnType>({\n config: this.config,\n middlewares: [...this.middlewares, middleware],\n handleServerError: this.handleServerError,\n validationAdapter: this.validationAdapter,\n baseContext: this.baseContext as unknown as TContext & TReturnType,\n });\n }\n\n handler(\n handler: HandlerFunction<InferMaybe<TParams>, InferMaybe<TQuery>, InferMaybe<TBody>, TContext>,\n ): OriginalRouteHandler {\n return async (request, context): Promise<Response> => {\n try {\n const routeContext = context ?? ({ params: {} } as RouteContext);\n const paramsInput = routeContext.params ?? {};\n const queryInput = this.getQueryParams(request);\n const bodyInput = await this.parseRequestBody(request);\n\n const paramsValidation = await this.validateInput(this.config.paramsSchema, paramsInput, 'Invalid params');\n if (paramsValidation.success === false) {\n return paramsValidation.response;\n }\n\n const queryValidation = await this.validateInput(this.config.querySchema, queryInput, 'Invalid query');\n if (queryValidation.success === false) {\n return queryValidation.response;\n }\n\n const bodyValidation = await this.validateInput(this.config.bodySchema, bodyInput, 'Invalid body');\n if (bodyValidation.success === false) {\n return bodyValidation.response;\n }\n\n let middlewareContext: TContext = { ...this.baseContext };\n for (const middleware of this.middlewares) {\n const result = await middleware(request);\n middlewareContext = { ...middlewareContext, ...result };\n }\n\n return handler(request, {\n params: paramsValidation.data,\n query: queryValidation.data,\n body: bodyValidation.data,\n data: middlewareContext,\n });\n } catch (error) {\n if (error instanceof BodyParsingError) {\n return this.buildErrorResponse(error.message, undefined, 400);\n }\n\n if (this.handleServerError) {\n return this.handleServerError(error as Error);\n }\n\n return this.buildErrorResponse('Internal server error', undefined, 500);\n }\n };\n }\n\n private async validateInput<S extends Schema | undefined>(\n schema: S,\n data: unknown,\n errorMessage: string,\n ): Promise<{ success: true; data: InferMaybe<S> } | { success: false; response: Response }> {\n if (!schema) {\n return { success: true, data: (data ?? {}) as InferMaybe<S> };\n }\n\n const result = await this.validationAdapter.validate(schema, data);\n if (result.success === true) {\n return { success: true, data: result.data as InferMaybe<S> };\n }\n\n return { success: false, response: this.buildErrorResponse(errorMessage, result.issues) };\n }\n\n private getQueryParams(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n }\n\n private async parseRequestBody(request: Request): Promise<unknown> {\n if (!this.config.bodySchema) {\n return {};\n }\n\n const contentType = request.headers.get('content-type') ?? '';\n const rawBody = await request.text();\n\n if (rawBody.length === 0) {\n return {};\n }\n\n if (!contentType.includes('application/json')) {\n throw new BodyParsingError('Unsupported content type. Expected application/json.');\n }\n\n try {\n return JSON.parse(rawBody);\n } catch (error) {\n throw new BodyParsingError('Invalid JSON body.');\n }\n }\n\n private buildErrorResponse(message: string, issues?: ValidationIssue[], status = 400) {\n return new Response(JSON.stringify({ message, issues }), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n }\n}\n","import { ValidationAdapter } from './adapters/types';\nimport { zodAdapter } from './adapters/zod';\nimport { RouteHandlerBuilder } from './routeHandlerBuilder';\nimport { HandlerServerErrorFn } from './types';\n\ntype CreateSafeRouteParams<TContext extends Record<string, unknown>> = {\n handleServerError?: HandlerServerErrorFn;\n validationAdapter?: ValidationAdapter;\n baseContext?: TContext;\n};\n\nexport function createSafeRoute<TContext extends Record<string, unknown> = Record<string, unknown>>(\n params?: CreateSafeRouteParams<TContext>,\n) {\n return new RouteHandlerBuilder<undefined, undefined, undefined, TContext>({\n handleServerError: params?.handleServerError,\n validationAdapter: params?.validationAdapter ?? zodAdapter(),\n baseContext: params?.baseContext,\n });\n}\n","// Code based on https://github.com/decs/typeschema/blob/main/packages/valibot/src/validation.ts\n// MIT License\n// Copyright (c) 2023 André Costa\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nimport { type GenericSchema, type GenericSchemaAsync, getDotPath, safeParseAsync } from 'valibot';\n\nimport type { IfInstalled, Infer, ValidationAdapter } from './types';\n\nclass ValibotAdapter implements ValidationAdapter {\n async validate<S extends IfInstalled<GenericSchema | GenericSchemaAsync>>(schema: S, data: unknown) {\n const result = await safeParseAsync(schema, data);\n\n if (result.success) {\n return {\n success: true,\n data: result.output as Infer<S>,\n } as const;\n }\n\n return {\n success: false,\n issues: result.issues.map((issue) => ({\n message: issue.message,\n path: getDotPath(issue)?.split('.'),\n })),\n } as const;\n }\n}\n\nexport function valibotAdapter() {\n return new ValibotAdapter();\n}\n","// Code based on https://github.com/decs/typeschema/blob/main/packages/yup/src/validation.ts\n// MIT License\n// Copyright (c) 2023 André Costa\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nimport type { Schema as YupSchema } from 'yup';\nimport { ValidationError } from 'yup';\n\nimport type { IfInstalled, Infer, ValidationAdapter, ValidationIssue } from './types';\n\nclass YupAdapter implements ValidationAdapter {\n async validate<S extends IfInstalled<YupSchema>>(schema: S, data: unknown) {\n try {\n const result = await schema.validate(data, { strict: true });\n\n return {\n success: true,\n data: result as Infer<S>,\n } as const;\n } catch (e) {\n if (e instanceof ValidationError) {\n const { message, path } = e;\n\n return {\n success: false,\n issues: [\n {\n message,\n path: path && path.length > 0 ? path.split('.') : undefined,\n },\n ] as ValidationIssue[],\n } as const;\n }\n\n throw e;\n }\n }\n}\n\nexport function yupAdapter() {\n return new YupAdapter();\n}\n"],"mappings":"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","names":["ZodAdapter","schema","data","result","message","path","zodAdapter","BodyParsingError","message","RouteHandlerBuilder","_RouteHandlerBuilder","config","validationAdapter","zodAdapter","middlewares","handleServerError","baseContext","schema","middleware","handler","request","context","paramsInput","queryInput","bodyInput","paramsValidation","queryValidation","bodyValidation","middlewareContext","result","error","data","errorMessage","url","contentType","rawBody","issues","status","createSafeRoute","params","RouteHandlerBuilder","zodAdapter","getDotPath","safeParseAsync","ValibotAdapter","schema","data","result","issue","valibotAdapter","ValidationError","YupAdapter","schema","data","e","message","path","yupAdapter"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mhbdev/next-safe-route",
|
|
3
|
+
"version": "0.0.31",
|
|
4
|
+
"description": "A safer way to define route handlers in Next.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"next",
|
|
7
|
+
"nextjs",
|
|
8
|
+
"next route",
|
|
9
|
+
"next safe route",
|
|
10
|
+
"nextjs safe",
|
|
11
|
+
"nextjs safe route",
|
|
12
|
+
"next api route",
|
|
13
|
+
"nextjs api route"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/mhbdev/next-safe-route#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/mhbdev/next-safe-route/issues",
|
|
18
|
+
"email": "richard@solomou.dev"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/mhbdev/next-safe-route/next-safe-route"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "MonoBit",
|
|
27
|
+
"email": "1839491@gmail.com"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "pnpm build --watch",
|
|
38
|
+
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
39
|
+
"lint": "tsc --noEmit && eslint .",
|
|
40
|
+
"lint-staged": "lint-staged",
|
|
41
|
+
"prepare": "husky",
|
|
42
|
+
"release": "release-it",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:coverage": "vitest run --coverage",
|
|
45
|
+
"test:watch": "vitest"
|
|
46
|
+
},
|
|
47
|
+
"lint-staged": {
|
|
48
|
+
"**/*.{js,jsx,ts,tsx}": [
|
|
49
|
+
"eslint --fix",
|
|
50
|
+
"prettier --write"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@sinclair/typebox": "^0.33.7",
|
|
55
|
+
"valibot": "^0.39.0",
|
|
56
|
+
"yup": "^1.4.0",
|
|
57
|
+
"zod": "^4.2.1"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@sinclair/typebox": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"valibot": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"yup": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@sinclair/typebox": "^0.33.7",
|
|
72
|
+
"@swc/core": "^1.5.29",
|
|
73
|
+
"@tronite/style-guide": "^0.0.13",
|
|
74
|
+
"@types/node": "^20.14.2",
|
|
75
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
76
|
+
"eslint": "^8.57.0",
|
|
77
|
+
"husky": "^9.0.11",
|
|
78
|
+
"lint-staged": "^15.2.7",
|
|
79
|
+
"prettier": "^3.3.2",
|
|
80
|
+
"release-it": "^17.3.0",
|
|
81
|
+
"tsup": "^8.1.0",
|
|
82
|
+
"typescript": "^5.4.5",
|
|
83
|
+
"valibot": "^0.39.0",
|
|
84
|
+
"vitest": "^1.6.0",
|
|
85
|
+
"yup": "^1.4.0",
|
|
86
|
+
"zod": "^4.2.1"
|
|
87
|
+
},
|
|
88
|
+
"packageManager": "pnpm@9.1.2",
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=18"
|
|
91
|
+
},
|
|
92
|
+
"publishConfig": {
|
|
93
|
+
"access": "public"
|
|
94
|
+
},
|
|
95
|
+
"release-it": {
|
|
96
|
+
"git": {
|
|
97
|
+
"requireBranch": "main",
|
|
98
|
+
"commitMessage": "chore: release v${version}"
|
|
99
|
+
},
|
|
100
|
+
"github": {
|
|
101
|
+
"release": true,
|
|
102
|
+
"releaseName": "${version}"
|
|
103
|
+
},
|
|
104
|
+
"hooks": {
|
|
105
|
+
"before:init": [
|
|
106
|
+
"git pull",
|
|
107
|
+
"pnpm lint",
|
|
108
|
+
"pnpm build"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|