@carno.js/core 1.1.0 → 1.1.2
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 -21
- package/README.md +188 -188
- package/dist/Carno.js +46 -26
- package/dist/Carno.mjs +46 -26
- package/dist/bun/index.js +4 -4
- package/dist/bun/index.js.map +29 -29
- package/package.json +2 -2
- package/src/Carno.ts +718 -673
- 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 +130 -130
- 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 +95 -95
- 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 +185 -185
- package/src/utils/Metadata.ts +43 -43
- package/src/utils/parseQuery.ts +161 -161
- package/src/validation/ValibotAdapter.ts +95 -95
- package/src/validation/ValidatorAdapter.ts +69 -69
- package/src/validation/ZodAdapter.ts +102 -102
- package/dist/Carno.d.js +0 -14
- package/dist/Carno.d.mjs +0 -1
- package/dist/DefaultRoutes.d.js +0 -13
- package/dist/DefaultRoutes.d.mjs +0 -0
- package/dist/cache/CacheDriver.d.js +0 -13
- package/dist/cache/CacheDriver.d.mjs +0 -0
- package/dist/cache/CacheService.d.js +0 -13
- package/dist/cache/CacheService.d.mjs +0 -0
- package/dist/cache/MemoryDriver.d.js +0 -13
- package/dist/cache/MemoryDriver.d.mjs +0 -0
- package/dist/cache/RedisDriver.d.js +0 -13
- package/dist/cache/RedisDriver.d.mjs +0 -0
- package/dist/compiler/JITCompiler.d.js +0 -13
- package/dist/compiler/JITCompiler.d.mjs +0 -0
- package/dist/container/Container.d.js +0 -13
- package/dist/container/Container.d.mjs +0 -0
- package/dist/context/Context.d.js +0 -13
- package/dist/context/Context.d.mjs +0 -0
- package/dist/cors/CorsHandler.d.js +0 -13
- package/dist/cors/CorsHandler.d.mjs +0 -0
- package/dist/decorators/Controller.d.js +0 -13
- package/dist/decorators/Controller.d.mjs +0 -0
- package/dist/decorators/Inject.d.js +0 -13
- package/dist/decorators/Inject.d.mjs +0 -0
- package/dist/decorators/Middleware.d.js +0 -13
- package/dist/decorators/Middleware.d.mjs +0 -0
- package/dist/decorators/Service.d.js +0 -13
- package/dist/decorators/Service.d.mjs +0 -0
- package/dist/decorators/methods.d.js +0 -13
- package/dist/decorators/methods.d.mjs +0 -0
- package/dist/decorators/params.d.js +0 -13
- package/dist/decorators/params.d.mjs +0 -0
- package/dist/events/Lifecycle.d.js +0 -13
- package/dist/events/Lifecycle.d.mjs +0 -0
- package/dist/exceptions/HttpException.d.js +0 -13
- package/dist/exceptions/HttpException.d.mjs +0 -0
- package/dist/index.d.js +0 -130
- package/dist/index.d.mjs +0 -78
- package/dist/metadata.d.js +0 -13
- package/dist/metadata.d.mjs +0 -0
- package/dist/middleware/CarnoMiddleware.d.js +0 -13
- package/dist/middleware/CarnoMiddleware.d.mjs +0 -0
- package/dist/router/RadixRouter.d.js +0 -13
- package/dist/router/RadixRouter.d.mjs +0 -0
- package/dist/testing/TestHarness.d.js +0 -13
- package/dist/testing/TestHarness.d.mjs +0 -0
- package/dist/utils/Metadata.d.js +0 -13
- package/dist/utils/Metadata.d.mjs +0 -0
- package/dist/utils/parseQuery.d.js +0 -13
- package/dist/utils/parseQuery.d.mjs +0 -0
- package/dist/validation/ValibotAdapter.d.js +0 -13
- package/dist/validation/ValibotAdapter.d.mjs +0 -0
- package/dist/validation/ValidatorAdapter.d.js +0 -13
- package/dist/validation/ValidatorAdapter.d.mjs +0 -0
- package/dist/validation/ZodAdapter.d.js +0 -13
- package/dist/validation/ZodAdapter.d.mjs +0 -0
- package/src/Carno.d.ts +0 -135
- package/src/DefaultRoutes.d.ts +0 -19
- package/src/cache/CacheDriver.d.ts +0 -43
- package/src/cache/CacheService.d.ts +0 -89
- package/src/cache/MemoryDriver.d.ts +0 -32
- package/src/cache/RedisDriver.d.ts +0 -34
- package/src/compiler/JITCompiler.d.ts +0 -36
- package/src/container/Container.d.ts +0 -38
- package/src/context/Context.d.ts +0 -36
- package/src/cors/CorsHandler.d.ts +0 -47
- package/src/decorators/Controller.d.ts +0 -13
- package/src/decorators/Inject.d.ts +0 -6
- package/src/decorators/Middleware.d.ts +0 -5
- package/src/decorators/Service.d.ts +0 -9
- package/src/decorators/methods.d.ts +0 -7
- package/src/decorators/params.d.ts +0 -13
- package/src/events/Lifecycle.d.ts +0 -54
- package/src/exceptions/HttpException.d.ts +0 -43
- package/src/index.d.ts +0 -42
- package/src/metadata.d.ts +0 -41
- package/src/middleware/CarnoMiddleware.d.ts +0 -12
- package/src/router/RadixRouter.d.ts +0 -19
- package/src/testing/TestHarness.d.ts +0 -71
- package/src/utils/Metadata.d.ts +0 -20
- package/src/utils/parseQuery.d.ts +0 -23
- package/src/validation/ValibotAdapter.d.ts +0 -30
- package/src/validation/ValidatorAdapter.d.ts +0 -54
- package/src/validation/ZodAdapter.d.ts +0 -35
package/src/utils/parseQuery.ts
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* High-performance query string parser.
|
|
3
|
-
*
|
|
4
|
-
* Based on Elysia's approach - uses manual string parsing with charCodeAt()
|
|
5
|
-
* instead of new URL() for significant performance gains.
|
|
6
|
-
*
|
|
7
|
-
* Benchmark: ~10x faster than new URL().searchParams
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// Bit flags for tracking decode requirements
|
|
11
|
-
const KEY_HAS_PLUS = 1;
|
|
12
|
-
const KEY_NEEDS_DECODE = 2;
|
|
13
|
-
const VALUE_HAS_PLUS = 4;
|
|
14
|
-
const VALUE_NEEDS_DECODE = 8;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parse query string from a full URL.
|
|
18
|
-
* Extracts the query portion and parses key-value pairs.
|
|
19
|
-
*
|
|
20
|
-
* @param url Full URL string (e.g., "http://localhost/path?foo=bar&baz=123")
|
|
21
|
-
* @returns Record<string, string> - parsed query parameters
|
|
22
|
-
*/
|
|
23
|
-
export function parseQueryFromURL(url: string): Record<string, string> {
|
|
24
|
-
// Find the start of query string
|
|
25
|
-
const queryStart = url.indexOf('?');
|
|
26
|
-
|
|
27
|
-
if (queryStart === -1) {
|
|
28
|
-
return Object.create(null);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Find the end of query string (before hash if present)
|
|
32
|
-
let queryEnd = url.indexOf('#', queryStart);
|
|
33
|
-
|
|
34
|
-
if (queryEnd === -1) {
|
|
35
|
-
queryEnd = url.length;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return parseQuery(url, queryStart + 1, queryEnd);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse query string directly.
|
|
43
|
-
*
|
|
44
|
-
* @param input Query string without leading '?' (e.g., "foo=bar&baz=123")
|
|
45
|
-
* @returns Record<string, string> - parsed query parameters
|
|
46
|
-
*/
|
|
47
|
-
export function parseQueryString(input: string): Record<string, string> {
|
|
48
|
-
return parseQuery(input, 0, input.length);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Internal parser - parses query string from startIndex to endIndex.
|
|
53
|
-
*/
|
|
54
|
-
function parseQuery(
|
|
55
|
-
input: string,
|
|
56
|
-
startIndex: number,
|
|
57
|
-
endIndex: number
|
|
58
|
-
): Record<string, string> {
|
|
59
|
-
const result: Record<string, string> = Object.create(null);
|
|
60
|
-
|
|
61
|
-
let flags = 0;
|
|
62
|
-
let startingIndex = startIndex - 1;
|
|
63
|
-
let equalityIndex = startingIndex;
|
|
64
|
-
|
|
65
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
66
|
-
switch (input.charCodeAt(i)) {
|
|
67
|
-
// '&' - separator between key-value pairs
|
|
68
|
-
case 38:
|
|
69
|
-
processKeyValuePair(i);
|
|
70
|
-
startingIndex = i;
|
|
71
|
-
equalityIndex = i;
|
|
72
|
-
flags = 0;
|
|
73
|
-
break;
|
|
74
|
-
|
|
75
|
-
// '=' - separator between key and value
|
|
76
|
-
case 61:
|
|
77
|
-
if (equalityIndex <= startingIndex) {
|
|
78
|
-
equalityIndex = i;
|
|
79
|
-
} else {
|
|
80
|
-
// Multiple '=' means value needs decode
|
|
81
|
-
flags |= VALUE_NEEDS_DECODE;
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
|
|
85
|
-
// '+' - space encoding
|
|
86
|
-
case 43:
|
|
87
|
-
if (equalityIndex > startingIndex) {
|
|
88
|
-
flags |= VALUE_HAS_PLUS;
|
|
89
|
-
} else {
|
|
90
|
-
flags |= KEY_HAS_PLUS;
|
|
91
|
-
}
|
|
92
|
-
break;
|
|
93
|
-
|
|
94
|
-
// '%' - URL encoding
|
|
95
|
-
case 37:
|
|
96
|
-
if (equalityIndex > startingIndex) {
|
|
97
|
-
flags |= VALUE_NEEDS_DECODE;
|
|
98
|
-
} else {
|
|
99
|
-
flags |= KEY_NEEDS_DECODE;
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Process the last pair
|
|
106
|
-
if (startingIndex < endIndex) {
|
|
107
|
-
processKeyValuePair(endIndex);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return result;
|
|
111
|
-
|
|
112
|
-
function processKeyValuePair(pairEndIndex: number) {
|
|
113
|
-
const hasBothKeyValuePair = equalityIndex > startingIndex;
|
|
114
|
-
const effectiveEqualityIndex = hasBothKeyValuePair
|
|
115
|
-
? equalityIndex
|
|
116
|
-
: pairEndIndex;
|
|
117
|
-
|
|
118
|
-
const keySlice = input.slice(startingIndex + 1, effectiveEqualityIndex);
|
|
119
|
-
|
|
120
|
-
// Skip empty keys
|
|
121
|
-
if (!hasBothKeyValuePair && keySlice.length === 0) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let finalKey = keySlice;
|
|
126
|
-
|
|
127
|
-
if (flags & KEY_HAS_PLUS) {
|
|
128
|
-
finalKey = finalKey.replace(/\+/g, ' ');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (flags & KEY_NEEDS_DECODE) {
|
|
132
|
-
try {
|
|
133
|
-
finalKey = decodeURIComponent(finalKey);
|
|
134
|
-
} catch {
|
|
135
|
-
// Keep original if decode fails
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let finalValue = '';
|
|
140
|
-
|
|
141
|
-
if (hasBothKeyValuePair) {
|
|
142
|
-
let valueSlice = input.slice(equalityIndex + 1, pairEndIndex);
|
|
143
|
-
|
|
144
|
-
if (flags & VALUE_HAS_PLUS) {
|
|
145
|
-
valueSlice = valueSlice.replace(/\+/g, ' ');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (flags & VALUE_NEEDS_DECODE) {
|
|
149
|
-
try {
|
|
150
|
-
finalValue = decodeURIComponent(valueSlice);
|
|
151
|
-
} catch {
|
|
152
|
-
finalValue = valueSlice;
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
finalValue = valueSlice;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
result[finalKey] = finalValue;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* High-performance query string parser.
|
|
3
|
+
*
|
|
4
|
+
* Based on Elysia's approach - uses manual string parsing with charCodeAt()
|
|
5
|
+
* instead of new URL() for significant performance gains.
|
|
6
|
+
*
|
|
7
|
+
* Benchmark: ~10x faster than new URL().searchParams
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Bit flags for tracking decode requirements
|
|
11
|
+
const KEY_HAS_PLUS = 1;
|
|
12
|
+
const KEY_NEEDS_DECODE = 2;
|
|
13
|
+
const VALUE_HAS_PLUS = 4;
|
|
14
|
+
const VALUE_NEEDS_DECODE = 8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse query string from a full URL.
|
|
18
|
+
* Extracts the query portion and parses key-value pairs.
|
|
19
|
+
*
|
|
20
|
+
* @param url Full URL string (e.g., "http://localhost/path?foo=bar&baz=123")
|
|
21
|
+
* @returns Record<string, string> - parsed query parameters
|
|
22
|
+
*/
|
|
23
|
+
export function parseQueryFromURL(url: string): Record<string, string> {
|
|
24
|
+
// Find the start of query string
|
|
25
|
+
const queryStart = url.indexOf('?');
|
|
26
|
+
|
|
27
|
+
if (queryStart === -1) {
|
|
28
|
+
return Object.create(null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find the end of query string (before hash if present)
|
|
32
|
+
let queryEnd = url.indexOf('#', queryStart);
|
|
33
|
+
|
|
34
|
+
if (queryEnd === -1) {
|
|
35
|
+
queryEnd = url.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parseQuery(url, queryStart + 1, queryEnd);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse query string directly.
|
|
43
|
+
*
|
|
44
|
+
* @param input Query string without leading '?' (e.g., "foo=bar&baz=123")
|
|
45
|
+
* @returns Record<string, string> - parsed query parameters
|
|
46
|
+
*/
|
|
47
|
+
export function parseQueryString(input: string): Record<string, string> {
|
|
48
|
+
return parseQuery(input, 0, input.length);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Internal parser - parses query string from startIndex to endIndex.
|
|
53
|
+
*/
|
|
54
|
+
function parseQuery(
|
|
55
|
+
input: string,
|
|
56
|
+
startIndex: number,
|
|
57
|
+
endIndex: number
|
|
58
|
+
): Record<string, string> {
|
|
59
|
+
const result: Record<string, string> = Object.create(null);
|
|
60
|
+
|
|
61
|
+
let flags = 0;
|
|
62
|
+
let startingIndex = startIndex - 1;
|
|
63
|
+
let equalityIndex = startingIndex;
|
|
64
|
+
|
|
65
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
66
|
+
switch (input.charCodeAt(i)) {
|
|
67
|
+
// '&' - separator between key-value pairs
|
|
68
|
+
case 38:
|
|
69
|
+
processKeyValuePair(i);
|
|
70
|
+
startingIndex = i;
|
|
71
|
+
equalityIndex = i;
|
|
72
|
+
flags = 0;
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
// '=' - separator between key and value
|
|
76
|
+
case 61:
|
|
77
|
+
if (equalityIndex <= startingIndex) {
|
|
78
|
+
equalityIndex = i;
|
|
79
|
+
} else {
|
|
80
|
+
// Multiple '=' means value needs decode
|
|
81
|
+
flags |= VALUE_NEEDS_DECODE;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
// '+' - space encoding
|
|
86
|
+
case 43:
|
|
87
|
+
if (equalityIndex > startingIndex) {
|
|
88
|
+
flags |= VALUE_HAS_PLUS;
|
|
89
|
+
} else {
|
|
90
|
+
flags |= KEY_HAS_PLUS;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
// '%' - URL encoding
|
|
95
|
+
case 37:
|
|
96
|
+
if (equalityIndex > startingIndex) {
|
|
97
|
+
flags |= VALUE_NEEDS_DECODE;
|
|
98
|
+
} else {
|
|
99
|
+
flags |= KEY_NEEDS_DECODE;
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Process the last pair
|
|
106
|
+
if (startingIndex < endIndex) {
|
|
107
|
+
processKeyValuePair(endIndex);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
|
|
112
|
+
function processKeyValuePair(pairEndIndex: number) {
|
|
113
|
+
const hasBothKeyValuePair = equalityIndex > startingIndex;
|
|
114
|
+
const effectiveEqualityIndex = hasBothKeyValuePair
|
|
115
|
+
? equalityIndex
|
|
116
|
+
: pairEndIndex;
|
|
117
|
+
|
|
118
|
+
const keySlice = input.slice(startingIndex + 1, effectiveEqualityIndex);
|
|
119
|
+
|
|
120
|
+
// Skip empty keys
|
|
121
|
+
if (!hasBothKeyValuePair && keySlice.length === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let finalKey = keySlice;
|
|
126
|
+
|
|
127
|
+
if (flags & KEY_HAS_PLUS) {
|
|
128
|
+
finalKey = finalKey.replace(/\+/g, ' ');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (flags & KEY_NEEDS_DECODE) {
|
|
132
|
+
try {
|
|
133
|
+
finalKey = decodeURIComponent(finalKey);
|
|
134
|
+
} catch {
|
|
135
|
+
// Keep original if decode fails
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let finalValue = '';
|
|
140
|
+
|
|
141
|
+
if (hasBothKeyValuePair) {
|
|
142
|
+
let valueSlice = input.slice(equalityIndex + 1, pairEndIndex);
|
|
143
|
+
|
|
144
|
+
if (flags & VALUE_HAS_PLUS) {
|
|
145
|
+
valueSlice = valueSlice.replace(/\+/g, ' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (flags & VALUE_NEEDS_DECODE) {
|
|
149
|
+
try {
|
|
150
|
+
finalValue = decodeURIComponent(valueSlice);
|
|
151
|
+
} catch {
|
|
152
|
+
finalValue = valueSlice;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
finalValue = valueSlice;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
result[finalKey] = finalValue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation result type.
|
|
3
|
-
*/
|
|
4
|
-
export interface ValidationResult<T = any> {
|
|
5
|
-
success: boolean;
|
|
6
|
-
data?: T;
|
|
7
|
-
errors?: ValidationError[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ValidationError {
|
|
11
|
-
path: string;
|
|
12
|
-
message: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Base interface for validation adapters.
|
|
17
|
-
* Adapters provide validation capabilities for different libraries.
|
|
18
|
-
*/
|
|
19
|
-
export interface ValidatorAdapter {
|
|
20
|
-
/**
|
|
21
|
-
* Validator name for debugging.
|
|
22
|
-
*/
|
|
23
|
-
readonly name: string;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if a target has validation schema.
|
|
27
|
-
*/
|
|
28
|
-
hasValidation(target: any): boolean;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Validate and transform a value.
|
|
32
|
-
* Returns result object instead of throwing for better performance.
|
|
33
|
-
*/
|
|
34
|
-
validate<T>(target: any, value: unknown): ValidationResult<T>;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Validate and transform, throwing on error.
|
|
38
|
-
* Used when you want to short-circuit on failure.
|
|
39
|
-
*/
|
|
40
|
-
validateOrThrow<T>(target: any, value: unknown): T;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Validation configuration for Turbo.
|
|
45
|
-
*/
|
|
46
|
-
export interface ValidationConfig {
|
|
47
|
-
adapter: ValidatorAdapter;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Symbol for storing validation schema on DTOs.
|
|
52
|
-
*/
|
|
53
|
-
export const VALIDATION_SCHEMA = Symbol('turbo:validation');
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Decorator to attach a validation schema to a DTO class.
|
|
57
|
-
*/
|
|
58
|
-
export function Schema(schema: any): ClassDecorator {
|
|
59
|
-
return (target) => {
|
|
60
|
-
Reflect.defineMetadata(VALIDATION_SCHEMA, schema, target);
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Get the validation schema from a DTO class.
|
|
66
|
-
*/
|
|
67
|
-
export function getSchema(target: any): any | undefined {
|
|
68
|
-
return Reflect.getMetadata(VALIDATION_SCHEMA, target);
|
|
69
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Validation result type.
|
|
3
|
+
*/
|
|
4
|
+
export interface ValidationResult<T = any> {
|
|
5
|
+
success: boolean;
|
|
6
|
+
data?: T;
|
|
7
|
+
errors?: ValidationError[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ValidationError {
|
|
11
|
+
path: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Base interface for validation adapters.
|
|
17
|
+
* Adapters provide validation capabilities for different libraries.
|
|
18
|
+
*/
|
|
19
|
+
export interface ValidatorAdapter {
|
|
20
|
+
/**
|
|
21
|
+
* Validator name for debugging.
|
|
22
|
+
*/
|
|
23
|
+
readonly name: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a target has validation schema.
|
|
27
|
+
*/
|
|
28
|
+
hasValidation(target: any): boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate and transform a value.
|
|
32
|
+
* Returns result object instead of throwing for better performance.
|
|
33
|
+
*/
|
|
34
|
+
validate<T>(target: any, value: unknown): ValidationResult<T>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate and transform, throwing on error.
|
|
38
|
+
* Used when you want to short-circuit on failure.
|
|
39
|
+
*/
|
|
40
|
+
validateOrThrow<T>(target: any, value: unknown): T;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validation configuration for Turbo.
|
|
45
|
+
*/
|
|
46
|
+
export interface ValidationConfig {
|
|
47
|
+
adapter: ValidatorAdapter;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Symbol for storing validation schema on DTOs.
|
|
52
|
+
*/
|
|
53
|
+
export const VALIDATION_SCHEMA = Symbol('turbo:validation');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decorator to attach a validation schema to a DTO class.
|
|
57
|
+
*/
|
|
58
|
+
export function Schema(schema: any): ClassDecorator {
|
|
59
|
+
return (target) => {
|
|
60
|
+
Reflect.defineMetadata(VALIDATION_SCHEMA, schema, target);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the validation schema from a DTO class.
|
|
66
|
+
*/
|
|
67
|
+
export function getSchema(target: any): any | undefined {
|
|
68
|
+
return Reflect.getMetadata(VALIDATION_SCHEMA, target);
|
|
69
|
+
}
|