@spfn/core 0.2.0-beta.5 → 0.2.0-beta.50
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 +1 -1
- package/README.md +181 -1281
- package/dist/{boss-BO8ty33K.d.ts → boss-Cxqc-Oiw.d.ts} +37 -7
- package/dist/cache/index.js +32 -29
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +179 -5
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +168 -6
- package/dist/config/index.js +29 -5
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +218 -4
- package/dist/db/index.js +351 -57
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +2 -1
- package/dist/env/index.js +2 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +17 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +188 -20
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +213 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +23 -8
- package/dist/job/index.js +154 -44
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +23 -1
- package/dist/middleware/index.js +58 -5
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +77 -31
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +44 -23
- package/dist/nextjs/server.js +83 -65
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +158 -4
- package/dist/route/index.js +238 -22
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +308 -17
- package/dist/server/index.js +1128 -261
- package/dist/server/index.js.map +1 -1
- package/dist/{router-Di7ENoah.d.ts → token-manager-CyG7la3p.d.ts} +116 -1
- package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
- package/dist/types-C1jMLGwK.d.ts +257 -0
- package/dist/types-Cfj--lfr.d.ts +151 -0
- package/docs/file-upload.md +717 -0
- package/package.json +18 -5
- package/dist/types-B-e_f2dQ.d.ts +0 -121
package/dist/route/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
2
|
-
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
+
import { TSchema, Static, Kind } from '@sinclair/typebox';
|
|
3
3
|
import { Context, MiddlewareHandler, Hono } from 'hono';
|
|
4
4
|
import { ContentfulStatusCode, RedirectStatusCode } from 'hono/utils/http-status';
|
|
5
5
|
import { HttpMethod } from './types.js';
|
|
@@ -22,6 +22,8 @@ type RouteInput = {
|
|
|
22
22
|
query?: TSchema;
|
|
23
23
|
/** Request body (JSON) */
|
|
24
24
|
body?: TSchema;
|
|
25
|
+
/** Form data (multipart/form-data) for file uploads */
|
|
26
|
+
formData?: TSchema;
|
|
25
27
|
/** HTTP headers */
|
|
26
28
|
headers?: TSchema;
|
|
27
29
|
/** Cookies */
|
|
@@ -61,6 +63,7 @@ type MergedInput<TInput extends RouteInput, TInterceptor extends RouteInput> = {
|
|
|
61
63
|
params: (TInput['params'] extends TSchema ? Static<TInput['params']> : {}) & (TInterceptor['params'] extends TSchema ? Static<TInterceptor['params']> : {});
|
|
62
64
|
query: (TInput['query'] extends TSchema ? Static<TInput['query']> : {}) & (TInterceptor['query'] extends TSchema ? Static<TInterceptor['query']> : {});
|
|
63
65
|
body: (TInput['body'] extends TSchema ? Static<TInput['body']> : {}) & (TInterceptor['body'] extends TSchema ? Static<TInterceptor['body']> : {});
|
|
66
|
+
formData: (TInput['formData'] extends TSchema ? Static<TInput['formData']> : {}) & (TInterceptor['formData'] extends TSchema ? Static<TInterceptor['formData']> : {});
|
|
64
67
|
headers: (TInput['headers'] extends TSchema ? Static<TInput['headers']> : {}) & (TInterceptor['headers'] extends TSchema ? Static<TInterceptor['headers']> : {});
|
|
65
68
|
cookies: (TInput['cookies'] extends TSchema ? Static<TInput['cookies']> : {}) & (TInterceptor['cookies'] extends TSchema ? Static<TInterceptor['cookies']> : {});
|
|
66
69
|
};
|
|
@@ -233,6 +236,7 @@ type NamedMiddleware<TName extends string = string> = {
|
|
|
233
236
|
name: TName;
|
|
234
237
|
handler: MiddlewareHandler;
|
|
235
238
|
_name: TName;
|
|
239
|
+
skips?: string[];
|
|
236
240
|
};
|
|
237
241
|
/**
|
|
238
242
|
* Named middleware factory with type inference
|
|
@@ -307,8 +311,27 @@ type NamedMiddlewareFactory<TName extends string = string, TArgs extends any[] =
|
|
|
307
311
|
* .handler(async (c) => { ... });
|
|
308
312
|
* ```
|
|
309
313
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Options for defineMiddleware
|
|
316
|
+
*/
|
|
317
|
+
interface DefineMiddlewareOptions {
|
|
318
|
+
/**
|
|
319
|
+
* Server-level middleware names to auto-skip when this middleware is used at route level.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* // optionalAuth auto-skips the global 'auth' middleware
|
|
324
|
+
* export const optionalAuth = defineMiddleware('optionalAuth', handler, {
|
|
325
|
+
* skips: ['auth']
|
|
326
|
+
* });
|
|
327
|
+
*
|
|
328
|
+
* // Usage: .use([optionalAuth]) — no need for .skip(['auth'])
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
skips?: string[];
|
|
332
|
+
}
|
|
333
|
+
declare function defineMiddleware<TName extends string>(name: TName, handler: MiddlewareHandler, options?: DefineMiddlewareOptions): NamedMiddleware<TName>;
|
|
334
|
+
declare function defineMiddleware<TName extends string, TArgs extends any[]>(name: TName, factory: (...args: TArgs) => MiddlewareHandler, options?: DefineMiddlewareOptions): NamedMiddlewareFactory<TName, TArgs>;
|
|
312
335
|
/**
|
|
313
336
|
* Define a middleware factory explicitly
|
|
314
337
|
*
|
|
@@ -757,4 +780,135 @@ declare const Nullable: <T extends TSchema>(schema: T) => _sinclair_typebox.TUni
|
|
|
757
780
|
*/
|
|
758
781
|
declare const OptionalNullable: <T extends TSchema>(schema: T) => _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[T, _sinclair_typebox.TNull]>>;
|
|
759
782
|
|
|
760
|
-
|
|
783
|
+
/**
|
|
784
|
+
* File Schema Helpers for TypeBox
|
|
785
|
+
*
|
|
786
|
+
* Provides TypeBox schema definitions for file upload handling
|
|
787
|
+
* with optional validation constraints.
|
|
788
|
+
*/
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* File validation options
|
|
792
|
+
*/
|
|
793
|
+
interface FileSchemaOptions {
|
|
794
|
+
/**
|
|
795
|
+
* Maximum file size in bytes
|
|
796
|
+
*
|
|
797
|
+
* @example 5 * 1024 * 1024 // 5MB
|
|
798
|
+
*/
|
|
799
|
+
maxSize?: number;
|
|
800
|
+
/**
|
|
801
|
+
* Allowed MIME types
|
|
802
|
+
*
|
|
803
|
+
* @example ['image/jpeg', 'image/png', 'image/webp']
|
|
804
|
+
*/
|
|
805
|
+
allowedTypes?: string[];
|
|
806
|
+
/**
|
|
807
|
+
* Minimum file size in bytes (optional)
|
|
808
|
+
*
|
|
809
|
+
* @example 1024 // 1KB minimum
|
|
810
|
+
*/
|
|
811
|
+
minSize?: number;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* File array validation options
|
|
815
|
+
*/
|
|
816
|
+
interface FileArraySchemaOptions extends FileSchemaOptions {
|
|
817
|
+
/**
|
|
818
|
+
* Maximum number of files
|
|
819
|
+
*
|
|
820
|
+
* @example 5
|
|
821
|
+
*/
|
|
822
|
+
maxFiles?: number;
|
|
823
|
+
/**
|
|
824
|
+
* Minimum number of files (optional)
|
|
825
|
+
*
|
|
826
|
+
* @example 1
|
|
827
|
+
*/
|
|
828
|
+
minFiles?: number;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Internal schema type with file validation metadata
|
|
832
|
+
*/
|
|
833
|
+
interface FileSchemaType extends TSchema {
|
|
834
|
+
[Kind]: 'File';
|
|
835
|
+
fileOptions?: FileSchemaOptions;
|
|
836
|
+
}
|
|
837
|
+
interface FileArraySchemaType extends TSchema {
|
|
838
|
+
[Kind]: 'FileArray';
|
|
839
|
+
fileOptions?: FileArraySchemaOptions;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Create a File schema with optional validation
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* ```ts
|
|
846
|
+
* // Basic usage (no validation)
|
|
847
|
+
* formData: Type.Object({
|
|
848
|
+
* file: FileSchema()
|
|
849
|
+
* })
|
|
850
|
+
*
|
|
851
|
+
* // With validation
|
|
852
|
+
* formData: Type.Object({
|
|
853
|
+
* avatar: FileSchema({
|
|
854
|
+
* maxSize: 5 * 1024 * 1024, // 5MB
|
|
855
|
+
* allowedTypes: ['image/jpeg', 'image/png', 'image/webp']
|
|
856
|
+
* })
|
|
857
|
+
* })
|
|
858
|
+
* ```
|
|
859
|
+
*/
|
|
860
|
+
declare function FileSchema(options?: FileSchemaOptions): FileSchemaType;
|
|
861
|
+
/**
|
|
862
|
+
* Create a File array schema with optional validation
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* ```ts
|
|
866
|
+
* // Basic usage (no validation)
|
|
867
|
+
* formData: Type.Object({
|
|
868
|
+
* files: FileArraySchema()
|
|
869
|
+
* })
|
|
870
|
+
*
|
|
871
|
+
* // With validation
|
|
872
|
+
* formData: Type.Object({
|
|
873
|
+
* documents: FileArraySchema({
|
|
874
|
+
* maxSize: 10 * 1024 * 1024, // 10MB per file
|
|
875
|
+
* maxFiles: 5,
|
|
876
|
+
* allowedTypes: ['application/pdf', 'application/msword']
|
|
877
|
+
* })
|
|
878
|
+
* })
|
|
879
|
+
* ```
|
|
880
|
+
*/
|
|
881
|
+
declare function FileArraySchema(options?: FileArraySchemaOptions): FileArraySchemaType;
|
|
882
|
+
/**
|
|
883
|
+
* Create an optional File schema with validation
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
886
|
+
* ```ts
|
|
887
|
+
* formData: Type.Object({
|
|
888
|
+
* name: Type.String(),
|
|
889
|
+
* avatar: OptionalFileSchema({
|
|
890
|
+
* maxSize: 2 * 1024 * 1024,
|
|
891
|
+
* allowedTypes: ['image/jpeg', 'image/png']
|
|
892
|
+
* })
|
|
893
|
+
* })
|
|
894
|
+
* ```
|
|
895
|
+
*/
|
|
896
|
+
declare function OptionalFileSchema(options?: FileSchemaOptions): TSchema;
|
|
897
|
+
/**
|
|
898
|
+
* Check if a schema is a File schema
|
|
899
|
+
*/
|
|
900
|
+
declare function isFileSchema(schema: TSchema): schema is FileSchemaType;
|
|
901
|
+
/**
|
|
902
|
+
* Check if a schema is a FileArray schema
|
|
903
|
+
*/
|
|
904
|
+
declare function isFileArraySchema(schema: TSchema): schema is FileArraySchemaType;
|
|
905
|
+
/**
|
|
906
|
+
* Get file options from schema
|
|
907
|
+
*/
|
|
908
|
+
declare function getFileOptions(schema: TSchema): FileSchemaOptions | FileArraySchemaOptions | undefined;
|
|
909
|
+
/**
|
|
910
|
+
* Format file size for error messages
|
|
911
|
+
*/
|
|
912
|
+
declare function formatFileSize(bytes: number): string;
|
|
913
|
+
|
|
914
|
+
export { type ExtractMiddlewareNames, FileArraySchema, type FileArraySchemaOptions, type FileArraySchemaType, FileSchema, type FileSchemaOptions, type FileSchemaType, HttpMethod, type MergedInput, type NamedMiddleware, type NamedMiddlewareFactory, Nullable, OptionalFileSchema, OptionalNullable, type PaginatedResult, type RegisteredRoute, type RouteBuilderContext, type RouteDef, type RouteHandlerFn, type RouteInput, type Router, defineMiddleware, defineMiddlewareFactory, defineRouter, formatFileSize, getFileOptions, isFileArraySchema, isFileSchema, isHttpMethod, registerRoutes, route };
|
package/dist/route/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from '@spfn/core/logger';
|
|
2
|
-
import { FormatRegistry, Type } from '@sinclair/typebox';
|
|
2
|
+
import { FormatRegistry, Type, Kind } from '@sinclair/typebox';
|
|
3
3
|
import { Value } from '@sinclair/typebox/value';
|
|
4
4
|
import { ValidationError } from '@spfn/core/errors';
|
|
5
5
|
|
|
@@ -260,26 +260,108 @@ function createRouterInstance(routes, packageRouters = [], globalMiddlewares = [
|
|
|
260
260
|
function defineRouter(routes) {
|
|
261
261
|
return createRouterInstance(routes);
|
|
262
262
|
}
|
|
263
|
+
function FileSchema(options) {
|
|
264
|
+
return Type.Unsafe({
|
|
265
|
+
[Kind]: "File",
|
|
266
|
+
type: "object",
|
|
267
|
+
fileOptions: options
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function FileArraySchema(options) {
|
|
271
|
+
return Type.Unsafe({
|
|
272
|
+
[Kind]: "FileArray",
|
|
273
|
+
type: "array",
|
|
274
|
+
items: { [Kind]: "File", type: "object" },
|
|
275
|
+
fileOptions: options
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function OptionalFileSchema(options) {
|
|
279
|
+
return Type.Optional(FileSchema(options));
|
|
280
|
+
}
|
|
281
|
+
function isFileSchema(schema) {
|
|
282
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
283
|
+
return kind === "File";
|
|
284
|
+
}
|
|
285
|
+
function isFileArraySchema(schema) {
|
|
286
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
287
|
+
return kind === "FileArray";
|
|
288
|
+
}
|
|
289
|
+
function getFileOptions(schema) {
|
|
290
|
+
return schema.fileOptions;
|
|
291
|
+
}
|
|
292
|
+
function formatFileSize(bytes) {
|
|
293
|
+
if (bytes >= 1024 * 1024 * 1024) {
|
|
294
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
295
|
+
}
|
|
296
|
+
if (bytes >= 1024 * 1024) {
|
|
297
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
298
|
+
}
|
|
299
|
+
if (bytes >= 1024) {
|
|
300
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
301
|
+
}
|
|
302
|
+
return `${bytes}B`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/route/validation.ts
|
|
263
306
|
FormatRegistry.Set(
|
|
264
307
|
"email",
|
|
265
|
-
(value) =>
|
|
308
|
+
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
266
309
|
);
|
|
267
310
|
FormatRegistry.Set(
|
|
268
311
|
"uri",
|
|
269
|
-
(value) =>
|
|
312
|
+
(value) => /^https?:\/\/.+/.test(value)
|
|
270
313
|
);
|
|
271
314
|
FormatRegistry.Set(
|
|
272
315
|
"uuid",
|
|
273
|
-
(value) =>
|
|
316
|
+
(value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
274
317
|
);
|
|
275
318
|
FormatRegistry.Set(
|
|
276
319
|
"date",
|
|
277
|
-
(value) =>
|
|
320
|
+
(value) => /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
278
321
|
);
|
|
279
322
|
FormatRegistry.Set(
|
|
280
323
|
"date-time",
|
|
281
|
-
(value) =>
|
|
324
|
+
(value) => !isNaN(Date.parse(value))
|
|
282
325
|
);
|
|
326
|
+
function isFile(value) {
|
|
327
|
+
return value instanceof File || typeof value === "object" && value !== null && "name" in value && "size" in value && "type" in value && typeof value.arrayBuffer === "function";
|
|
328
|
+
}
|
|
329
|
+
function isFileSchemaDef(schema) {
|
|
330
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
331
|
+
return kind === "File";
|
|
332
|
+
}
|
|
333
|
+
function isFileArraySchemaDef(schema) {
|
|
334
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
335
|
+
return kind === "FileArray";
|
|
336
|
+
}
|
|
337
|
+
function getSchemaFileOptions(schema) {
|
|
338
|
+
return schema.fileOptions;
|
|
339
|
+
}
|
|
340
|
+
function validateSingleFile(file, fieldPath, options, errors) {
|
|
341
|
+
if (!options) return;
|
|
342
|
+
const { maxSize, minSize, allowedTypes } = options;
|
|
343
|
+
if (maxSize !== void 0 && file.size > maxSize) {
|
|
344
|
+
errors.push({
|
|
345
|
+
path: fieldPath,
|
|
346
|
+
message: `File size ${formatFileSize(file.size)} exceeds maximum ${formatFileSize(maxSize)}`,
|
|
347
|
+
value: file.size
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (minSize !== void 0 && file.size < minSize) {
|
|
351
|
+
errors.push({
|
|
352
|
+
path: fieldPath,
|
|
353
|
+
message: `File size ${formatFileSize(file.size)} is below minimum ${formatFileSize(minSize)}`,
|
|
354
|
+
value: file.size
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
|
|
358
|
+
errors.push({
|
|
359
|
+
path: fieldPath,
|
|
360
|
+
message: `File type "${file.type}" is not allowed. Allowed: ${allowedTypes.join(", ")}`,
|
|
361
|
+
value: file.type
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
283
365
|
function validateField(schema, rawValue, fieldName) {
|
|
284
366
|
if (!schema) {
|
|
285
367
|
return {};
|
|
@@ -298,6 +380,87 @@ function validateField(schema, rawValue, fieldName) {
|
|
|
298
380
|
}
|
|
299
381
|
return converted;
|
|
300
382
|
}
|
|
383
|
+
function validateFormData(schema, rawValue, fieldName) {
|
|
384
|
+
if (!schema) {
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
const schemaProps = schema.properties;
|
|
388
|
+
if (!schemaProps) {
|
|
389
|
+
return rawValue;
|
|
390
|
+
}
|
|
391
|
+
const result = {};
|
|
392
|
+
const nonFileData = {};
|
|
393
|
+
const nonFileSchema = {};
|
|
394
|
+
const fileErrors = [];
|
|
395
|
+
for (const [key, value] of Object.entries(rawValue)) {
|
|
396
|
+
const propSchema = schemaProps[key];
|
|
397
|
+
if (propSchema && isFileSchemaDef(propSchema)) {
|
|
398
|
+
result[key] = value;
|
|
399
|
+
if (isFile(value)) {
|
|
400
|
+
const fileOptions = getSchemaFileOptions(propSchema);
|
|
401
|
+
validateSingleFile(value, `/${key}`, fileOptions, fileErrors);
|
|
402
|
+
}
|
|
403
|
+
} else if (propSchema && isFileArraySchemaDef(propSchema)) {
|
|
404
|
+
result[key] = value;
|
|
405
|
+
const fileOptions = getSchemaFileOptions(propSchema);
|
|
406
|
+
const files = Array.isArray(value) ? value : [value];
|
|
407
|
+
const fileArray = files.filter(isFile);
|
|
408
|
+
if (fileOptions?.maxFiles !== void 0 && fileArray.length > fileOptions.maxFiles) {
|
|
409
|
+
fileErrors.push({
|
|
410
|
+
path: `/${key}`,
|
|
411
|
+
message: `Too many files. Maximum: ${fileOptions.maxFiles}, received: ${fileArray.length}`,
|
|
412
|
+
value: fileArray.length
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (fileOptions?.minFiles !== void 0 && fileArray.length < fileOptions.minFiles) {
|
|
416
|
+
fileErrors.push({
|
|
417
|
+
path: `/${key}`,
|
|
418
|
+
message: `Too few files. Minimum: ${fileOptions.minFiles}, received: ${fileArray.length}`,
|
|
419
|
+
value: fileArray.length
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
fileArray.forEach((file, index) => {
|
|
423
|
+
validateSingleFile(file, `/${key}/${index}`, fileOptions, fileErrors);
|
|
424
|
+
});
|
|
425
|
+
} else if (isFile(value) || Array.isArray(value) && value.some(isFile)) {
|
|
426
|
+
result[key] = value;
|
|
427
|
+
} else {
|
|
428
|
+
nonFileData[key] = value;
|
|
429
|
+
if (propSchema) {
|
|
430
|
+
nonFileSchema[key] = propSchema;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (fileErrors.length > 0) {
|
|
435
|
+
throw new ValidationError({
|
|
436
|
+
message: `Invalid ${fieldName}`,
|
|
437
|
+
fields: fileErrors
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (Object.keys(nonFileSchema).length > 0) {
|
|
441
|
+
const tempSchema = {
|
|
442
|
+
...schema,
|
|
443
|
+
properties: nonFileSchema,
|
|
444
|
+
required: schema.required?.filter((r) => r in nonFileSchema) ?? []
|
|
445
|
+
};
|
|
446
|
+
const converted = Value.Convert(tempSchema, nonFileData);
|
|
447
|
+
const errors = [...Value.Errors(tempSchema, converted)];
|
|
448
|
+
if (errors.length > 0) {
|
|
449
|
+
throw new ValidationError({
|
|
450
|
+
message: `Invalid ${fieldName}`,
|
|
451
|
+
fields: errors.map((e) => ({
|
|
452
|
+
path: e.path,
|
|
453
|
+
message: e.message,
|
|
454
|
+
value: e.value
|
|
455
|
+
}))
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
Object.assign(result, converted);
|
|
459
|
+
} else {
|
|
460
|
+
Object.assign(result, nonFileData);
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
301
464
|
function extractQueryParams(c) {
|
|
302
465
|
const url = new URL(c.req.url);
|
|
303
466
|
const queryObj = {};
|
|
@@ -345,6 +508,34 @@ async function parseJsonBody(c) {
|
|
|
345
508
|
});
|
|
346
509
|
}
|
|
347
510
|
}
|
|
511
|
+
async function parseFormData(c) {
|
|
512
|
+
try {
|
|
513
|
+
const formData = await c.req.formData();
|
|
514
|
+
const result = {};
|
|
515
|
+
formData.forEach((value, key) => {
|
|
516
|
+
const existing = result[key];
|
|
517
|
+
if (existing !== void 0) {
|
|
518
|
+
if (Array.isArray(existing)) {
|
|
519
|
+
existing.push(value);
|
|
520
|
+
} else {
|
|
521
|
+
result[key] = [existing, value];
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
result[key] = value;
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
return result;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new ValidationError({
|
|
530
|
+
message: "Invalid form data",
|
|
531
|
+
fields: [{
|
|
532
|
+
path: "/",
|
|
533
|
+
message: "Failed to parse form data",
|
|
534
|
+
value: error instanceof Error ? error.message : "Unknown error"
|
|
535
|
+
}]
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
348
539
|
|
|
349
540
|
// src/route/register-routes.ts
|
|
350
541
|
function isRouter(value) {
|
|
@@ -411,18 +602,28 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
|
|
|
411
602
|
const registeredNames = /* @__PURE__ */ new Set();
|
|
412
603
|
const registeredHandlers = /* @__PURE__ */ new Set();
|
|
413
604
|
const skipAll = skipMiddlewares === "*";
|
|
605
|
+
const autoSkips = /* @__PURE__ */ new Set();
|
|
606
|
+
for (const mw of middlewares) {
|
|
607
|
+
if (isNamedMiddleware(mw) && mw.skips) {
|
|
608
|
+
for (const skipName of mw.skips) {
|
|
609
|
+
autoSkips.add(skipName);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
414
613
|
if (namedMiddlewares && namedMiddlewares.length > 0) {
|
|
415
614
|
if (skipAll) {
|
|
416
615
|
logger.debug(`\u23ED\uFE0F Skipping all middlewares (*) for route: ${method} ${path}`, { name });
|
|
417
616
|
} else {
|
|
418
617
|
const skipSet = new Set(Array.isArray(skipMiddlewares) ? skipMiddlewares : []);
|
|
419
618
|
for (const middleware of namedMiddlewares) {
|
|
420
|
-
if (
|
|
619
|
+
if (skipSet.has(middleware.name)) {
|
|
620
|
+
logger.debug(`\u23ED\uFE0F Skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
|
|
621
|
+
} else if (autoSkips.has(middleware.name)) {
|
|
622
|
+
logger.debug(`\u23ED\uFE0F Auto-skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
|
|
623
|
+
} else {
|
|
421
624
|
allMiddlewares.push(middleware.handler);
|
|
422
625
|
registeredNames.add(middleware.name);
|
|
423
626
|
registeredHandlers.add(middleware.handler);
|
|
424
|
-
} else {
|
|
425
|
-
logger.debug(`\u23ED\uFE0F Skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
|
|
426
627
|
}
|
|
427
628
|
}
|
|
428
629
|
}
|
|
@@ -445,11 +646,8 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
|
|
|
445
646
|
}
|
|
446
647
|
}
|
|
447
648
|
const methodLower = method.toLowerCase();
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
} else {
|
|
451
|
-
app[methodLower](path, wrappedHandler);
|
|
452
|
-
}
|
|
649
|
+
const handlers = [...allMiddlewares, wrappedHandler];
|
|
650
|
+
app.on([methodLower], [path], ...handlers);
|
|
453
651
|
logger.debug(`Registered route: ${method} ${path}`, { name });
|
|
454
652
|
return { method, path, name };
|
|
455
653
|
}
|
|
@@ -459,9 +657,16 @@ async function createRouteBuilderContext(c, input) {
|
|
|
459
657
|
const headers = validateField(input.headers, extractHeaders(c), "headers");
|
|
460
658
|
const cookies = validateField(input.cookies, extractCookies(c), "cookies");
|
|
461
659
|
let body = {};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
660
|
+
let formData = {};
|
|
661
|
+
if (input.body || input.formData) {
|
|
662
|
+
const contentType = c.req.header("content-type") || "";
|
|
663
|
+
if (contentType.includes("multipart/form-data") && input.formData) {
|
|
664
|
+
const rawFormData = await parseFormData(c);
|
|
665
|
+
formData = validateFormData(input.formData, rawFormData, "form data");
|
|
666
|
+
} else if (input.body) {
|
|
667
|
+
const rawBody = await parseJsonBody(c);
|
|
668
|
+
body = validateField(input.body, rawBody, "request body");
|
|
669
|
+
}
|
|
465
670
|
}
|
|
466
671
|
let cachedData = null;
|
|
467
672
|
const responseMeta = {
|
|
@@ -472,7 +677,7 @@ async function createRouteBuilderContext(c, input) {
|
|
|
472
677
|
const context = {
|
|
473
678
|
data: async () => {
|
|
474
679
|
if (!cachedData) {
|
|
475
|
-
cachedData = { params, query, body, headers, cookies };
|
|
680
|
+
cachedData = { params, query, body, formData, headers, cookies };
|
|
476
681
|
}
|
|
477
682
|
return cachedData;
|
|
478
683
|
},
|
|
@@ -522,14 +727,16 @@ async function createRouteBuilderContext(c, input) {
|
|
|
522
727
|
}
|
|
523
728
|
|
|
524
729
|
// src/route/define-middleware.ts
|
|
525
|
-
function defineMiddleware(name, handlerOrFactory) {
|
|
730
|
+
function defineMiddleware(name, handlerOrFactory, options) {
|
|
731
|
+
const skips = options?.skips;
|
|
526
732
|
if (typeof handlerOrFactory === "function") {
|
|
527
733
|
const paramCount = handlerOrFactory.length;
|
|
528
734
|
if (paramCount === 2) {
|
|
529
735
|
return {
|
|
530
736
|
name,
|
|
531
737
|
handler: handlerOrFactory,
|
|
532
|
-
_name: name
|
|
738
|
+
_name: name,
|
|
739
|
+
...skips && { skips }
|
|
533
740
|
};
|
|
534
741
|
} else {
|
|
535
742
|
const factory = handlerOrFactory;
|
|
@@ -546,13 +753,22 @@ function defineMiddleware(name, handlerOrFactory) {
|
|
|
546
753
|
enumerable: false,
|
|
547
754
|
configurable: true
|
|
548
755
|
});
|
|
756
|
+
if (skips) {
|
|
757
|
+
Object.defineProperty(wrapper, "skips", {
|
|
758
|
+
value: skips,
|
|
759
|
+
writable: false,
|
|
760
|
+
enumerable: false,
|
|
761
|
+
configurable: true
|
|
762
|
+
});
|
|
763
|
+
}
|
|
549
764
|
return wrapper;
|
|
550
765
|
}
|
|
551
766
|
}
|
|
552
767
|
return {
|
|
553
768
|
name,
|
|
554
769
|
handler: handlerOrFactory,
|
|
555
|
-
_name: name
|
|
770
|
+
_name: name,
|
|
771
|
+
...skips && { skips }
|
|
556
772
|
};
|
|
557
773
|
}
|
|
558
774
|
function defineMiddlewareFactory(name, factory) {
|
|
@@ -577,6 +793,6 @@ function isHttpMethod(value) {
|
|
|
577
793
|
var Nullable = (schema) => Type.Union([schema, Type.Null()]);
|
|
578
794
|
var OptionalNullable = (schema) => Type.Optional(Type.Union([schema, Type.Null()]));
|
|
579
795
|
|
|
580
|
-
export { Nullable, OptionalNullable, defineMiddleware, defineMiddlewareFactory, defineRouter, isHttpMethod, registerRoutes, route };
|
|
796
|
+
export { FileArraySchema, FileSchema, Nullable, OptionalFileSchema, OptionalNullable, defineMiddleware, defineMiddlewareFactory, defineRouter, formatFileSize, getFileOptions, isFileArraySchema, isFileSchema, isHttpMethod, registerRoutes, route };
|
|
581
797
|
//# sourceMappingURL=index.js.map
|
|
582
798
|
//# sourceMappingURL=index.js.map
|