next-form-request 0.1.1 → 2.0.0
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/dist/{ZodAdapter-D7D3Sc-a.d.mts → ZodAdapter-Ni7VwvnR.d.mts} +1 -1
- package/dist/{ZodAdapter-D7D3Sc-a.d.ts → ZodAdapter-Ni7VwvnR.d.ts} +1 -1
- package/dist/adapters/validators/ZodAdapter.d.mts +1 -1
- package/dist/adapters/validators/ZodAdapter.d.ts +1 -1
- package/dist/index.d.mts +849 -15
- package/dist/index.d.ts +849 -15
- package/dist/index.js +987 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +958 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
3
5
|
// src/core/types.ts
|
|
4
6
|
function isAppRouterRequest(request) {
|
|
5
7
|
return "headers" in request && request.headers instanceof Headers;
|
|
@@ -64,6 +66,225 @@ var AuthorizationError = class _AuthorizationError extends Error {
|
|
|
64
66
|
}
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
// src/utils/rateLimit.ts
|
|
70
|
+
var MemoryRateLimitStore = class {
|
|
71
|
+
constructor() {
|
|
72
|
+
this.store = /* @__PURE__ */ new Map();
|
|
73
|
+
}
|
|
74
|
+
async get(key) {
|
|
75
|
+
const state = this.store.get(key);
|
|
76
|
+
if (!state) return null;
|
|
77
|
+
if (Date.now() > state.resetAt) {
|
|
78
|
+
this.store.delete(key);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
async set(key, state, _ttlMs) {
|
|
84
|
+
this.store.set(key, state);
|
|
85
|
+
if (this.store.size > 1e3) {
|
|
86
|
+
this.cleanup();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async increment(key, windowMs) {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const existing = this.store.get(key);
|
|
92
|
+
if (!existing || now > existing.resetAt) {
|
|
93
|
+
const state = {
|
|
94
|
+
count: 1,
|
|
95
|
+
resetAt: now + windowMs
|
|
96
|
+
};
|
|
97
|
+
this.store.set(key, state);
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
existing.count++;
|
|
101
|
+
return existing;
|
|
102
|
+
}
|
|
103
|
+
cleanup() {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
for (const [key, state] of this.store.entries()) {
|
|
106
|
+
if (now > state.resetAt) {
|
|
107
|
+
this.store.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var defaultStore = new MemoryRateLimitStore();
|
|
113
|
+
function setDefaultRateLimitStore(store) {
|
|
114
|
+
defaultStore = store;
|
|
115
|
+
}
|
|
116
|
+
function getClientIp(request) {
|
|
117
|
+
if ("headers" in request && request.headers instanceof Headers) {
|
|
118
|
+
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown";
|
|
119
|
+
}
|
|
120
|
+
const req = request;
|
|
121
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
122
|
+
if (typeof forwarded === "string") {
|
|
123
|
+
return forwarded.split(",")[0]?.trim() || "unknown";
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(forwarded)) {
|
|
126
|
+
return forwarded[0]?.split(",")[0]?.trim() || "unknown";
|
|
127
|
+
}
|
|
128
|
+
const realIp = req.headers["x-real-ip"];
|
|
129
|
+
if (typeof realIp === "string") {
|
|
130
|
+
return realIp;
|
|
131
|
+
}
|
|
132
|
+
const socket = req.socket;
|
|
133
|
+
return socket?.remoteAddress || "unknown";
|
|
134
|
+
}
|
|
135
|
+
async function checkRateLimit(request, config) {
|
|
136
|
+
const { maxAttempts, windowMs, key, store = defaultStore, skip } = config;
|
|
137
|
+
if (skip && await skip(request)) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: true,
|
|
140
|
+
remaining: maxAttempts,
|
|
141
|
+
limit: maxAttempts,
|
|
142
|
+
resetAt: Date.now() + windowMs,
|
|
143
|
+
retryAfter: 0
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const rateLimitKey = key ? await key(request) : getClientIp(request);
|
|
147
|
+
const fullKey = `ratelimit:${rateLimitKey}`;
|
|
148
|
+
const state = await store.increment(fullKey, windowMs);
|
|
149
|
+
const allowed = state.count <= maxAttempts;
|
|
150
|
+
const remaining = Math.max(0, maxAttempts - state.count);
|
|
151
|
+
const retryAfter = allowed ? 0 : Math.ceil((state.resetAt - Date.now()) / 1e3);
|
|
152
|
+
return {
|
|
153
|
+
allowed,
|
|
154
|
+
remaining,
|
|
155
|
+
limit: maxAttempts,
|
|
156
|
+
resetAt: state.resetAt,
|
|
157
|
+
retryAfter
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var RateLimitError = class extends Error {
|
|
161
|
+
constructor(result, message) {
|
|
162
|
+
super(message || `Rate limit exceeded. Retry after ${result.retryAfter} seconds.`);
|
|
163
|
+
this.name = "RateLimitError";
|
|
164
|
+
this.retryAfter = result.retryAfter;
|
|
165
|
+
this.remaining = result.remaining;
|
|
166
|
+
this.limit = result.limit;
|
|
167
|
+
this.resetAt = result.resetAt;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get headers to send with the rate limit response
|
|
171
|
+
*/
|
|
172
|
+
getHeaders() {
|
|
173
|
+
return {
|
|
174
|
+
"X-RateLimit-Limit": String(this.limit),
|
|
175
|
+
"X-RateLimit-Remaining": String(this.remaining),
|
|
176
|
+
"X-RateLimit-Reset": String(this.resetAt),
|
|
177
|
+
"Retry-After": String(this.retryAfter)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
function rateLimit(config) {
|
|
182
|
+
return {
|
|
183
|
+
key: getClientIp,
|
|
184
|
+
...config
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/utils/coerce.ts
|
|
189
|
+
var defaultOptions = {
|
|
190
|
+
booleans: true,
|
|
191
|
+
numbers: true,
|
|
192
|
+
dates: true,
|
|
193
|
+
nulls: true,
|
|
194
|
+
emptyStrings: false,
|
|
195
|
+
json: false
|
|
196
|
+
};
|
|
197
|
+
function isIsoDateString(value) {
|
|
198
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
199
|
+
return isoDateRegex.test(value);
|
|
200
|
+
}
|
|
201
|
+
function isNumericString(value) {
|
|
202
|
+
if (value === "" || value === null) return false;
|
|
203
|
+
if (/^0\d/.test(value)) return false;
|
|
204
|
+
if (value.length > 15) return false;
|
|
205
|
+
return !isNaN(Number(value)) && isFinite(Number(value));
|
|
206
|
+
}
|
|
207
|
+
function coerceValue(value, options) {
|
|
208
|
+
if (typeof value !== "string") {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
if (options.emptyStrings && value === "") {
|
|
212
|
+
return void 0;
|
|
213
|
+
}
|
|
214
|
+
if (options.nulls && value === "null") {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (options.booleans) {
|
|
218
|
+
const lower = value.toLowerCase();
|
|
219
|
+
if (lower === "true") return true;
|
|
220
|
+
if (lower === "false") return false;
|
|
221
|
+
}
|
|
222
|
+
if (options.dates && isIsoDateString(value)) {
|
|
223
|
+
const date = new Date(value);
|
|
224
|
+
if (!isNaN(date.getTime())) {
|
|
225
|
+
return date;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (options.numbers && isNumericString(value)) {
|
|
229
|
+
return Number(value);
|
|
230
|
+
}
|
|
231
|
+
if (options.json) {
|
|
232
|
+
const trimmed = value.trim();
|
|
233
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(value);
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
function coerceObject(data, options, path = "") {
|
|
243
|
+
const result = {};
|
|
244
|
+
for (const [key, value] of Object.entries(data)) {
|
|
245
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
246
|
+
if (options.fields && options.fields[fieldPath]) {
|
|
247
|
+
result[key] = options.fields[fieldPath](value);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(value)) {
|
|
251
|
+
result[key] = value.map((item, index) => {
|
|
252
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
253
|
+
return coerceObject(item, options, `${fieldPath}.${index}`);
|
|
254
|
+
}
|
|
255
|
+
return coerceValue(item, options);
|
|
256
|
+
});
|
|
257
|
+
} else if (value !== null && typeof value === "object") {
|
|
258
|
+
result[key] = coerceObject(value, options, fieldPath);
|
|
259
|
+
} else {
|
|
260
|
+
result[key] = coerceValue(value, options);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function coerceFormData(data, options = {}) {
|
|
266
|
+
const mergedOptions = { ...defaultOptions, ...options };
|
|
267
|
+
return coerceObject(data, mergedOptions);
|
|
268
|
+
}
|
|
269
|
+
function zodCoerce(options = {}) {
|
|
270
|
+
return (data) => {
|
|
271
|
+
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
272
|
+
return coerceFormData(data, options);
|
|
273
|
+
}
|
|
274
|
+
return data;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
var coercionPresets = {
|
|
278
|
+
/** Coerce all supported types */
|
|
279
|
+
all: { booleans: true, numbers: true, dates: true, nulls: true, emptyStrings: true, json: true },
|
|
280
|
+
/** Only coerce booleans and numbers (safest) */
|
|
281
|
+
safe: { booleans: true, numbers: true, dates: false, nulls: false, emptyStrings: false, json: false },
|
|
282
|
+
/** Coerce booleans, numbers, and dates */
|
|
283
|
+
standard: { booleans: true, numbers: true, dates: true, nulls: false, emptyStrings: false, json: false },
|
|
284
|
+
/** No coercion */
|
|
285
|
+
none: { booleans: false, numbers: false, dates: false, nulls: false, emptyStrings: false, json: false }
|
|
286
|
+
};
|
|
287
|
+
|
|
67
288
|
// src/core/FormRequest.ts
|
|
68
289
|
var FormRequest = class {
|
|
69
290
|
constructor() {
|
|
@@ -104,6 +325,42 @@ var FormRequest = class {
|
|
|
104
325
|
authorize() {
|
|
105
326
|
return true;
|
|
106
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Define rate limiting for this request.
|
|
330
|
+
* Override this method to add rate limiting.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* rateLimit() {
|
|
335
|
+
* return {
|
|
336
|
+
* maxAttempts: 5,
|
|
337
|
+
* windowMs: 60000, // 1 minute
|
|
338
|
+
* key: (req) => this.input('email') || 'anonymous',
|
|
339
|
+
* };
|
|
340
|
+
* }
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
rateLimit() {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Define coercion options for form data.
|
|
348
|
+
* Override this method to enable automatic type coercion.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* ```typescript
|
|
352
|
+
* coercion() {
|
|
353
|
+
* return {
|
|
354
|
+
* booleans: true, // "true" → true
|
|
355
|
+
* numbers: true, // "123" → 123
|
|
356
|
+
* dates: true, // "2024-01-01" → Date
|
|
357
|
+
* };
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
coercion() {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
107
364
|
/**
|
|
108
365
|
* Called before validation runs.
|
|
109
366
|
* Use this to normalize or transform input data.
|
|
@@ -245,15 +502,27 @@ var FormRequest = class {
|
|
|
245
502
|
* Run validation and return the validated data.
|
|
246
503
|
* Throws ValidationError if validation fails.
|
|
247
504
|
* Throws AuthorizationError if authorization fails.
|
|
505
|
+
* Throws RateLimitError if rate limit is exceeded.
|
|
248
506
|
*
|
|
249
507
|
* @returns The validated data with full type inference
|
|
250
508
|
*/
|
|
251
509
|
async validate() {
|
|
510
|
+
const rateLimitConfig = this.rateLimit();
|
|
511
|
+
if (rateLimitConfig) {
|
|
512
|
+
const result2 = await checkRateLimit(this.request, rateLimitConfig);
|
|
513
|
+
if (!result2.allowed) {
|
|
514
|
+
throw new RateLimitError(result2, rateLimitConfig.message);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
252
517
|
const isAuthorized = await this.authorize();
|
|
253
518
|
if (!isAuthorized) {
|
|
254
519
|
await this.onAuthorizationFailed();
|
|
255
520
|
throw new AuthorizationError();
|
|
256
521
|
}
|
|
522
|
+
const coercionOptions = this.coercion();
|
|
523
|
+
if (coercionOptions) {
|
|
524
|
+
this.body = coerceFormData(this.body, coercionOptions);
|
|
525
|
+
}
|
|
257
526
|
await this.beforeValidation();
|
|
258
527
|
const validator = this.rules();
|
|
259
528
|
const result = await validator.validate(this.getDataForValidation(), {
|
|
@@ -432,6 +701,59 @@ function createPagesRouterWrapper(options) {
|
|
|
432
701
|
};
|
|
433
702
|
}
|
|
434
703
|
|
|
704
|
+
// src/middleware/withSchema.ts
|
|
705
|
+
async function parseRequestBody(request) {
|
|
706
|
+
const contentType = request.headers.get("content-type") || "";
|
|
707
|
+
if (contentType.includes("application/json")) {
|
|
708
|
+
try {
|
|
709
|
+
return await request.json();
|
|
710
|
+
} catch {
|
|
711
|
+
return {};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
715
|
+
try {
|
|
716
|
+
const formData = await request.formData();
|
|
717
|
+
const data = {};
|
|
718
|
+
formData.forEach((value, key) => {
|
|
719
|
+
if (data[key]) {
|
|
720
|
+
if (Array.isArray(data[key])) {
|
|
721
|
+
data[key].push(value);
|
|
722
|
+
} else {
|
|
723
|
+
data[key] = [data[key], value];
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
data[key] = value;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
return data;
|
|
730
|
+
} catch {
|
|
731
|
+
return {};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return {};
|
|
735
|
+
}
|
|
736
|
+
function withSchema(adapter, handler) {
|
|
737
|
+
return async (request, context) => {
|
|
738
|
+
const body = await parseRequestBody(request);
|
|
739
|
+
const result = await adapter.validate(body);
|
|
740
|
+
if (!result.success) {
|
|
741
|
+
throw new ValidationError(result.errors || {});
|
|
742
|
+
}
|
|
743
|
+
return handler(result.data, request);
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function withApiSchema(adapter, handler) {
|
|
747
|
+
return async (req, res) => {
|
|
748
|
+
const body = req.body;
|
|
749
|
+
const result = await adapter.validate(body);
|
|
750
|
+
if (!result.success) {
|
|
751
|
+
throw new ValidationError(result.errors || {});
|
|
752
|
+
}
|
|
753
|
+
await handler(result.data, req, res);
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
435
757
|
// src/adapters/validators/ZodAdapter.ts
|
|
436
758
|
var ZodAdapter = class {
|
|
437
759
|
constructor(schema) {
|
|
@@ -483,15 +805,680 @@ var ZodAdapter = class {
|
|
|
483
805
|
}
|
|
484
806
|
};
|
|
485
807
|
|
|
808
|
+
// src/adapters/validators/YupAdapter.ts
|
|
809
|
+
var YupAdapter = class {
|
|
810
|
+
constructor(schema) {
|
|
811
|
+
this.schema = schema;
|
|
812
|
+
}
|
|
813
|
+
async validate(data, config) {
|
|
814
|
+
try {
|
|
815
|
+
const validated = await this.schema.validate(data, {
|
|
816
|
+
abortEarly: false,
|
|
817
|
+
stripUnknown: true
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
success: true,
|
|
821
|
+
data: validated
|
|
822
|
+
};
|
|
823
|
+
} catch (error) {
|
|
824
|
+
if (this.isYupValidationError(error)) {
|
|
825
|
+
const errors = this.formatYupErrors(error, config);
|
|
826
|
+
return {
|
|
827
|
+
success: false,
|
|
828
|
+
errors
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
validateSync(data, config) {
|
|
835
|
+
try {
|
|
836
|
+
const validated = this.schema.validateSync(data, {
|
|
837
|
+
abortEarly: false,
|
|
838
|
+
stripUnknown: true
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
success: true,
|
|
842
|
+
data: validated
|
|
843
|
+
};
|
|
844
|
+
} catch (error) {
|
|
845
|
+
if (this.isYupValidationError(error)) {
|
|
846
|
+
const errors = this.formatYupErrors(error, config);
|
|
847
|
+
return {
|
|
848
|
+
success: false,
|
|
849
|
+
errors
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
isYupValidationError(error) {
|
|
856
|
+
return error !== null && typeof error === "object" && "inner" in error && "name" in error && error.name === "ValidationError";
|
|
857
|
+
}
|
|
858
|
+
formatYupErrors(error, config) {
|
|
859
|
+
const errors = {};
|
|
860
|
+
const customMessages = config?.messages ?? {};
|
|
861
|
+
const customAttributes = config?.attributes ?? {};
|
|
862
|
+
const innerErrors = error.inner.length > 0 ? error.inner : [error];
|
|
863
|
+
for (const issue of innerErrors) {
|
|
864
|
+
const path = issue.path ?? "_root";
|
|
865
|
+
const attributeName = customAttributes[path] ?? path;
|
|
866
|
+
const messageKey = `${path}.${issue.type ?? "invalid"}`;
|
|
867
|
+
let message;
|
|
868
|
+
if (customMessages[messageKey]) {
|
|
869
|
+
message = customMessages[messageKey];
|
|
870
|
+
} else if (customMessages[path]) {
|
|
871
|
+
message = customMessages[path];
|
|
872
|
+
} else {
|
|
873
|
+
message = issue.message.replace(
|
|
874
|
+
new RegExp(`\\b${path}\\b`, "gi"),
|
|
875
|
+
attributeName
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (!errors[path]) {
|
|
879
|
+
errors[path] = [];
|
|
880
|
+
}
|
|
881
|
+
errors[path].push(message);
|
|
882
|
+
}
|
|
883
|
+
return errors;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
// src/adapters/validators/ValibotAdapter.ts
|
|
888
|
+
var ValibotAdapter = class {
|
|
889
|
+
constructor(schema, safeParse) {
|
|
890
|
+
this.schema = schema;
|
|
891
|
+
this.safeParseFunc = safeParse;
|
|
892
|
+
}
|
|
893
|
+
async validate(data, config) {
|
|
894
|
+
return this.validateSync(data, config);
|
|
895
|
+
}
|
|
896
|
+
validateSync(data, config) {
|
|
897
|
+
const result = this.safeParseFunc(this.schema, data);
|
|
898
|
+
if (result.success) {
|
|
899
|
+
return {
|
|
900
|
+
success: true,
|
|
901
|
+
data: result.output
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
const errors = this.formatValibotErrors(result.issues ?? [], config);
|
|
905
|
+
return {
|
|
906
|
+
success: false,
|
|
907
|
+
errors
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
formatValibotErrors(issues, config) {
|
|
911
|
+
const errors = {};
|
|
912
|
+
const customMessages = config?.messages ?? {};
|
|
913
|
+
const customAttributes = config?.attributes ?? {};
|
|
914
|
+
for (const issue of issues) {
|
|
915
|
+
const pathParts = issue.path?.map((p) => {
|
|
916
|
+
if (typeof p === "object" && p !== null && "key" in p) {
|
|
917
|
+
return String(p.key);
|
|
918
|
+
}
|
|
919
|
+
return String(p);
|
|
920
|
+
}) ?? [];
|
|
921
|
+
const path = pathParts.join(".") || "_root";
|
|
922
|
+
const attributeName = customAttributes[path] ?? path;
|
|
923
|
+
const messageKey = `${path}.${issue.type ?? "invalid"}`;
|
|
924
|
+
let message;
|
|
925
|
+
if (customMessages[messageKey]) {
|
|
926
|
+
message = customMessages[messageKey];
|
|
927
|
+
} else if (customMessages[path]) {
|
|
928
|
+
message = customMessages[path];
|
|
929
|
+
} else {
|
|
930
|
+
message = (issue.message ?? "Validation failed").replace(
|
|
931
|
+
new RegExp(`\\b${path}\\b`, "gi"),
|
|
932
|
+
attributeName
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (!errors[path]) {
|
|
936
|
+
errors[path] = [];
|
|
937
|
+
}
|
|
938
|
+
errors[path].push(message);
|
|
939
|
+
}
|
|
940
|
+
return errors;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// src/adapters/validators/ArkTypeAdapter.ts
|
|
945
|
+
function isArkTypeErrors(result) {
|
|
946
|
+
return result !== null && typeof result === "object" && "summary" in result && true;
|
|
947
|
+
}
|
|
948
|
+
var ArkTypeAdapter = class {
|
|
949
|
+
constructor(schema) {
|
|
950
|
+
this.schema = schema;
|
|
951
|
+
}
|
|
952
|
+
async validate(data, config) {
|
|
953
|
+
return this.validateSync(data, config);
|
|
954
|
+
}
|
|
955
|
+
validateSync(data, config) {
|
|
956
|
+
const result = this.schema(data);
|
|
957
|
+
if (!isArkTypeErrors(result)) {
|
|
958
|
+
return {
|
|
959
|
+
success: true,
|
|
960
|
+
data: result
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const errors = this.formatArkTypeErrors(result, config);
|
|
964
|
+
return {
|
|
965
|
+
success: false,
|
|
966
|
+
errors
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
formatArkTypeErrors(arkErrors, config) {
|
|
970
|
+
const errors = {};
|
|
971
|
+
const customMessages = config?.messages ?? {};
|
|
972
|
+
const customAttributes = config?.attributes ?? {};
|
|
973
|
+
const errorList = [];
|
|
974
|
+
if (arkErrors.errors && Array.isArray(arkErrors.errors)) {
|
|
975
|
+
errorList.push(...arkErrors.errors);
|
|
976
|
+
} else if (arkErrors[Symbol.iterator]) {
|
|
977
|
+
const iteratorFn = arkErrors[Symbol.iterator];
|
|
978
|
+
if (typeof iteratorFn === "function") {
|
|
979
|
+
const iterator = iteratorFn.call(arkErrors);
|
|
980
|
+
let result = iterator.next();
|
|
981
|
+
while (!result.done) {
|
|
982
|
+
errorList.push(result.value);
|
|
983
|
+
result = iterator.next();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
987
|
+
errorList.push({
|
|
988
|
+
path: [],
|
|
989
|
+
message: arkErrors.summary
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
for (const issue of errorList) {
|
|
993
|
+
const path = issue.path.join(".") || "_root";
|
|
994
|
+
const attributeName = customAttributes[path] ?? path;
|
|
995
|
+
const messageKey = `${path}.${issue.code ?? "invalid"}`;
|
|
996
|
+
let message;
|
|
997
|
+
if (customMessages[messageKey]) {
|
|
998
|
+
message = customMessages[messageKey];
|
|
999
|
+
} else if (customMessages[path]) {
|
|
1000
|
+
message = customMessages[path];
|
|
1001
|
+
} else {
|
|
1002
|
+
message = issue.message.replace(
|
|
1003
|
+
new RegExp(`\\b${path}\\b`, "gi"),
|
|
1004
|
+
attributeName
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
if (!errors[path]) {
|
|
1008
|
+
errors[path] = [];
|
|
1009
|
+
}
|
|
1010
|
+
errors[path].push(message);
|
|
1011
|
+
}
|
|
1012
|
+
return errors;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
function parseSize(size) {
|
|
1016
|
+
if (typeof size === "number") {
|
|
1017
|
+
return size;
|
|
1018
|
+
}
|
|
1019
|
+
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
|
|
1020
|
+
if (!match) {
|
|
1021
|
+
throw new Error(`Invalid size format: ${size}. Use formats like "5mb", "1024kb", or "1gb"`);
|
|
1022
|
+
}
|
|
1023
|
+
const value = parseFloat(match[1]);
|
|
1024
|
+
const unit = match[2] || "b";
|
|
1025
|
+
const multipliers = {
|
|
1026
|
+
b: 1,
|
|
1027
|
+
kb: 1024,
|
|
1028
|
+
mb: 1024 * 1024,
|
|
1029
|
+
gb: 1024 * 1024 * 1024
|
|
1030
|
+
};
|
|
1031
|
+
return Math.floor(value * multipliers[unit]);
|
|
1032
|
+
}
|
|
1033
|
+
function mimeTypeMatches(mimeType, pattern) {
|
|
1034
|
+
if (pattern === "*" || pattern === "*/*") {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
if (pattern.endsWith("/*")) {
|
|
1038
|
+
const category = pattern.slice(0, -2);
|
|
1039
|
+
return mimeType.startsWith(category + "/");
|
|
1040
|
+
}
|
|
1041
|
+
return mimeType === pattern;
|
|
1042
|
+
}
|
|
1043
|
+
function createValidatedFile(file) {
|
|
1044
|
+
const extension = file.name.includes(".") ? file.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
1045
|
+
return {
|
|
1046
|
+
name: file.name,
|
|
1047
|
+
size: file.size,
|
|
1048
|
+
type: file.type,
|
|
1049
|
+
file,
|
|
1050
|
+
extension,
|
|
1051
|
+
arrayBuffer: () => file.arrayBuffer(),
|
|
1052
|
+
text: () => file.text(),
|
|
1053
|
+
stream: () => file.stream()
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function formFile(options = {}) {
|
|
1057
|
+
const {
|
|
1058
|
+
maxSize,
|
|
1059
|
+
minSize,
|
|
1060
|
+
types,
|
|
1061
|
+
extensions,
|
|
1062
|
+
required = true
|
|
1063
|
+
} = options;
|
|
1064
|
+
const maxSizeBytes = maxSize ? parseSize(maxSize) : void 0;
|
|
1065
|
+
const minSizeBytes = minSize ? parseSize(minSize) : void 0;
|
|
1066
|
+
const fileSchema = zod.z.instanceof(File, { message: "Expected a file" }).refine(
|
|
1067
|
+
(file) => {
|
|
1068
|
+
if (!required && file.size === 0) return true;
|
|
1069
|
+
return file.size > 0;
|
|
1070
|
+
},
|
|
1071
|
+
{ message: "File is required" }
|
|
1072
|
+
).refine(
|
|
1073
|
+
(file) => {
|
|
1074
|
+
if (!maxSizeBytes) return true;
|
|
1075
|
+
return file.size <= maxSizeBytes;
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
message: maxSize ? `File size must not exceed ${typeof maxSize === "string" ? maxSize : `${maxSize} bytes`}` : "File is too large"
|
|
1079
|
+
}
|
|
1080
|
+
).refine(
|
|
1081
|
+
(file) => {
|
|
1082
|
+
if (!minSizeBytes) return true;
|
|
1083
|
+
return file.size >= minSizeBytes;
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
message: minSize ? `File size must be at least ${typeof minSize === "string" ? minSize : `${minSize} bytes`}` : "File is too small"
|
|
1087
|
+
}
|
|
1088
|
+
).refine(
|
|
1089
|
+
(file) => {
|
|
1090
|
+
if (!types || types.length === 0) return true;
|
|
1091
|
+
return types.some((pattern) => mimeTypeMatches(file.type, pattern));
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
message: types ? `File type must be one of: ${types.join(", ")}` : "Invalid file type"
|
|
1095
|
+
}
|
|
1096
|
+
).refine(
|
|
1097
|
+
(file) => {
|
|
1098
|
+
if (!extensions || extensions.length === 0) return true;
|
|
1099
|
+
const fileExt = file.name.includes(".") ? file.name.split(".").pop()?.toLowerCase() : "";
|
|
1100
|
+
return extensions.some((ext) => ext.toLowerCase() === fileExt);
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
message: extensions ? `File extension must be one of: ${extensions.join(", ")}` : "Invalid file extension"
|
|
1104
|
+
}
|
|
1105
|
+
).transform(createValidatedFile);
|
|
1106
|
+
return fileSchema;
|
|
1107
|
+
}
|
|
1108
|
+
function formFiles(options = {}) {
|
|
1109
|
+
const { minFiles, maxFiles, ...fileOptions } = options;
|
|
1110
|
+
const singleFileSchema = formFile({ ...fileOptions, required: true });
|
|
1111
|
+
let arraySchema = zod.z.array(singleFileSchema);
|
|
1112
|
+
if (minFiles !== void 0) {
|
|
1113
|
+
arraySchema = arraySchema.min(minFiles, {
|
|
1114
|
+
message: `At least ${minFiles} file(s) required`
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (maxFiles !== void 0) {
|
|
1118
|
+
arraySchema = arraySchema.max(maxFiles, {
|
|
1119
|
+
message: `Maximum ${maxFiles} file(s) allowed`
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
return arraySchema;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/utils/errorFormatting.ts
|
|
1126
|
+
function parsePath(path) {
|
|
1127
|
+
const segments = [];
|
|
1128
|
+
const parts = path.split(".");
|
|
1129
|
+
for (const part of parts) {
|
|
1130
|
+
const arrayMatch = part.match(/^(.+?)\[(\d+)\]$/);
|
|
1131
|
+
if (arrayMatch) {
|
|
1132
|
+
segments.push(arrayMatch[1]);
|
|
1133
|
+
segments.push(parseInt(arrayMatch[2], 10));
|
|
1134
|
+
} else if (/^\d+$/.test(part)) {
|
|
1135
|
+
segments.push(parseInt(part, 10));
|
|
1136
|
+
} else {
|
|
1137
|
+
segments.push(part);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return segments;
|
|
1141
|
+
}
|
|
1142
|
+
function setNestedValue(obj, path, value) {
|
|
1143
|
+
let current = obj;
|
|
1144
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1145
|
+
const key = path[i];
|
|
1146
|
+
const nextKey = path[i + 1];
|
|
1147
|
+
const keyStr = String(key);
|
|
1148
|
+
if (!(keyStr in current)) {
|
|
1149
|
+
current[keyStr] = typeof nextKey === "number" ? [] : {};
|
|
1150
|
+
}
|
|
1151
|
+
current = current[keyStr];
|
|
1152
|
+
}
|
|
1153
|
+
const lastKey = String(path[path.length - 1]);
|
|
1154
|
+
current[lastKey] = value;
|
|
1155
|
+
}
|
|
1156
|
+
function formatErrors(errors, options = {}) {
|
|
1157
|
+
const { maxErrorsPerField } = options;
|
|
1158
|
+
const flat = {};
|
|
1159
|
+
const nested = {};
|
|
1160
|
+
const all = [];
|
|
1161
|
+
for (const [field, messages] of Object.entries(errors)) {
|
|
1162
|
+
const limitedMessages = maxErrorsPerField ? messages.slice(0, maxErrorsPerField) : messages;
|
|
1163
|
+
flat[field] = limitedMessages;
|
|
1164
|
+
all.push(...limitedMessages);
|
|
1165
|
+
const path = parsePath(field);
|
|
1166
|
+
setNestedValue(nested, path, limitedMessages);
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
flat,
|
|
1170
|
+
nested,
|
|
1171
|
+
all,
|
|
1172
|
+
count: all.length,
|
|
1173
|
+
has(field) {
|
|
1174
|
+
return field in flat && flat[field].length > 0;
|
|
1175
|
+
},
|
|
1176
|
+
get(field) {
|
|
1177
|
+
return flat[field] ?? [];
|
|
1178
|
+
},
|
|
1179
|
+
first(field) {
|
|
1180
|
+
return flat[field]?.[0];
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function flattenErrors(nested, options = {}) {
|
|
1185
|
+
const { pathSeparator = ".", includeArrayIndices = true } = options;
|
|
1186
|
+
const result = {};
|
|
1187
|
+
function flatten(obj, prefix = "") {
|
|
1188
|
+
if (Array.isArray(obj)) {
|
|
1189
|
+
if (obj.every((item) => typeof item === "string")) {
|
|
1190
|
+
result[prefix] = obj;
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
obj.forEach((item, index) => {
|
|
1194
|
+
const key = includeArrayIndices ? `${prefix}${prefix ? pathSeparator : ""}${index}` : prefix;
|
|
1195
|
+
flatten(item, key);
|
|
1196
|
+
});
|
|
1197
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
1198
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1199
|
+
const newPrefix = prefix ? `${prefix}${pathSeparator}${key}` : key;
|
|
1200
|
+
flatten(value, newPrefix);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
flatten(nested);
|
|
1205
|
+
return result;
|
|
1206
|
+
}
|
|
1207
|
+
function summarizeErrors(errors) {
|
|
1208
|
+
const entries = Object.entries(errors);
|
|
1209
|
+
if (entries.length === 0) {
|
|
1210
|
+
return "No errors";
|
|
1211
|
+
}
|
|
1212
|
+
if (entries.length === 1) {
|
|
1213
|
+
const [field, messages] = entries[0];
|
|
1214
|
+
return `${field}: ${messages[0]}`;
|
|
1215
|
+
}
|
|
1216
|
+
const totalErrors = entries.reduce((sum, [, msgs]) => sum + msgs.length, 0);
|
|
1217
|
+
return `${totalErrors} validation error${totalErrors > 1 ? "s" : ""} in ${entries.length} field${entries.length > 1 ? "s" : ""}`;
|
|
1218
|
+
}
|
|
1219
|
+
function filterErrors(errors, fields) {
|
|
1220
|
+
const result = {};
|
|
1221
|
+
for (const field of fields) {
|
|
1222
|
+
if (errors[field]) {
|
|
1223
|
+
result[field] = errors[field];
|
|
1224
|
+
}
|
|
1225
|
+
for (const [key, messages] of Object.entries(errors)) {
|
|
1226
|
+
if (key.startsWith(`${field}.`)) {
|
|
1227
|
+
result[key] = messages;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
function mergeErrors(...errorSets) {
|
|
1234
|
+
const result = {};
|
|
1235
|
+
for (const errors of errorSets) {
|
|
1236
|
+
for (const [field, messages] of Object.entries(errors)) {
|
|
1237
|
+
if (!result[field]) {
|
|
1238
|
+
result[field] = [];
|
|
1239
|
+
}
|
|
1240
|
+
result[field].push(...messages);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return result;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// src/utils/testing.ts
|
|
1247
|
+
function createMockRequest(body, options = {}) {
|
|
1248
|
+
const {
|
|
1249
|
+
method = "POST",
|
|
1250
|
+
headers = {},
|
|
1251
|
+
query = {},
|
|
1252
|
+
baseUrl = "http://localhost:3000/api/test"
|
|
1253
|
+
} = options;
|
|
1254
|
+
const url = new URL(baseUrl);
|
|
1255
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1256
|
+
url.searchParams.set(key, value);
|
|
1257
|
+
}
|
|
1258
|
+
const requestHeaders = new Headers(headers);
|
|
1259
|
+
if (["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
|
|
1260
|
+
if (!requestHeaders.has("content-type")) {
|
|
1261
|
+
requestHeaders.set("content-type", "application/json");
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return new Request(url.toString(), {
|
|
1265
|
+
method,
|
|
1266
|
+
headers: requestHeaders,
|
|
1267
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async function testFormRequest(RequestClass, body, options = {}) {
|
|
1271
|
+
const request = createMockRequest(body, options);
|
|
1272
|
+
const FormRequestWithStatic = RequestClass;
|
|
1273
|
+
const instance = await FormRequestWithStatic.fromAppRouter(request, options.params);
|
|
1274
|
+
try {
|
|
1275
|
+
const data = await instance.validate();
|
|
1276
|
+
return {
|
|
1277
|
+
success: true,
|
|
1278
|
+
data,
|
|
1279
|
+
instance
|
|
1280
|
+
};
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
if (error instanceof ValidationError) {
|
|
1283
|
+
return {
|
|
1284
|
+
success: false,
|
|
1285
|
+
errors: error.errors,
|
|
1286
|
+
instance
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
if (error instanceof AuthorizationError) {
|
|
1290
|
+
return {
|
|
1291
|
+
success: false,
|
|
1292
|
+
unauthorized: true,
|
|
1293
|
+
instance
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function addMockMethod(RequestClass) {
|
|
1300
|
+
RequestClass.mock = async (body, options) => testFormRequest(RequestClass, body, options);
|
|
1301
|
+
}
|
|
1302
|
+
function expectValid(result) {
|
|
1303
|
+
if (!result.success) {
|
|
1304
|
+
const errorDetails = result.errors ? Object.entries(result.errors).map(([field, messages]) => `${field}: ${messages.join(", ")}`).join("\n") : result.unauthorized ? "Authorization denied" : "Unknown error";
|
|
1305
|
+
throw new Error(`Expected validation to pass, but it failed:
|
|
1306
|
+
${errorDetails}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function expectInvalid(result) {
|
|
1310
|
+
if (result.success) {
|
|
1311
|
+
throw new Error(`Expected validation to fail, but it passed with data: ${JSON.stringify(result.data)}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (!result.errors) {
|
|
1314
|
+
throw new Error("Expected validation errors, but got none");
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function expectFieldError(result, field, messagePattern) {
|
|
1318
|
+
expectInvalid(result);
|
|
1319
|
+
const fieldErrors = result.errors[field];
|
|
1320
|
+
if (!fieldErrors || fieldErrors.length === 0) {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`Expected errors for field "${field}", but found none. Errors: ${JSON.stringify(result.errors)}`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
if (messagePattern) {
|
|
1326
|
+
const hasMatch = fieldErrors.some(
|
|
1327
|
+
(msg) => typeof messagePattern === "string" ? msg.includes(messagePattern) : messagePattern.test(msg)
|
|
1328
|
+
);
|
|
1329
|
+
if (!hasMatch) {
|
|
1330
|
+
throw new Error(
|
|
1331
|
+
`Expected error for "${field}" to match "${messagePattern}", but got: ${fieldErrors.join(", ")}`
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/utils/compose.ts
|
|
1338
|
+
function createAuthenticatedRequest(authorizeFn) {
|
|
1339
|
+
class AuthenticatedFormRequest extends FormRequest {
|
|
1340
|
+
async authorize() {
|
|
1341
|
+
return authorizeFn(this);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return AuthenticatedFormRequest;
|
|
1345
|
+
}
|
|
1346
|
+
function composeAuthorization(...checks) {
|
|
1347
|
+
return async function() {
|
|
1348
|
+
for (const check of checks) {
|
|
1349
|
+
const result = await check(this);
|
|
1350
|
+
if (!result) return false;
|
|
1351
|
+
}
|
|
1352
|
+
return true;
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
var authHelpers = {
|
|
1356
|
+
/**
|
|
1357
|
+
* Check if request has a specific header
|
|
1358
|
+
*/
|
|
1359
|
+
hasHeader: (headerName) => (request) => {
|
|
1360
|
+
return !!request.header(headerName);
|
|
1361
|
+
},
|
|
1362
|
+
/**
|
|
1363
|
+
* Check if request has authorization header
|
|
1364
|
+
*/
|
|
1365
|
+
isAuthenticated: (request) => {
|
|
1366
|
+
return !!request.header("authorization");
|
|
1367
|
+
},
|
|
1368
|
+
/**
|
|
1369
|
+
* Check if request has a bearer token
|
|
1370
|
+
*/
|
|
1371
|
+
hasBearerToken: (request) => {
|
|
1372
|
+
const auth = request.header("authorization");
|
|
1373
|
+
return typeof auth === "string" && auth.startsWith("Bearer ");
|
|
1374
|
+
},
|
|
1375
|
+
/**
|
|
1376
|
+
* Extract bearer token from request
|
|
1377
|
+
*/
|
|
1378
|
+
getBearerToken: (request) => {
|
|
1379
|
+
const auth = request.header("authorization");
|
|
1380
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
1381
|
+
return auth.slice(7);
|
|
1382
|
+
}
|
|
1383
|
+
return null;
|
|
1384
|
+
},
|
|
1385
|
+
/**
|
|
1386
|
+
* Check if request has API key
|
|
1387
|
+
*/
|
|
1388
|
+
hasApiKey: (headerName = "x-api-key") => (request) => {
|
|
1389
|
+
return !!request.header(headerName);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
var hookHelpers = {
|
|
1393
|
+
/**
|
|
1394
|
+
* Compose multiple beforeValidation hooks
|
|
1395
|
+
*/
|
|
1396
|
+
beforeValidation: (...hooks) => async function() {
|
|
1397
|
+
for (const hook of hooks) {
|
|
1398
|
+
await hook.call(this);
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
/**
|
|
1402
|
+
* Compose multiple afterValidation hooks
|
|
1403
|
+
*/
|
|
1404
|
+
afterValidation: (...hooks) => async function(data) {
|
|
1405
|
+
for (const hook of hooks) {
|
|
1406
|
+
await hook.call(this, data);
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
/**
|
|
1410
|
+
* Common beforeValidation transformations
|
|
1411
|
+
*/
|
|
1412
|
+
transforms: {
|
|
1413
|
+
/** Trim all string values */
|
|
1414
|
+
trimStrings: function() {
|
|
1415
|
+
const body = this.all();
|
|
1416
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1417
|
+
if (typeof value === "string") {
|
|
1418
|
+
body[key] = value.trim();
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
/** Lowercase specific fields */
|
|
1423
|
+
lowercase: (...fields) => function() {
|
|
1424
|
+
for (const field of fields) {
|
|
1425
|
+
const value = this.input(field);
|
|
1426
|
+
if (typeof value === "string") {
|
|
1427
|
+
this.all()[field] = value.toLowerCase();
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
/** Uppercase specific fields */
|
|
1432
|
+
uppercase: (...fields) => function() {
|
|
1433
|
+
for (const field of fields) {
|
|
1434
|
+
const value = this.input(field);
|
|
1435
|
+
if (typeof value === "string") {
|
|
1436
|
+
this.all()[field] = value.toUpperCase();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
exports.ArkTypeAdapter = ArkTypeAdapter;
|
|
486
1444
|
exports.AuthorizationError = AuthorizationError;
|
|
487
1445
|
exports.FormRequest = FormRequest;
|
|
1446
|
+
exports.MemoryRateLimitStore = MemoryRateLimitStore;
|
|
1447
|
+
exports.RateLimitError = RateLimitError;
|
|
1448
|
+
exports.ValibotAdapter = ValibotAdapter;
|
|
488
1449
|
exports.ValidationError = ValidationError;
|
|
1450
|
+
exports.YupAdapter = YupAdapter;
|
|
489
1451
|
exports.ZodAdapter = ZodAdapter;
|
|
1452
|
+
exports.addMockMethod = addMockMethod;
|
|
1453
|
+
exports.authHelpers = authHelpers;
|
|
1454
|
+
exports.checkRateLimit = checkRateLimit;
|
|
1455
|
+
exports.coerceFormData = coerceFormData;
|
|
1456
|
+
exports.coercionPresets = coercionPresets;
|
|
1457
|
+
exports.composeAuthorization = composeAuthorization;
|
|
490
1458
|
exports.createAppRouterWrapper = createAppRouterWrapper;
|
|
1459
|
+
exports.createAuthenticatedRequest = createAuthenticatedRequest;
|
|
1460
|
+
exports.createMockRequest = createMockRequest;
|
|
491
1461
|
exports.createPagesRouterWrapper = createPagesRouterWrapper;
|
|
1462
|
+
exports.expectFieldError = expectFieldError;
|
|
1463
|
+
exports.expectInvalid = expectInvalid;
|
|
1464
|
+
exports.expectValid = expectValid;
|
|
1465
|
+
exports.filterErrors = filterErrors;
|
|
1466
|
+
exports.flattenErrors = flattenErrors;
|
|
1467
|
+
exports.formFile = formFile;
|
|
1468
|
+
exports.formFiles = formFiles;
|
|
1469
|
+
exports.formatErrors = formatErrors;
|
|
1470
|
+
exports.hookHelpers = hookHelpers;
|
|
492
1471
|
exports.isAppRouterRequest = isAppRouterRequest;
|
|
493
1472
|
exports.isPagesRouterRequest = isPagesRouterRequest;
|
|
1473
|
+
exports.mergeErrors = mergeErrors;
|
|
1474
|
+
exports.rateLimit = rateLimit;
|
|
1475
|
+
exports.setDefaultRateLimitStore = setDefaultRateLimitStore;
|
|
1476
|
+
exports.summarizeErrors = summarizeErrors;
|
|
1477
|
+
exports.testFormRequest = testFormRequest;
|
|
494
1478
|
exports.withApiRequest = withApiRequest;
|
|
1479
|
+
exports.withApiSchema = withApiSchema;
|
|
495
1480
|
exports.withRequest = withRequest;
|
|
1481
|
+
exports.withSchema = withSchema;
|
|
1482
|
+
exports.zodCoerce = zodCoerce;
|
|
496
1483
|
//# sourceMappingURL=index.js.map
|
|
497
1484
|
//# sourceMappingURL=index.js.map
|