@onebun/core 0.2.0 → 0.2.1
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/package.json +1 -1
- package/src/application/application.ts +188 -0
- package/src/decorators/decorators.test.ts +139 -0
- package/src/decorators/decorators.ts +115 -0
- package/src/docs-examples.test.ts +373 -73
- package/src/file/index.ts +8 -0
- package/src/file/onebun-file.test.ts +315 -0
- package/src/file/onebun-file.ts +304 -0
- package/src/index.ts +11 -0
- package/src/types.ts +27 -0
package/package.json
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
getSseMetadata,
|
|
31
31
|
type SseDecoratorOptions,
|
|
32
32
|
} from '../decorators/decorators';
|
|
33
|
+
import { OneBunFile, validateFile } from '../file/onebun-file';
|
|
33
34
|
import {
|
|
34
35
|
NotInitializedConfig,
|
|
35
36
|
type IConfig,
|
|
@@ -911,6 +912,43 @@ export class OneBunApplication {
|
|
|
911
912
|
throw error;
|
|
912
913
|
}
|
|
913
914
|
|
|
915
|
+
/**
|
|
916
|
+
* Extract an OneBunFile from a JSON value.
|
|
917
|
+
* Supports two formats:
|
|
918
|
+
* - String: raw base64 data
|
|
919
|
+
* - Object: { data: string, filename?: string, mimeType?: string }
|
|
920
|
+
*/
|
|
921
|
+
function extractFileFromJsonValue(value: unknown): OneBunFile | undefined {
|
|
922
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
923
|
+
return OneBunFile.fromBase64(value);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
927
|
+
const obj = value as Record<string, unknown>;
|
|
928
|
+
if (typeof obj.data === 'string' && obj.data.length > 0) {
|
|
929
|
+
return OneBunFile.fromBase64(
|
|
930
|
+
obj.data,
|
|
931
|
+
typeof obj.filename === 'string' ? obj.filename : undefined,
|
|
932
|
+
typeof obj.mimeType === 'string' ? obj.mimeType : undefined,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Extract a file from a JSON body by field name
|
|
942
|
+
*/
|
|
943
|
+
function extractFileFromJson(
|
|
944
|
+
jsonBody: Record<string, unknown>,
|
|
945
|
+
fieldName: string,
|
|
946
|
+
): OneBunFile | undefined {
|
|
947
|
+
const fieldValue = jsonBody[fieldName];
|
|
948
|
+
|
|
949
|
+
return extractFileFromJsonValue(fieldValue);
|
|
950
|
+
}
|
|
951
|
+
|
|
914
952
|
/**
|
|
915
953
|
* Execute route handler with parameter injection and validation.
|
|
916
954
|
* Path parameters come from BunRequest.params (populated by Bun routes API).
|
|
@@ -951,6 +989,52 @@ export class OneBunApplication {
|
|
|
951
989
|
// Sort params by index to ensure correct order
|
|
952
990
|
const sortedParams = [...(route.params || [])].sort((a, b) => a.index - b.index);
|
|
953
991
|
|
|
992
|
+
// Pre-parse body for file upload params (FormData or JSON, cached for all params)
|
|
993
|
+
const needsFileData = sortedParams.some(
|
|
994
|
+
(p) =>
|
|
995
|
+
p.type === ParamType.FILE ||
|
|
996
|
+
p.type === ParamType.FILES ||
|
|
997
|
+
p.type === ParamType.FORM_FIELD,
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
// Validate that @Body and file decorators are not used on the same method
|
|
1001
|
+
if (needsFileData) {
|
|
1002
|
+
const hasBody = sortedParams.some((p) => p.type === ParamType.BODY);
|
|
1003
|
+
if (hasBody) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
'Cannot use @Body() together with @UploadedFile/@UploadedFiles/@FormField on the same method. ' +
|
|
1006
|
+
'Both consume the request body. Use file decorators for multipart/base64 uploads.',
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1012
|
+
let formData: any = null;
|
|
1013
|
+
let jsonBody: Record<string, unknown> | null = null;
|
|
1014
|
+
let isMultipart = false;
|
|
1015
|
+
|
|
1016
|
+
if (needsFileData) {
|
|
1017
|
+
const contentType = req.headers.get('content-type') || '';
|
|
1018
|
+
|
|
1019
|
+
if (contentType.includes('multipart/form-data')) {
|
|
1020
|
+
isMultipart = true;
|
|
1021
|
+
try {
|
|
1022
|
+
formData = await req.formData();
|
|
1023
|
+
} catch {
|
|
1024
|
+
formData = null;
|
|
1025
|
+
}
|
|
1026
|
+
} else if (contentType.includes('application/json')) {
|
|
1027
|
+
try {
|
|
1028
|
+
const parsed = await req.json();
|
|
1029
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1030
|
+
jsonBody = parsed as Record<string, unknown>;
|
|
1031
|
+
}
|
|
1032
|
+
} catch {
|
|
1033
|
+
jsonBody = null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
954
1038
|
for (const param of sortedParams) {
|
|
955
1039
|
switch (param.type) {
|
|
956
1040
|
case ParamType.PATH:
|
|
@@ -989,6 +1073,100 @@ export class OneBunApplication {
|
|
|
989
1073
|
args[param.index] = undefined;
|
|
990
1074
|
break;
|
|
991
1075
|
|
|
1076
|
+
case ParamType.FILE: {
|
|
1077
|
+
let file: OneBunFile | undefined;
|
|
1078
|
+
|
|
1079
|
+
if (isMultipart && formData && param.name) {
|
|
1080
|
+
const entry = formData.get(param.name);
|
|
1081
|
+
if (entry instanceof File) {
|
|
1082
|
+
file = new OneBunFile(entry);
|
|
1083
|
+
}
|
|
1084
|
+
} else if (jsonBody && param.name) {
|
|
1085
|
+
file = extractFileFromJson(jsonBody, param.name);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (file && param.fileOptions) {
|
|
1089
|
+
validateFile(file, param.fileOptions, param.name);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
args[param.index] = file;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
case ParamType.FILES: {
|
|
1097
|
+
let files: OneBunFile[] = [];
|
|
1098
|
+
|
|
1099
|
+
if (isMultipart && formData) {
|
|
1100
|
+
if (param.name) {
|
|
1101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1102
|
+
const entries: any[] = formData.getAll(param.name);
|
|
1103
|
+
files = entries
|
|
1104
|
+
.filter((entry: unknown): entry is File => entry instanceof File)
|
|
1105
|
+
.map((f: File) => new OneBunFile(f));
|
|
1106
|
+
} else {
|
|
1107
|
+
// Get all files from all fields
|
|
1108
|
+
for (const [, value] of formData.entries()) {
|
|
1109
|
+
if (value instanceof File) {
|
|
1110
|
+
files.push(new OneBunFile(value));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
} else if (jsonBody) {
|
|
1115
|
+
if (param.name) {
|
|
1116
|
+
const fieldValue = jsonBody[param.name];
|
|
1117
|
+
if (Array.isArray(fieldValue)) {
|
|
1118
|
+
files = fieldValue
|
|
1119
|
+
.map((item) => extractFileFromJsonValue(item))
|
|
1120
|
+
.filter((f): f is OneBunFile => f !== undefined);
|
|
1121
|
+
}
|
|
1122
|
+
} else {
|
|
1123
|
+
// Extract all file-like values from JSON
|
|
1124
|
+
for (const [, value] of Object.entries(jsonBody)) {
|
|
1125
|
+
const file = extractFileFromJsonValue(value);
|
|
1126
|
+
if (file) {
|
|
1127
|
+
files.push(file);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Validate maxCount
|
|
1134
|
+
if (param.fileOptions?.maxCount !== undefined && files.length > param.fileOptions.maxCount) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`Too many files for "${param.name || 'upload'}". Got ${files.length}, max is ${param.fileOptions.maxCount}`,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Validate each file
|
|
1141
|
+
if (param.fileOptions) {
|
|
1142
|
+
for (const file of files) {
|
|
1143
|
+
validateFile(file, param.fileOptions, param.name);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
args[param.index] = files;
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
case ParamType.FORM_FIELD: {
|
|
1152
|
+
let value: string | undefined;
|
|
1153
|
+
|
|
1154
|
+
if (isMultipart && formData && param.name) {
|
|
1155
|
+
const entry = formData.get(param.name);
|
|
1156
|
+
if (typeof entry === 'string') {
|
|
1157
|
+
value = entry;
|
|
1158
|
+
}
|
|
1159
|
+
} else if (jsonBody && param.name) {
|
|
1160
|
+
const jsonValue = jsonBody[param.name];
|
|
1161
|
+
if (jsonValue !== undefined && jsonValue !== null) {
|
|
1162
|
+
value = String(jsonValue);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
args[param.index] = value;
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
992
1170
|
default:
|
|
993
1171
|
args[param.index] = undefined;
|
|
994
1172
|
}
|
|
@@ -998,6 +1176,16 @@ export class OneBunApplication {
|
|
|
998
1176
|
throw new Error(`Required parameter ${param.name || param.index} is missing`);
|
|
999
1177
|
}
|
|
1000
1178
|
|
|
1179
|
+
// For FILES type, also check for empty array when required
|
|
1180
|
+
if (
|
|
1181
|
+
param.isRequired &&
|
|
1182
|
+
param.type === ParamType.FILES &&
|
|
1183
|
+
Array.isArray(args[param.index]) &&
|
|
1184
|
+
(args[param.index] as unknown[]).length === 0
|
|
1185
|
+
) {
|
|
1186
|
+
throw new Error(`Required parameter ${param.name || param.index} is missing`);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1001
1189
|
// Apply arktype schema validation if provided
|
|
1002
1190
|
if (param.schema && args[param.index] !== undefined) {
|
|
1003
1191
|
try {
|
|
@@ -49,6 +49,9 @@ import {
|
|
|
49
49
|
Module,
|
|
50
50
|
getModuleMetadata,
|
|
51
51
|
ApiResponse,
|
|
52
|
+
UploadedFile,
|
|
53
|
+
UploadedFiles,
|
|
54
|
+
FormField,
|
|
52
55
|
} from './decorators';
|
|
53
56
|
|
|
54
57
|
describe('decorators', () => {
|
|
@@ -1077,4 +1080,140 @@ describe('decorators', () => {
|
|
|
1077
1080
|
expect(isGlobalModule(NonGlobalModuleC)).toBe(false);
|
|
1078
1081
|
});
|
|
1079
1082
|
});
|
|
1083
|
+
|
|
1084
|
+
// ============================================================================
|
|
1085
|
+
// File Upload Decorators
|
|
1086
|
+
// ============================================================================
|
|
1087
|
+
|
|
1088
|
+
describe('File Upload Decorators', () => {
|
|
1089
|
+
test('@UploadedFile should store FILE param metadata', () => {
|
|
1090
|
+
@Controller('/test')
|
|
1091
|
+
class TestController extends BaseController {
|
|
1092
|
+
@Post('/upload')
|
|
1093
|
+
upload(@UploadedFile('avatar') _file: unknown) {}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const metadata = getControllerMetadata(TestController);
|
|
1097
|
+
expect(metadata).toBeDefined();
|
|
1098
|
+
const route = metadata!.routes[0];
|
|
1099
|
+
expect(route.params).toBeDefined();
|
|
1100
|
+
expect(route.params!.length).toBe(1);
|
|
1101
|
+
expect(route.params![0].type).toBe(ParamType.FILE);
|
|
1102
|
+
expect(route.params![0].name).toBe('avatar');
|
|
1103
|
+
expect(route.params![0].index).toBe(0);
|
|
1104
|
+
expect(route.params![0].isRequired).toBe(true);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test('@UploadedFile should store file options', () => {
|
|
1108
|
+
@Controller('/test')
|
|
1109
|
+
class TestController extends BaseController {
|
|
1110
|
+
@Post('/upload')
|
|
1111
|
+
upload(
|
|
1112
|
+
@UploadedFile('avatar', { maxSize: 1024, mimeTypes: ['image/*'], required: false }) _file: unknown,
|
|
1113
|
+
) {}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const metadata = getControllerMetadata(TestController);
|
|
1117
|
+
const param = metadata!.routes[0].params![0];
|
|
1118
|
+
expect(param.isRequired).toBe(false);
|
|
1119
|
+
expect(param.fileOptions).toBeDefined();
|
|
1120
|
+
expect(param.fileOptions!.maxSize).toBe(1024);
|
|
1121
|
+
expect(param.fileOptions!.mimeTypes).toEqual(['image/*']);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test('@UploadedFile without options should be required by default', () => {
|
|
1125
|
+
@Controller('/test')
|
|
1126
|
+
class TestController extends BaseController {
|
|
1127
|
+
@Post('/upload')
|
|
1128
|
+
upload(@UploadedFile('file') _file: unknown) {}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const metadata = getControllerMetadata(TestController);
|
|
1132
|
+
const param = metadata!.routes[0].params![0];
|
|
1133
|
+
expect(param.isRequired).toBe(true);
|
|
1134
|
+
expect(param.fileOptions).toBeUndefined();
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test('@UploadedFiles should store FILES param metadata', () => {
|
|
1138
|
+
@Controller('/test')
|
|
1139
|
+
class TestController extends BaseController {
|
|
1140
|
+
@Post('/upload')
|
|
1141
|
+
upload(@UploadedFiles('docs', { maxCount: 5, maxSize: 2048 }) _files: unknown) {}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const metadata = getControllerMetadata(TestController);
|
|
1145
|
+
const param = metadata!.routes[0].params![0];
|
|
1146
|
+
expect(param.type).toBe(ParamType.FILES);
|
|
1147
|
+
expect(param.name).toBe('docs');
|
|
1148
|
+
expect(param.isRequired).toBe(true);
|
|
1149
|
+
expect(param.fileOptions!.maxCount).toBe(5);
|
|
1150
|
+
expect(param.fileOptions!.maxSize).toBe(2048);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test('@UploadedFiles without field name should have empty name', () => {
|
|
1154
|
+
@Controller('/test')
|
|
1155
|
+
class TestController extends BaseController {
|
|
1156
|
+
@Post('/upload')
|
|
1157
|
+
upload(@UploadedFiles() _files: unknown) {}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const metadata = getControllerMetadata(TestController);
|
|
1161
|
+
const param = metadata!.routes[0].params![0];
|
|
1162
|
+
expect(param.type).toBe(ParamType.FILES);
|
|
1163
|
+
expect(param.name).toBe('');
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test('@FormField should store FORM_FIELD param metadata', () => {
|
|
1167
|
+
@Controller('/test')
|
|
1168
|
+
class TestController extends BaseController {
|
|
1169
|
+
@Post('/upload')
|
|
1170
|
+
upload(@FormField('name') _name: unknown) {}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const metadata = getControllerMetadata(TestController);
|
|
1174
|
+
const param = metadata!.routes[0].params![0];
|
|
1175
|
+
expect(param.type).toBe(ParamType.FORM_FIELD);
|
|
1176
|
+
expect(param.name).toBe('name');
|
|
1177
|
+
expect(param.isRequired).toBe(false);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test('@FormField with required option', () => {
|
|
1181
|
+
@Controller('/test')
|
|
1182
|
+
class TestController extends BaseController {
|
|
1183
|
+
@Post('/upload')
|
|
1184
|
+
upload(@FormField('name', { required: true }) _name: unknown) {}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const metadata = getControllerMetadata(TestController);
|
|
1188
|
+
const param = metadata!.routes[0].params![0];
|
|
1189
|
+
expect(param.isRequired).toBe(true);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
test('should support mixing file and form field decorators', () => {
|
|
1193
|
+
@Controller('/test')
|
|
1194
|
+
class TestController extends BaseController {
|
|
1195
|
+
@Post('/profile')
|
|
1196
|
+
createProfile(
|
|
1197
|
+
@UploadedFile('avatar') _avatar: unknown,
|
|
1198
|
+
@FormField('name', { required: true }) _name: unknown,
|
|
1199
|
+
@FormField('email') _email: unknown,
|
|
1200
|
+
) {}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const metadata = getControllerMetadata(TestController);
|
|
1204
|
+
const params = metadata!.routes[0].params!;
|
|
1205
|
+
expect(params.length).toBe(3);
|
|
1206
|
+
|
|
1207
|
+
const fileParam = params.find((p) => p.type === ParamType.FILE);
|
|
1208
|
+
const nameParam = params.find((p) => p.name === 'name');
|
|
1209
|
+
const emailParam = params.find((p) => p.name === 'email');
|
|
1210
|
+
|
|
1211
|
+
expect(fileParam).toBeDefined();
|
|
1212
|
+
expect(nameParam).toBeDefined();
|
|
1213
|
+
expect(nameParam!.type).toBe(ParamType.FORM_FIELD);
|
|
1214
|
+
expect(nameParam!.isRequired).toBe(true);
|
|
1215
|
+
expect(emailParam).toBeDefined();
|
|
1216
|
+
expect(emailParam!.isRequired).toBe(false);
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1080
1219
|
});
|
|
@@ -3,6 +3,8 @@ import { type, type Type } from 'arktype';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
type ControllerMetadata,
|
|
6
|
+
type FileUploadOptions,
|
|
7
|
+
type FilesUploadOptions,
|
|
6
8
|
HttpMethod,
|
|
7
9
|
type ParamDecoratorOptions,
|
|
8
10
|
type ParamMetadata,
|
|
@@ -519,6 +521,119 @@ export const Req = createParamDecorator(ParamType.REQUEST);
|
|
|
519
521
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
520
522
|
export const Res = createParamDecorator(ParamType.RESPONSE);
|
|
521
523
|
|
|
524
|
+
// =============================================================================
|
|
525
|
+
// File Upload Decorators
|
|
526
|
+
// =============================================================================
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Single file upload decorator.
|
|
530
|
+
* Extracts a single file from the request (multipart/form-data or JSON base64).
|
|
531
|
+
* Required by default.
|
|
532
|
+
*
|
|
533
|
+
* @param fieldName - Form field name to extract file from
|
|
534
|
+
* @param options - File upload options (maxSize, mimeTypes, required)
|
|
535
|
+
*
|
|
536
|
+
* @example \@UploadedFile('avatar')
|
|
537
|
+
* @example \@UploadedFile('avatar', { maxSize: 5 * 1024 * 1024 })
|
|
538
|
+
* @example \@UploadedFile('avatar', { mimeTypes: [MimeType.PNG, MimeType.JPEG] })
|
|
539
|
+
* @example \@UploadedFile('avatar', { maxSize: 5 * 1024 * 1024, mimeTypes: [MimeType.ANY_IMAGE] })
|
|
540
|
+
*/
|
|
541
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
542
|
+
export function UploadedFile(fieldName?: string, options?: FileUploadOptions): ParameterDecorator {
|
|
543
|
+
return (target: object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
|
|
544
|
+
const params: ParamMetadata[] =
|
|
545
|
+
Reflect.getMetadata(PARAMS_METADATA, target, propertyKey) || [];
|
|
546
|
+
|
|
547
|
+
const isRequired = options?.required ?? true;
|
|
548
|
+
|
|
549
|
+
const metadata: ParamMetadata = {
|
|
550
|
+
type: ParamType.FILE,
|
|
551
|
+
name: fieldName || '',
|
|
552
|
+
index: parameterIndex,
|
|
553
|
+
isRequired,
|
|
554
|
+
fileOptions: options ? {
|
|
555
|
+
maxSize: options.maxSize,
|
|
556
|
+
mimeTypes: options.mimeTypes,
|
|
557
|
+
required: options.required,
|
|
558
|
+
} : undefined,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
params.push(metadata);
|
|
562
|
+
Reflect.defineMetadata(PARAMS_METADATA, params, target, propertyKey as string);
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Multiple file upload decorator.
|
|
568
|
+
* Extracts multiple files from the request (multipart/form-data or JSON base64).
|
|
569
|
+
* Required by default (at least one file expected).
|
|
570
|
+
*
|
|
571
|
+
* @param fieldName - Form field name to extract files from. If omitted, extracts all files.
|
|
572
|
+
* @param options - File upload options (maxSize, mimeTypes, maxCount, required)
|
|
573
|
+
*
|
|
574
|
+
* @example \@UploadedFiles('documents')
|
|
575
|
+
* @example \@UploadedFiles('documents', { maxCount: 5 })
|
|
576
|
+
* @example \@UploadedFiles(undefined, { maxCount: 10 }) - all files
|
|
577
|
+
* @example \@UploadedFiles('images', { mimeTypes: [MimeType.ANY_IMAGE], maxSize: 10 * 1024 * 1024 })
|
|
578
|
+
*/
|
|
579
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
580
|
+
export function UploadedFiles(fieldName?: string, options?: FilesUploadOptions): ParameterDecorator {
|
|
581
|
+
return (target: object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
|
|
582
|
+
const params: ParamMetadata[] =
|
|
583
|
+
Reflect.getMetadata(PARAMS_METADATA, target, propertyKey) || [];
|
|
584
|
+
|
|
585
|
+
const isRequired = options?.required ?? true;
|
|
586
|
+
|
|
587
|
+
const metadata: ParamMetadata = {
|
|
588
|
+
type: ParamType.FILES,
|
|
589
|
+
name: fieldName || '',
|
|
590
|
+
index: parameterIndex,
|
|
591
|
+
isRequired,
|
|
592
|
+
fileOptions: options ? {
|
|
593
|
+
maxSize: options.maxSize,
|
|
594
|
+
mimeTypes: options.mimeTypes,
|
|
595
|
+
required: options.required,
|
|
596
|
+
maxCount: options.maxCount,
|
|
597
|
+
} : undefined,
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
params.push(metadata);
|
|
601
|
+
Reflect.defineMetadata(PARAMS_METADATA, params, target, propertyKey as string);
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Form field decorator.
|
|
607
|
+
* Extracts a non-file field from the request (multipart/form-data or JSON body).
|
|
608
|
+
* Optional by default.
|
|
609
|
+
*
|
|
610
|
+
* @param fieldName - Form field name to extract
|
|
611
|
+
* @param options - Options (required)
|
|
612
|
+
*
|
|
613
|
+
* @example \@FormField('name')
|
|
614
|
+
* @example \@FormField('name', { required: true })
|
|
615
|
+
* @example \@FormField('email')
|
|
616
|
+
*/
|
|
617
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
618
|
+
export function FormField(fieldName: string, options?: ParamDecoratorOptions): ParameterDecorator {
|
|
619
|
+
return (target: object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
|
|
620
|
+
const params: ParamMetadata[] =
|
|
621
|
+
Reflect.getMetadata(PARAMS_METADATA, target, propertyKey) || [];
|
|
622
|
+
|
|
623
|
+
const isRequired = options?.required ?? false;
|
|
624
|
+
|
|
625
|
+
const metadata: ParamMetadata = {
|
|
626
|
+
type: ParamType.FORM_FIELD,
|
|
627
|
+
name: fieldName,
|
|
628
|
+
index: parameterIndex,
|
|
629
|
+
isRequired,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
params.push(metadata);
|
|
633
|
+
Reflect.defineMetadata(PARAMS_METADATA, params, target, propertyKey as string);
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
522
637
|
/**
|
|
523
638
|
* Middleware decorator
|
|
524
639
|
* @example \@UseMiddleware(authMiddleware)
|