@onebun/core 0.1.24 → 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 +8 -8
- package/src/application/application.test.ts +492 -19
- package/src/application/application.ts +490 -358
- package/src/decorators/decorators.test.ts +139 -0
- package/src/decorators/decorators.ts +127 -0
- package/src/docs-examples.test.ts +670 -71
- 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 +13 -0
- package/src/module/controller.ts +7 -3
- package/src/queue/docs-examples.test.ts +86 -0
- package/src/service-client/service-client.test.ts +1 -1
- package/src/types.ts +45 -2
- package/src/validation/schemas.test.ts +0 -2
- package/src/websocket/ws-base-gateway.ts +2 -2
- package/src/websocket/ws-handler.ts +4 -3
- package/src/websocket/ws.types.ts +1 -1
|
@@ -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,
|
|
@@ -493,6 +495,18 @@ export const Body = createParamDecorator(ParamType.BODY);
|
|
|
493
495
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
494
496
|
export const Header = createParamDecorator(ParamType.HEADER);
|
|
495
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Cookie parameter decorator.
|
|
500
|
+
* Extracts a cookie value by name using BunRequest.cookies (CookieMap).
|
|
501
|
+
* Optional by default, use { required: true } for required cookies.
|
|
502
|
+
* @example \@Cookie('session_id') - optional
|
|
503
|
+
* @example \@Cookie('session_id', { required: true }) - required
|
|
504
|
+
* @example \@Cookie('session_id', schema) - optional with validation
|
|
505
|
+
* @example \@Cookie('session_id', schema, { required: true }) - required with validation
|
|
506
|
+
*/
|
|
507
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
508
|
+
export const Cookie = createParamDecorator(ParamType.COOKIE);
|
|
509
|
+
|
|
496
510
|
/**
|
|
497
511
|
* Request object decorator
|
|
498
512
|
* @example \@Req()
|
|
@@ -507,6 +521,119 @@ export const Req = createParamDecorator(ParamType.REQUEST);
|
|
|
507
521
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
508
522
|
export const Res = createParamDecorator(ParamType.RESPONSE);
|
|
509
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
|
+
|
|
510
637
|
/**
|
|
511
638
|
* Middleware decorator
|
|
512
639
|
* @example \@UseMiddleware(authMiddleware)
|