@carno.js/core 1.0.2 → 1.0.3
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 -674
- package/README.md +188 -188
- package/dist/Carno.js +466 -269
- package/dist/bun/index.js +6 -19
- package/dist/bun/index.js.map +27 -28
- package/package.json +3 -2
- package/src/Carno.ts +605 -605
- package/src/DefaultRoutes.ts +34 -34
- package/src/cache/CacheDriver.ts +50 -50
- package/src/cache/CacheService.ts +139 -139
- package/src/cache/MemoryDriver.ts +104 -104
- package/src/cache/RedisDriver.ts +116 -116
- package/src/compiler/JITCompiler.ts +167 -167
- package/src/container/Container.ts +168 -168
- package/src/context/Context.ts +128 -128
- package/src/cors/CorsHandler.ts +145 -145
- package/src/decorators/Controller.ts +63 -63
- package/src/decorators/Inject.ts +16 -16
- package/src/decorators/Middleware.ts +22 -22
- package/src/decorators/Service.ts +18 -18
- package/src/decorators/methods.ts +58 -58
- package/src/decorators/params.ts +47 -47
- package/src/events/Lifecycle.ts +97 -97
- package/src/exceptions/HttpException.ts +99 -99
- package/src/index.ts +92 -92
- package/src/metadata.ts +46 -46
- package/src/middleware/CarnoMiddleware.ts +14 -14
- package/src/router/RadixRouter.ts +225 -225
- package/src/testing/TestHarness.ts +177 -177
- package/src/utils/Metadata.ts +43 -43
- package/src/validation/ValibotAdapter.ts +95 -95
- package/src/validation/ValidatorAdapter.ts +69 -69
- package/src/validation/ZodAdapter.ts +102 -102
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
import type { Server } from 'bun';
|
|
2
|
-
import { Carno, type CarnoConfig } from '../Carno';
|
|
3
|
-
import { Container, type Token } from '../container/Container';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Test configuration options.
|
|
7
|
-
*/
|
|
8
|
-
export interface TestOptions {
|
|
9
|
-
config?: CarnoConfig;
|
|
10
|
-
listen?: boolean | number;
|
|
11
|
-
port?: number;
|
|
12
|
-
controllers?: (new (...args: any[]) => any)[];
|
|
13
|
-
services?: (Token | any)[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Test harness - provides utilities for testing Carno applications.
|
|
18
|
-
*/
|
|
19
|
-
export interface TestHarness {
|
|
20
|
-
/** The Carno app instance */
|
|
21
|
-
app: Carno;
|
|
22
|
-
|
|
23
|
-
/** The internal DI container */
|
|
24
|
-
container: Container;
|
|
25
|
-
|
|
26
|
-
/** The HTTP server (if listening) */
|
|
27
|
-
server?: Server<any>;
|
|
28
|
-
|
|
29
|
-
/** The port the server is running on */
|
|
30
|
-
port?: number;
|
|
31
|
-
|
|
32
|
-
/** Resolve a service from the container */
|
|
33
|
-
resolve<T>(token: Token<T>): T;
|
|
34
|
-
|
|
35
|
-
/** Make an HTTP request to the app */
|
|
36
|
-
request(path: string, init?: RequestInit): Promise<Response>;
|
|
37
|
-
|
|
38
|
-
/** Make a GET request */
|
|
39
|
-
get(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
40
|
-
|
|
41
|
-
/** Make a POST request */
|
|
42
|
-
post(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
43
|
-
|
|
44
|
-
/** Make a PUT request */
|
|
45
|
-
put(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
46
|
-
|
|
47
|
-
/** Make a DELETE request */
|
|
48
|
-
delete(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
49
|
-
|
|
50
|
-
/** Close the test harness and cleanup */
|
|
51
|
-
close(): Promise<void>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Create a test harness for Turbo applications.
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* const harness = await createTestHarness({
|
|
60
|
-
* controllers: [UserController],
|
|
61
|
-
* services: [UserService],
|
|
62
|
-
* listen: true
|
|
63
|
-
* });
|
|
64
|
-
*
|
|
65
|
-
* const response = await harness.get('/users');
|
|
66
|
-
* expect(response.status).toBe(200);
|
|
67
|
-
*
|
|
68
|
-
* await harness.close();
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export async function createTestHarness(options: TestOptions = {}): Promise<TestHarness> {
|
|
72
|
-
const config: CarnoConfig = {
|
|
73
|
-
...options.config,
|
|
74
|
-
disableStartupLog: true
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const app = new Carno(config);
|
|
78
|
-
|
|
79
|
-
// Register controllers
|
|
80
|
-
if (options.controllers) {
|
|
81
|
-
app.controllers(options.controllers);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Register services
|
|
85
|
-
if (options.services) {
|
|
86
|
-
app.services(options.services);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const port = resolvePort(options);
|
|
90
|
-
let server: Server<any> | undefined;
|
|
91
|
-
|
|
92
|
-
if (shouldListen(options.listen)) {
|
|
93
|
-
app.listen(port);
|
|
94
|
-
server = (app as any).server;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const actualPort = server?.port ?? port;
|
|
98
|
-
const container = (app as any).container as Container;
|
|
99
|
-
|
|
100
|
-
// Pre-bind methods for performance
|
|
101
|
-
const baseUrl = `http://127.0.0.1:${actualPort}`;
|
|
102
|
-
|
|
103
|
-
const request = async (path: string, init?: RequestInit): Promise<Response> => {
|
|
104
|
-
if (!server) {
|
|
105
|
-
throw new Error('Server not running. Set listen: true in options.');
|
|
106
|
-
}
|
|
107
|
-
const url = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
108
|
-
return fetch(url, init);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
app,
|
|
113
|
-
container,
|
|
114
|
-
server,
|
|
115
|
-
port: actualPort,
|
|
116
|
-
|
|
117
|
-
resolve: <T>(token: Token<T>): T => container.get(token),
|
|
118
|
-
|
|
119
|
-
request,
|
|
120
|
-
|
|
121
|
-
get: (path, init) => request(path, { ...init, method: 'GET' }),
|
|
122
|
-
|
|
123
|
-
post: (path, body, init) => request(path, {
|
|
124
|
-
...init,
|
|
125
|
-
method: 'POST',
|
|
126
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
127
|
-
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
128
|
-
}),
|
|
129
|
-
|
|
130
|
-
put: (path, body, init) => request(path, {
|
|
131
|
-
...init,
|
|
132
|
-
method: 'PUT',
|
|
133
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
134
|
-
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
135
|
-
}),
|
|
136
|
-
|
|
137
|
-
delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
|
|
138
|
-
|
|
139
|
-
close: async () => {
|
|
140
|
-
app.stop();
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Run a test routine with automatic harness cleanup.
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* ```typescript
|
|
150
|
-
* await withTestApp(async (harness) => {
|
|
151
|
-
* const response = await harness.get('/health');
|
|
152
|
-
* expect(response.status).toBe(200);
|
|
153
|
-
* }, { controllers: [HealthController], listen: true });
|
|
154
|
-
* ```
|
|
155
|
-
*/
|
|
156
|
-
export async function withTestApp(
|
|
157
|
-
routine: (harness: TestHarness) => Promise<void>,
|
|
158
|
-
options: TestOptions = {}
|
|
159
|
-
): Promise<void> {
|
|
160
|
-
const harness = await createTestHarness(options);
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
await routine(harness);
|
|
164
|
-
} finally {
|
|
165
|
-
await harness.close();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function shouldListen(value: TestOptions['listen']): boolean {
|
|
170
|
-
return typeof value === 'number' || Boolean(value);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resolvePort(options: TestOptions): number {
|
|
174
|
-
if (typeof options.listen === 'number') return options.listen;
|
|
175
|
-
if (typeof options.port === 'number') return options.port;
|
|
176
|
-
return 0; // Random port
|
|
177
|
-
}
|
|
1
|
+
import type { Server } from 'bun';
|
|
2
|
+
import { Carno, type CarnoConfig } from '../Carno';
|
|
3
|
+
import { Container, type Token } from '../container/Container';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test configuration options.
|
|
7
|
+
*/
|
|
8
|
+
export interface TestOptions {
|
|
9
|
+
config?: CarnoConfig;
|
|
10
|
+
listen?: boolean | number;
|
|
11
|
+
port?: number;
|
|
12
|
+
controllers?: (new (...args: any[]) => any)[];
|
|
13
|
+
services?: (Token | any)[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Test harness - provides utilities for testing Carno applications.
|
|
18
|
+
*/
|
|
19
|
+
export interface TestHarness {
|
|
20
|
+
/** The Carno app instance */
|
|
21
|
+
app: Carno;
|
|
22
|
+
|
|
23
|
+
/** The internal DI container */
|
|
24
|
+
container: Container;
|
|
25
|
+
|
|
26
|
+
/** The HTTP server (if listening) */
|
|
27
|
+
server?: Server<any>;
|
|
28
|
+
|
|
29
|
+
/** The port the server is running on */
|
|
30
|
+
port?: number;
|
|
31
|
+
|
|
32
|
+
/** Resolve a service from the container */
|
|
33
|
+
resolve<T>(token: Token<T>): T;
|
|
34
|
+
|
|
35
|
+
/** Make an HTTP request to the app */
|
|
36
|
+
request(path: string, init?: RequestInit): Promise<Response>;
|
|
37
|
+
|
|
38
|
+
/** Make a GET request */
|
|
39
|
+
get(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
40
|
+
|
|
41
|
+
/** Make a POST request */
|
|
42
|
+
post(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
43
|
+
|
|
44
|
+
/** Make a PUT request */
|
|
45
|
+
put(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
|
|
46
|
+
|
|
47
|
+
/** Make a DELETE request */
|
|
48
|
+
delete(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
|
|
49
|
+
|
|
50
|
+
/** Close the test harness and cleanup */
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a test harness for Turbo applications.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const harness = await createTestHarness({
|
|
60
|
+
* controllers: [UserController],
|
|
61
|
+
* services: [UserService],
|
|
62
|
+
* listen: true
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* const response = await harness.get('/users');
|
|
66
|
+
* expect(response.status).toBe(200);
|
|
67
|
+
*
|
|
68
|
+
* await harness.close();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export async function createTestHarness(options: TestOptions = {}): Promise<TestHarness> {
|
|
72
|
+
const config: CarnoConfig = {
|
|
73
|
+
...options.config,
|
|
74
|
+
disableStartupLog: true
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const app = new Carno(config);
|
|
78
|
+
|
|
79
|
+
// Register controllers
|
|
80
|
+
if (options.controllers) {
|
|
81
|
+
app.controllers(options.controllers);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Register services
|
|
85
|
+
if (options.services) {
|
|
86
|
+
app.services(options.services);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const port = resolvePort(options);
|
|
90
|
+
let server: Server<any> | undefined;
|
|
91
|
+
|
|
92
|
+
if (shouldListen(options.listen)) {
|
|
93
|
+
app.listen(port);
|
|
94
|
+
server = (app as any).server;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const actualPort = server?.port ?? port;
|
|
98
|
+
const container = (app as any).container as Container;
|
|
99
|
+
|
|
100
|
+
// Pre-bind methods for performance
|
|
101
|
+
const baseUrl = `http://127.0.0.1:${actualPort}`;
|
|
102
|
+
|
|
103
|
+
const request = async (path: string, init?: RequestInit): Promise<Response> => {
|
|
104
|
+
if (!server) {
|
|
105
|
+
throw new Error('Server not running. Set listen: true in options.');
|
|
106
|
+
}
|
|
107
|
+
const url = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
108
|
+
return fetch(url, init);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
app,
|
|
113
|
+
container,
|
|
114
|
+
server,
|
|
115
|
+
port: actualPort,
|
|
116
|
+
|
|
117
|
+
resolve: <T>(token: Token<T>): T => container.get(token),
|
|
118
|
+
|
|
119
|
+
request,
|
|
120
|
+
|
|
121
|
+
get: (path, init) => request(path, { ...init, method: 'GET' }),
|
|
122
|
+
|
|
123
|
+
post: (path, body, init) => request(path, {
|
|
124
|
+
...init,
|
|
125
|
+
method: 'POST',
|
|
126
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
127
|
+
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
put: (path, body, init) => request(path, {
|
|
131
|
+
...init,
|
|
132
|
+
method: 'PUT',
|
|
133
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
134
|
+
headers: { 'Content-Type': 'application/json', ...init?.headers }
|
|
135
|
+
}),
|
|
136
|
+
|
|
137
|
+
delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
|
|
138
|
+
|
|
139
|
+
close: async () => {
|
|
140
|
+
app.stop();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a test routine with automatic harness cleanup.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* await withTestApp(async (harness) => {
|
|
151
|
+
* const response = await harness.get('/health');
|
|
152
|
+
* expect(response.status).toBe(200);
|
|
153
|
+
* }, { controllers: [HealthController], listen: true });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export async function withTestApp(
|
|
157
|
+
routine: (harness: TestHarness) => Promise<void>,
|
|
158
|
+
options: TestOptions = {}
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const harness = await createTestHarness(options);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await routine(harness);
|
|
164
|
+
} finally {
|
|
165
|
+
await harness.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function shouldListen(value: TestOptions['listen']): boolean {
|
|
170
|
+
return typeof value === 'number' || Boolean(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolvePort(options: TestOptions): number {
|
|
174
|
+
if (typeof options.listen === 'number') return options.listen;
|
|
175
|
+
if (typeof options.port === 'number') return options.port;
|
|
176
|
+
return 0; // Random port
|
|
177
|
+
}
|
package/src/utils/Metadata.ts
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility class for handling metadata operations.
|
|
3
|
-
* Wraps Reflect.getMetadata and Reflect.defineMetadata.
|
|
4
|
-
*/
|
|
5
|
-
export class Metadata {
|
|
6
|
-
static get<T = any>(key: string | symbol, target: any): T | undefined {
|
|
7
|
-
return Reflect.getMetadata(key, target);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
static set(key: string | symbol, value: any, target: any): void {
|
|
11
|
-
Reflect.defineMetadata(key, value, target);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
static has(key: string | symbol, target: any): boolean {
|
|
15
|
-
return Reflect.hasMetadata(key, target);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static delete(key: string | symbol, target: any): boolean {
|
|
19
|
-
return Reflect.deleteMetadata(key, target);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static keys(target: any): (string | symbol)[] {
|
|
23
|
-
return Reflect.getMetadataKeys(target);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
static getType(target: any, propertyKey: string | symbol): any {
|
|
27
|
-
return Reflect.getMetadata('design:type', target, propertyKey);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Type guard for checking if value is an object.
|
|
33
|
-
*/
|
|
34
|
-
export function isObject(value: unknown): value is Record<string, any> {
|
|
35
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Type guard for checking if value is a string.
|
|
40
|
-
*/
|
|
41
|
-
export function isString(value: unknown): value is string {
|
|
42
|
-
return typeof value === 'string';
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for handling metadata operations.
|
|
3
|
+
* Wraps Reflect.getMetadata and Reflect.defineMetadata.
|
|
4
|
+
*/
|
|
5
|
+
export class Metadata {
|
|
6
|
+
static get<T = any>(key: string | symbol, target: any): T | undefined {
|
|
7
|
+
return Reflect.getMetadata(key, target);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static set(key: string | symbol, value: any, target: any): void {
|
|
11
|
+
Reflect.defineMetadata(key, value, target);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static has(key: string | symbol, target: any): boolean {
|
|
15
|
+
return Reflect.hasMetadata(key, target);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static delete(key: string | symbol, target: any): boolean {
|
|
19
|
+
return Reflect.deleteMetadata(key, target);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static keys(target: any): (string | symbol)[] {
|
|
23
|
+
return Reflect.getMetadataKeys(target);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static getType(target: any, propertyKey: string | symbol): any {
|
|
27
|
+
return Reflect.getMetadata('design:type', target, propertyKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Type guard for checking if value is an object.
|
|
33
|
+
*/
|
|
34
|
+
export function isObject(value: unknown): value is Record<string, any> {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type guard for checking if value is a string.
|
|
40
|
+
*/
|
|
41
|
+
export function isString(value: unknown): value is string {
|
|
42
|
+
return typeof value === 'string';
|
|
43
|
+
}
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import type { ValidatorAdapter, ValidationResult, ValidationError } from './ValidatorAdapter';
|
|
2
|
-
import { VALIDATION_SCHEMA, getSchema } from './ValidatorAdapter';
|
|
3
|
-
import { ValidationException } from './ZodAdapter';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Valibot Adapter for Turbo validation.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* ```typescript
|
|
10
|
-
* import * as v from 'valibot';
|
|
11
|
-
*
|
|
12
|
-
* @Schema(v.object({
|
|
13
|
-
* name: v.pipe(v.string(), v.minLength(1)),
|
|
14
|
-
* email: v.pipe(v.string(), v.email())
|
|
15
|
-
* }))
|
|
16
|
-
* class CreateUserDto {
|
|
17
|
-
* name: string;
|
|
18
|
-
* email: string;
|
|
19
|
-
* }
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export class ValibotAdapter implements ValidatorAdapter {
|
|
23
|
-
readonly name = 'ValibotAdapter';
|
|
24
|
-
|
|
25
|
-
private schemaCache = new Map<any, any>();
|
|
26
|
-
private valibot: any = null;
|
|
27
|
-
|
|
28
|
-
constructor() {
|
|
29
|
-
// Lazy load valibot
|
|
30
|
-
try {
|
|
31
|
-
this.valibot = require('valibot');
|
|
32
|
-
} catch {
|
|
33
|
-
// Will be loaded on first use
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private ensureValibot(): any {
|
|
38
|
-
if (!this.valibot) {
|
|
39
|
-
this.valibot = require('valibot');
|
|
40
|
-
}
|
|
41
|
-
return this.valibot;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
hasValidation(target: any): boolean {
|
|
45
|
-
return getSchema(target) !== undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
validate<T>(target: any, value: unknown): ValidationResult<T> {
|
|
49
|
-
const schema = this.getOrCacheSchema(target);
|
|
50
|
-
|
|
51
|
-
if (!schema) {
|
|
52
|
-
return { success: true, data: value as T };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const v = this.ensureValibot();
|
|
56
|
-
const result = v.safeParse(schema, value);
|
|
57
|
-
|
|
58
|
-
if (result.success) {
|
|
59
|
-
return { success: true, data: result.output };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
success: false,
|
|
64
|
-
errors: this.formatErrors(result.issues)
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
validateOrThrow<T>(target: any, value: unknown): T {
|
|
69
|
-
const result = this.validate<T>(target, value);
|
|
70
|
-
|
|
71
|
-
if (result.success) {
|
|
72
|
-
return result.data!;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
throw new ValidationException(result.errors!);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private getOrCacheSchema(target: any): any {
|
|
79
|
-
let schema = this.schemaCache.get(target);
|
|
80
|
-
|
|
81
|
-
if (schema === undefined) {
|
|
82
|
-
schema = getSchema(target) ?? null;
|
|
83
|
-
this.schemaCache.set(target, schema);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return schema;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private formatErrors(issues: any[]): ValidationError[] {
|
|
90
|
-
return issues.map((issue: any) => ({
|
|
91
|
-
path: issue.path?.map((p: any) => p.key).join('.') || '',
|
|
92
|
-
message: issue.message
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
1
|
+
import type { ValidatorAdapter, ValidationResult, ValidationError } from './ValidatorAdapter';
|
|
2
|
+
import { VALIDATION_SCHEMA, getSchema } from './ValidatorAdapter';
|
|
3
|
+
import { ValidationException } from './ZodAdapter';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Valibot Adapter for Turbo validation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import * as v from 'valibot';
|
|
11
|
+
*
|
|
12
|
+
* @Schema(v.object({
|
|
13
|
+
* name: v.pipe(v.string(), v.minLength(1)),
|
|
14
|
+
* email: v.pipe(v.string(), v.email())
|
|
15
|
+
* }))
|
|
16
|
+
* class CreateUserDto {
|
|
17
|
+
* name: string;
|
|
18
|
+
* email: string;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class ValibotAdapter implements ValidatorAdapter {
|
|
23
|
+
readonly name = 'ValibotAdapter';
|
|
24
|
+
|
|
25
|
+
private schemaCache = new Map<any, any>();
|
|
26
|
+
private valibot: any = null;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
// Lazy load valibot
|
|
30
|
+
try {
|
|
31
|
+
this.valibot = require('valibot');
|
|
32
|
+
} catch {
|
|
33
|
+
// Will be loaded on first use
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private ensureValibot(): any {
|
|
38
|
+
if (!this.valibot) {
|
|
39
|
+
this.valibot = require('valibot');
|
|
40
|
+
}
|
|
41
|
+
return this.valibot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hasValidation(target: any): boolean {
|
|
45
|
+
return getSchema(target) !== undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
validate<T>(target: any, value: unknown): ValidationResult<T> {
|
|
49
|
+
const schema = this.getOrCacheSchema(target);
|
|
50
|
+
|
|
51
|
+
if (!schema) {
|
|
52
|
+
return { success: true, data: value as T };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const v = this.ensureValibot();
|
|
56
|
+
const result = v.safeParse(schema, value);
|
|
57
|
+
|
|
58
|
+
if (result.success) {
|
|
59
|
+
return { success: true, data: result.output };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
errors: this.formatErrors(result.issues)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
validateOrThrow<T>(target: any, value: unknown): T {
|
|
69
|
+
const result = this.validate<T>(target, value);
|
|
70
|
+
|
|
71
|
+
if (result.success) {
|
|
72
|
+
return result.data!;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new ValidationException(result.errors!);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private getOrCacheSchema(target: any): any {
|
|
79
|
+
let schema = this.schemaCache.get(target);
|
|
80
|
+
|
|
81
|
+
if (schema === undefined) {
|
|
82
|
+
schema = getSchema(target) ?? null;
|
|
83
|
+
this.schemaCache.set(target, schema);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return schema;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private formatErrors(issues: any[]): ValidationError[] {
|
|
90
|
+
return issues.map((issue: any) => ({
|
|
91
|
+
path: issue.path?.map((p: any) => p.key).join('.') || '',
|
|
92
|
+
message: issue.message
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
}
|