@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
|
@@ -33,9 +33,93 @@ import type {
|
|
|
33
33
|
BeforeApplicationDestroy,
|
|
34
34
|
OnApplicationDestroy,
|
|
35
35
|
} from './';
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
SseEvent,
|
|
38
|
+
SseGenerator,
|
|
39
|
+
OneBunRequest,
|
|
40
|
+
OneBunResponse,
|
|
41
|
+
} from './types';
|
|
37
42
|
import type { ServerWebSocket } from 'bun';
|
|
38
43
|
|
|
44
|
+
import {
|
|
45
|
+
Controller,
|
|
46
|
+
Get,
|
|
47
|
+
Post,
|
|
48
|
+
Put,
|
|
49
|
+
Delete,
|
|
50
|
+
Patch,
|
|
51
|
+
Param,
|
|
52
|
+
Query,
|
|
53
|
+
Body,
|
|
54
|
+
Header,
|
|
55
|
+
Req,
|
|
56
|
+
Cookie,
|
|
57
|
+
Module,
|
|
58
|
+
Service,
|
|
59
|
+
BaseService,
|
|
60
|
+
BaseController,
|
|
61
|
+
UseMiddleware,
|
|
62
|
+
getServiceTag,
|
|
63
|
+
getControllerMetadata,
|
|
64
|
+
HttpStatusCode,
|
|
65
|
+
ParamType,
|
|
66
|
+
NotFoundError,
|
|
67
|
+
InternalServerError,
|
|
68
|
+
OneBunBaseError,
|
|
69
|
+
Env,
|
|
70
|
+
validate,
|
|
71
|
+
validateOrThrow,
|
|
72
|
+
MultiServiceApplication,
|
|
73
|
+
OneBunApplication,
|
|
74
|
+
createServiceDefinition,
|
|
75
|
+
createServiceClient,
|
|
76
|
+
WebSocketGateway,
|
|
77
|
+
BaseWebSocketGateway,
|
|
78
|
+
OnConnect,
|
|
79
|
+
OnDisconnect,
|
|
80
|
+
OnJoinRoom,
|
|
81
|
+
OnLeaveRoom,
|
|
82
|
+
OnMessage,
|
|
83
|
+
Client,
|
|
84
|
+
Socket,
|
|
85
|
+
MessageData,
|
|
86
|
+
RoomName,
|
|
87
|
+
PatternParams,
|
|
88
|
+
WsServer,
|
|
89
|
+
UseWsGuards,
|
|
90
|
+
WsAuthGuard,
|
|
91
|
+
WsPermissionGuard,
|
|
92
|
+
WsAnyPermissionGuard,
|
|
93
|
+
createGuard,
|
|
94
|
+
createInMemoryWsStorage,
|
|
95
|
+
SharedRedisProvider,
|
|
96
|
+
Sse,
|
|
97
|
+
getSseMetadata,
|
|
98
|
+
formatSseEvent,
|
|
99
|
+
createSseStream,
|
|
100
|
+
createWsServiceDefinition,
|
|
101
|
+
createWsClient,
|
|
102
|
+
createNativeWsClient,
|
|
103
|
+
matchPattern,
|
|
104
|
+
makeMockLoggerLayer,
|
|
105
|
+
hasOnModuleInit,
|
|
106
|
+
hasOnApplicationInit,
|
|
107
|
+
hasOnModuleDestroy,
|
|
108
|
+
hasBeforeApplicationDestroy,
|
|
109
|
+
hasOnApplicationDestroy,
|
|
110
|
+
callOnModuleInit,
|
|
111
|
+
callOnApplicationInit,
|
|
112
|
+
callOnModuleDestroy,
|
|
113
|
+
callBeforeApplicationDestroy,
|
|
114
|
+
callOnApplicationDestroy,
|
|
115
|
+
UploadedFile,
|
|
116
|
+
UploadedFiles,
|
|
117
|
+
FormField,
|
|
118
|
+
OneBunFile,
|
|
119
|
+
MimeType,
|
|
120
|
+
matchMimeType,
|
|
121
|
+
} from './';
|
|
122
|
+
|
|
39
123
|
|
|
40
124
|
/**
|
|
41
125
|
* @source docs/index.md#minimal-working-example
|
|
@@ -3416,76 +3500,6 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
3416
3500
|
// SSE (Server-Sent Events) Documentation Tests
|
|
3417
3501
|
// ============================================================================
|
|
3418
3502
|
|
|
3419
|
-
import { Sse, getSseMetadata } from './decorators/decorators';
|
|
3420
|
-
import { formatSseEvent, createSseStream } from './module/controller';
|
|
3421
|
-
|
|
3422
|
-
import {
|
|
3423
|
-
Controller,
|
|
3424
|
-
Get,
|
|
3425
|
-
Post,
|
|
3426
|
-
Put,
|
|
3427
|
-
Delete,
|
|
3428
|
-
Patch,
|
|
3429
|
-
Param,
|
|
3430
|
-
Query,
|
|
3431
|
-
Body,
|
|
3432
|
-
Header,
|
|
3433
|
-
Req,
|
|
3434
|
-
Module,
|
|
3435
|
-
Service,
|
|
3436
|
-
BaseService,
|
|
3437
|
-
BaseController,
|
|
3438
|
-
UseMiddleware,
|
|
3439
|
-
getServiceTag,
|
|
3440
|
-
HttpStatusCode,
|
|
3441
|
-
NotFoundError,
|
|
3442
|
-
InternalServerError,
|
|
3443
|
-
OneBunBaseError,
|
|
3444
|
-
Env,
|
|
3445
|
-
validate,
|
|
3446
|
-
validateOrThrow,
|
|
3447
|
-
MultiServiceApplication,
|
|
3448
|
-
OneBunApplication,
|
|
3449
|
-
createServiceDefinition,
|
|
3450
|
-
createServiceClient,
|
|
3451
|
-
WebSocketGateway,
|
|
3452
|
-
BaseWebSocketGateway,
|
|
3453
|
-
OnConnect,
|
|
3454
|
-
OnDisconnect,
|
|
3455
|
-
OnJoinRoom,
|
|
3456
|
-
OnLeaveRoom,
|
|
3457
|
-
OnMessage,
|
|
3458
|
-
Client,
|
|
3459
|
-
Socket,
|
|
3460
|
-
MessageData,
|
|
3461
|
-
RoomName,
|
|
3462
|
-
PatternParams,
|
|
3463
|
-
WsServer,
|
|
3464
|
-
UseWsGuards,
|
|
3465
|
-
WsAuthGuard,
|
|
3466
|
-
WsPermissionGuard,
|
|
3467
|
-
WsAnyPermissionGuard,
|
|
3468
|
-
createGuard,
|
|
3469
|
-
createInMemoryWsStorage,
|
|
3470
|
-
SharedRedisProvider,
|
|
3471
|
-
createWsServiceDefinition,
|
|
3472
|
-
createWsClient,
|
|
3473
|
-
createNativeWsClient,
|
|
3474
|
-
matchPattern,
|
|
3475
|
-
makeMockLoggerLayer,
|
|
3476
|
-
hasOnModuleInit,
|
|
3477
|
-
hasOnApplicationInit,
|
|
3478
|
-
hasOnModuleDestroy,
|
|
3479
|
-
hasBeforeApplicationDestroy,
|
|
3480
|
-
hasOnApplicationDestroy,
|
|
3481
|
-
callOnModuleInit,
|
|
3482
|
-
callOnApplicationInit,
|
|
3483
|
-
callOnModuleDestroy,
|
|
3484
|
-
callBeforeApplicationDestroy,
|
|
3485
|
-
callOnApplicationDestroy,
|
|
3486
|
-
} from './';
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
3503
|
describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)', () => {
|
|
3490
3504
|
describe('SseEvent Type (docs/api/controllers.md)', () => {
|
|
3491
3505
|
/**
|
|
@@ -3829,3 +3843,588 @@ describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)',
|
|
|
3829
3843
|
});
|
|
3830
3844
|
});
|
|
3831
3845
|
});
|
|
3846
|
+
|
|
3847
|
+
// ============================================================================
|
|
3848
|
+
// Cookie, Headers, @Req with OneBunRequest
|
|
3849
|
+
// ============================================================================
|
|
3850
|
+
|
|
3851
|
+
describe('@Cookie Decorator (docs/api/decorators.md)', () => {
|
|
3852
|
+
/**
|
|
3853
|
+
* @source docs/api/decorators.md#cookie
|
|
3854
|
+
*/
|
|
3855
|
+
it('should define @Cookie decorator with optional parameter', () => {
|
|
3856
|
+
// From docs: @Cookie('session_id') - optional
|
|
3857
|
+
@Controller('/api')
|
|
3858
|
+
class ApiController extends BaseController {
|
|
3859
|
+
@Get('/me')
|
|
3860
|
+
async getMe(@Cookie('session_id') sessionId?: string) {
|
|
3861
|
+
return { sessionId };
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
expect(ApiController).toBeDefined();
|
|
3866
|
+
const metadata = getControllerMetadata(ApiController);
|
|
3867
|
+
expect(metadata).toBeDefined();
|
|
3868
|
+
expect(metadata!.routes.length).toBe(1);
|
|
3869
|
+
expect(metadata!.routes[0].params!.length).toBe(1);
|
|
3870
|
+
expect(metadata!.routes[0].params![0].type).toBe(ParamType.COOKIE);
|
|
3871
|
+
expect(metadata!.routes[0].params![0].name).toBe('session_id');
|
|
3872
|
+
expect(metadata!.routes[0].params![0].isRequired).toBe(false);
|
|
3873
|
+
});
|
|
3874
|
+
|
|
3875
|
+
/**
|
|
3876
|
+
* @source docs/api/decorators.md#cookie-required
|
|
3877
|
+
*/
|
|
3878
|
+
it('should define @Cookie decorator with required option', () => {
|
|
3879
|
+
// From docs: @Cookie('session_id', { required: true }) - required
|
|
3880
|
+
@Controller('/api')
|
|
3881
|
+
class AuthController extends BaseController {
|
|
3882
|
+
@Get('/protected')
|
|
3883
|
+
async protectedRoute(@Cookie('session_id', { required: true }) sessionId: string) {
|
|
3884
|
+
return { sessionId };
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
expect(AuthController).toBeDefined();
|
|
3889
|
+
const metadata = getControllerMetadata(AuthController);
|
|
3890
|
+
expect(metadata!.routes[0].params![0].isRequired).toBe(true);
|
|
3891
|
+
});
|
|
3892
|
+
|
|
3893
|
+
/**
|
|
3894
|
+
* @source docs/api/decorators.md#cookie-with-validation
|
|
3895
|
+
*/
|
|
3896
|
+
it('should define @Cookie decorator with validation schema', () => {
|
|
3897
|
+
// From docs: @Cookie('session_id', schema) - optional with validation
|
|
3898
|
+
const uuidSchema = type('string');
|
|
3899
|
+
|
|
3900
|
+
@Controller('/api')
|
|
3901
|
+
class ApiController extends BaseController {
|
|
3902
|
+
@Get('/session')
|
|
3903
|
+
async getSession(@Cookie('session_id', uuidSchema) sessionId?: string) {
|
|
3904
|
+
return { sessionId };
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
expect(ApiController).toBeDefined();
|
|
3909
|
+
const metadata = getControllerMetadata(ApiController);
|
|
3910
|
+
expect(metadata!.routes[0].params![0].schema).toBeDefined();
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
/**
|
|
3914
|
+
* @source docs/api/decorators.md#cookie-combined-example
|
|
3915
|
+
*/
|
|
3916
|
+
it('should combine @Cookie with other parameter decorators', () => {
|
|
3917
|
+
// From docs: combining @Cookie, @Param, @Header, @Query
|
|
3918
|
+
@Controller('/api')
|
|
3919
|
+
class CombinedController extends BaseController {
|
|
3920
|
+
@Get('/users/:id')
|
|
3921
|
+
async getUser(
|
|
3922
|
+
@Param('id') id: string,
|
|
3923
|
+
@Query('fields') fields?: string,
|
|
3924
|
+
@Header('Authorization') auth?: string,
|
|
3925
|
+
@Cookie('session') session?: string,
|
|
3926
|
+
) {
|
|
3927
|
+
return {
|
|
3928
|
+
id, fields, auth, session,
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
expect(CombinedController).toBeDefined();
|
|
3934
|
+
const metadata = getControllerMetadata(CombinedController);
|
|
3935
|
+
expect(metadata!.routes[0].params!.length).toBe(4);
|
|
3936
|
+
});
|
|
3937
|
+
});
|
|
3938
|
+
|
|
3939
|
+
describe('@Req() with OneBunRequest (docs/api/decorators.md)', () => {
|
|
3940
|
+
/**
|
|
3941
|
+
* @source docs/api/decorators.md#req-onebunrequest
|
|
3942
|
+
*/
|
|
3943
|
+
it('should define @Req() handler with OneBunRequest type', () => {
|
|
3944
|
+
// From docs: @Req() with OneBunRequest type
|
|
3945
|
+
// OneBunRequest extends Request with .cookies (CookieMap) and .params
|
|
3946
|
+
@Controller('/api')
|
|
3947
|
+
class ApiController extends BaseController {
|
|
3948
|
+
@Get('/raw')
|
|
3949
|
+
async handleRaw(@Req() req: OneBunRequest) {
|
|
3950
|
+
const url = new URL(req.url);
|
|
3951
|
+
|
|
3952
|
+
// req.cookies is CookieMap, req.params is available from routes API
|
|
3953
|
+
return { url: url.pathname };
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
expect(ApiController).toBeDefined();
|
|
3958
|
+
const metadata = getControllerMetadata(ApiController);
|
|
3959
|
+
expect(metadata!.routes[0].params![0].type).toBe(ParamType.REQUEST);
|
|
3960
|
+
});
|
|
3961
|
+
|
|
3962
|
+
/**
|
|
3963
|
+
* @source docs/api/decorators.md#req-cookies-access
|
|
3964
|
+
*/
|
|
3965
|
+
it('should define handler accessing cookies via req.cookies', () => {
|
|
3966
|
+
// From docs: reading cookies through @Req()
|
|
3967
|
+
@Controller('/api')
|
|
3968
|
+
class ApiController extends BaseController {
|
|
3969
|
+
@Get('/session')
|
|
3970
|
+
async session(@Req() req: OneBunRequest) {
|
|
3971
|
+
// Access cookies via CookieMap
|
|
3972
|
+
const session = req.cookies.get('session');
|
|
3973
|
+
|
|
3974
|
+
return { session };
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
expect(ApiController).toBeDefined();
|
|
3979
|
+
});
|
|
3980
|
+
});
|
|
3981
|
+
|
|
3982
|
+
describe('OneBunRequest and OneBunResponse Types (docs/api/decorators.md)', () => {
|
|
3983
|
+
/**
|
|
3984
|
+
* @source docs/api/decorators.md#onebunrequest-type
|
|
3985
|
+
*/
|
|
3986
|
+
it('should use OneBunRequest as type alias for BunRequest', () => {
|
|
3987
|
+
// OneBunRequest is an alias for BunRequest
|
|
3988
|
+
// It extends standard Request with .cookies and .params
|
|
3989
|
+
const _check: OneBunRequest extends Request ? true : false = true;
|
|
3990
|
+
expect(_check).toBe(true);
|
|
3991
|
+
});
|
|
3992
|
+
|
|
3993
|
+
/**
|
|
3994
|
+
* @source docs/api/decorators.md#onebunresponse-type
|
|
3995
|
+
*/
|
|
3996
|
+
it('should use OneBunResponse as type alias for Response', () => {
|
|
3997
|
+
// OneBunResponse is an alias for Response
|
|
3998
|
+
const response: OneBunResponse = new Response('ok');
|
|
3999
|
+
expect(response).toBeInstanceOf(Response);
|
|
4000
|
+
});
|
|
4001
|
+
});
|
|
4002
|
+
|
|
4003
|
+
describe('Custom Response Headers (docs/api/controllers.md)', () => {
|
|
4004
|
+
/**
|
|
4005
|
+
* @source docs/api/controllers.md#custom-response-headers
|
|
4006
|
+
*/
|
|
4007
|
+
it('should define handler returning Response with custom headers', () => {
|
|
4008
|
+
// From docs: returning Response with custom headers
|
|
4009
|
+
@Controller('/api')
|
|
4010
|
+
class ApiController extends BaseController {
|
|
4011
|
+
@Get('/download')
|
|
4012
|
+
async download() {
|
|
4013
|
+
return new Response(JSON.stringify({ data: 'file content' }), {
|
|
4014
|
+
status: 200,
|
|
4015
|
+
headers: {
|
|
4016
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
4017
|
+
'Content-Type': 'application/json',
|
|
4018
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
4019
|
+
'X-Custom-Header': 'custom-value',
|
|
4020
|
+
},
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
expect(ApiController).toBeDefined();
|
|
4026
|
+
});
|
|
4027
|
+
|
|
4028
|
+
/**
|
|
4029
|
+
* @source docs/api/controllers.md#set-cookie-header
|
|
4030
|
+
*/
|
|
4031
|
+
it('should define handler returning Response with Set-Cookie header', () => {
|
|
4032
|
+
// From docs: setting cookies via Set-Cookie header
|
|
4033
|
+
@Controller('/api')
|
|
4034
|
+
class AuthController extends BaseController {
|
|
4035
|
+
@Post('/login')
|
|
4036
|
+
|
|
4037
|
+
async login(@Body() _body: unknown) {
|
|
4038
|
+
const headers = new Headers();
|
|
4039
|
+
headers.set('Content-Type', 'application/json');
|
|
4040
|
+
headers.append('Set-Cookie', 'session=abc123; Path=/; HttpOnly');
|
|
4041
|
+
headers.append('Set-Cookie', 'theme=dark; Path=/');
|
|
4042
|
+
|
|
4043
|
+
return new Response(JSON.stringify({ loggedIn: true }), {
|
|
4044
|
+
status: 200,
|
|
4045
|
+
headers,
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
expect(AuthController).toBeDefined();
|
|
4051
|
+
});
|
|
4052
|
+
});
|
|
4053
|
+
|
|
4054
|
+
describe('Working with Cookies (docs/api/controllers.md)', () => {
|
|
4055
|
+
/**
|
|
4056
|
+
* @source docs/api/controllers.md#reading-cookies-via-decorator
|
|
4057
|
+
*/
|
|
4058
|
+
it('should define handler reading cookies via @Cookie decorator', () => {
|
|
4059
|
+
// From docs: reading cookies via @Cookie('name')
|
|
4060
|
+
@Controller('/api')
|
|
4061
|
+
class PrefsController extends BaseController {
|
|
4062
|
+
@Get('/preferences')
|
|
4063
|
+
async getPrefs(
|
|
4064
|
+
@Cookie('theme') theme?: string,
|
|
4065
|
+
@Cookie('lang') lang?: string,
|
|
4066
|
+
) {
|
|
4067
|
+
return {
|
|
4068
|
+
theme: theme ?? 'light',
|
|
4069
|
+
lang: lang ?? 'en',
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
expect(PrefsController).toBeDefined();
|
|
4075
|
+
});
|
|
4076
|
+
|
|
4077
|
+
/**
|
|
4078
|
+
* @source docs/api/controllers.md#reading-cookies-via-req
|
|
4079
|
+
*/
|
|
4080
|
+
it('should define handler reading cookies via req.cookies', () => {
|
|
4081
|
+
// From docs: reading cookies through @Req() with req.cookies.get()
|
|
4082
|
+
@Controller('/api')
|
|
4083
|
+
class ApiController extends BaseController {
|
|
4084
|
+
@Get('/session')
|
|
4085
|
+
async session(@Req() req: OneBunRequest) {
|
|
4086
|
+
const session = req.cookies.get('session');
|
|
4087
|
+
|
|
4088
|
+
return { session };
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
|
|
4092
|
+
expect(ApiController).toBeDefined();
|
|
4093
|
+
});
|
|
4094
|
+
|
|
4095
|
+
/**
|
|
4096
|
+
* @source docs/api/controllers.md#setting-cookies-via-req
|
|
4097
|
+
*/
|
|
4098
|
+
it('should define handler setting cookies via req.cookies', () => {
|
|
4099
|
+
// From docs: setting cookies using req.cookies.set()
|
|
4100
|
+
@Controller('/api')
|
|
4101
|
+
class AuthController extends BaseController {
|
|
4102
|
+
@Post('/login')
|
|
4103
|
+
|
|
4104
|
+
async login(@Req() req: OneBunRequest, @Body() _body: unknown) {
|
|
4105
|
+
// Set cookie via CookieMap
|
|
4106
|
+
req.cookies.set('session', 'new-session-id', {
|
|
4107
|
+
httpOnly: true,
|
|
4108
|
+
path: '/',
|
|
4109
|
+
maxAge: 3600,
|
|
4110
|
+
});
|
|
4111
|
+
|
|
4112
|
+
return { loggedIn: true };
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
expect(AuthController).toBeDefined();
|
|
4117
|
+
});
|
|
4118
|
+
|
|
4119
|
+
/**
|
|
4120
|
+
* @source docs/api/controllers.md#deleting-cookies-via-req
|
|
4121
|
+
*/
|
|
4122
|
+
it('should define handler deleting cookies via req.cookies', () => {
|
|
4123
|
+
// From docs: deleting cookies using req.cookies.delete()
|
|
4124
|
+
@Controller('/api')
|
|
4125
|
+
class AuthController extends BaseController {
|
|
4126
|
+
@Post('/logout')
|
|
4127
|
+
async logout(@Req() req: OneBunRequest) {
|
|
4128
|
+
req.cookies.delete('session');
|
|
4129
|
+
|
|
4130
|
+
return { loggedOut: true };
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
expect(AuthController).toBeDefined();
|
|
4135
|
+
});
|
|
4136
|
+
});
|
|
4137
|
+
|
|
4138
|
+
// ============================================================================
|
|
4139
|
+
// File Upload Documentation Tests
|
|
4140
|
+
// ============================================================================
|
|
4141
|
+
|
|
4142
|
+
describe('File Upload API Documentation (docs/api/decorators.md)', () => {
|
|
4143
|
+
/**
|
|
4144
|
+
* @source docs/api/decorators.md#uploadedfile
|
|
4145
|
+
*/
|
|
4146
|
+
describe('Single File Upload (docs/api/decorators.md#uploadedfile)', () => {
|
|
4147
|
+
it('should define controller with @UploadedFile decorator', () => {
|
|
4148
|
+
@Controller('/api/files')
|
|
4149
|
+
class FileController extends BaseController {
|
|
4150
|
+
@Post('/avatar')
|
|
4151
|
+
async uploadAvatar(
|
|
4152
|
+
@UploadedFile('avatar', {
|
|
4153
|
+
maxSize: 5 * 1024 * 1024,
|
|
4154
|
+
mimeTypes: [MimeType.ANY_IMAGE],
|
|
4155
|
+
}) file: OneBunFile,
|
|
4156
|
+
): Promise<Response> {
|
|
4157
|
+
await file.writeTo(`./uploads/${file.name}`);
|
|
4158
|
+
|
|
4159
|
+
return this.success({ filename: file.name, size: file.size });
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
expect(FileController).toBeDefined();
|
|
4164
|
+
const metadata = getControllerMetadata(FileController);
|
|
4165
|
+
expect(metadata).toBeDefined();
|
|
4166
|
+
expect(metadata!.routes).toHaveLength(1);
|
|
4167
|
+
|
|
4168
|
+
const route = metadata!.routes[0];
|
|
4169
|
+
expect(route.params).toBeDefined();
|
|
4170
|
+
expect(route.params!.length).toBe(1);
|
|
4171
|
+
expect(route.params![0].type).toBe(ParamType.FILE);
|
|
4172
|
+
expect(route.params![0].name).toBe('avatar');
|
|
4173
|
+
expect(route.params![0].isRequired).toBe(true);
|
|
4174
|
+
expect(route.params![0].fileOptions).toBeDefined();
|
|
4175
|
+
expect(route.params![0].fileOptions!.maxSize).toBe(5 * 1024 * 1024);
|
|
4176
|
+
expect(route.params![0].fileOptions!.mimeTypes).toEqual([MimeType.ANY_IMAGE]);
|
|
4177
|
+
});
|
|
4178
|
+
});
|
|
4179
|
+
|
|
4180
|
+
/**
|
|
4181
|
+
* @source docs/api/decorators.md#uploadedfiles
|
|
4182
|
+
*/
|
|
4183
|
+
describe('Multiple File Upload (docs/api/decorators.md#uploadedfiles)', () => {
|
|
4184
|
+
it('should define controller with @UploadedFiles decorator', () => {
|
|
4185
|
+
@Controller('/api/files')
|
|
4186
|
+
class FileController extends BaseController {
|
|
4187
|
+
@Post('/documents')
|
|
4188
|
+
async uploadDocs(
|
|
4189
|
+
@UploadedFiles('docs', { maxCount: 10 }) files: OneBunFile[],
|
|
4190
|
+
): Promise<Response> {
|
|
4191
|
+
for (const file of files) {
|
|
4192
|
+
await file.writeTo(`./uploads/${file.name}`);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
return this.success({ count: files.length });
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
expect(FileController).toBeDefined();
|
|
4200
|
+
const metadata = getControllerMetadata(FileController);
|
|
4201
|
+
expect(metadata).toBeDefined();
|
|
4202
|
+
|
|
4203
|
+
const route = metadata!.routes[0];
|
|
4204
|
+
expect(route.params).toBeDefined();
|
|
4205
|
+
expect(route.params![0].type).toBe(ParamType.FILES);
|
|
4206
|
+
expect(route.params![0].name).toBe('docs');
|
|
4207
|
+
expect(route.params![0].fileOptions!.maxCount).toBe(10);
|
|
4208
|
+
});
|
|
4209
|
+
|
|
4210
|
+
it('should support @UploadedFiles without field name (all files)', () => {
|
|
4211
|
+
@Controller('/api/files')
|
|
4212
|
+
class FileController extends BaseController {
|
|
4213
|
+
@Post('/batch')
|
|
4214
|
+
async uploadBatch(
|
|
4215
|
+
@UploadedFiles(undefined, { maxCount: 20 }) files: OneBunFile[],
|
|
4216
|
+
): Promise<Response> {
|
|
4217
|
+
return this.success({ count: files.length });
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
expect(FileController).toBeDefined();
|
|
4222
|
+
const metadata = getControllerMetadata(FileController);
|
|
4223
|
+
const route = metadata!.routes[0];
|
|
4224
|
+
expect(route.params![0].type).toBe(ParamType.FILES);
|
|
4225
|
+
expect(route.params![0].name).toBe('');
|
|
4226
|
+
});
|
|
4227
|
+
});
|
|
4228
|
+
|
|
4229
|
+
/**
|
|
4230
|
+
* @source docs/api/decorators.md#formfield
|
|
4231
|
+
*/
|
|
4232
|
+
describe('Form Field (docs/api/decorators.md#formfield)', () => {
|
|
4233
|
+
it('should define controller with @FormField decorator', () => {
|
|
4234
|
+
@Controller('/api/files')
|
|
4235
|
+
class FileController extends BaseController {
|
|
4236
|
+
@Post('/profile')
|
|
4237
|
+
async createProfile(
|
|
4238
|
+
@UploadedFile('avatar', { mimeTypes: [MimeType.ANY_IMAGE] }) avatar: OneBunFile,
|
|
4239
|
+
@FormField('name', { required: true }) name: string,
|
|
4240
|
+
@FormField('email') email: string,
|
|
4241
|
+
): Promise<Response> {
|
|
4242
|
+
await avatar.writeTo(`./uploads/${avatar.name}`);
|
|
4243
|
+
|
|
4244
|
+
return this.success({ name, email, avatar: avatar.name });
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4248
|
+
expect(FileController).toBeDefined();
|
|
4249
|
+
const metadata = getControllerMetadata(FileController);
|
|
4250
|
+
const route = metadata!.routes[0];
|
|
4251
|
+
expect(route.params).toBeDefined();
|
|
4252
|
+
expect(route.params!.length).toBe(3);
|
|
4253
|
+
|
|
4254
|
+
// @UploadedFile
|
|
4255
|
+
const fileParam = route.params!.find((p) => p.type === ParamType.FILE);
|
|
4256
|
+
expect(fileParam).toBeDefined();
|
|
4257
|
+
expect(fileParam!.name).toBe('avatar');
|
|
4258
|
+
|
|
4259
|
+
// @FormField (required)
|
|
4260
|
+
const nameParam = route.params!.find((p) => p.name === 'name');
|
|
4261
|
+
expect(nameParam).toBeDefined();
|
|
4262
|
+
expect(nameParam!.type).toBe(ParamType.FORM_FIELD);
|
|
4263
|
+
expect(nameParam!.isRequired).toBe(true);
|
|
4264
|
+
|
|
4265
|
+
// @FormField (optional)
|
|
4266
|
+
const emailParam = route.params!.find((p) => p.name === 'email');
|
|
4267
|
+
expect(emailParam).toBeDefined();
|
|
4268
|
+
expect(emailParam!.type).toBe(ParamType.FORM_FIELD);
|
|
4269
|
+
expect(emailParam!.isRequired).toBe(false);
|
|
4270
|
+
});
|
|
4271
|
+
});
|
|
4272
|
+
|
|
4273
|
+
/**
|
|
4274
|
+
* @source docs/api/decorators.md#onebunfile
|
|
4275
|
+
*/
|
|
4276
|
+
describe('OneBunFile Class (docs/api/decorators.md#onebunfile)', () => {
|
|
4277
|
+
it('should create OneBunFile from File and support all methods', async () => {
|
|
4278
|
+
const content = 'test file content';
|
|
4279
|
+
const file = new File([content], 'test.txt', { type: 'text/plain' });
|
|
4280
|
+
const oneBunFile = new OneBunFile(file);
|
|
4281
|
+
|
|
4282
|
+
expect(oneBunFile.name).toBe('test.txt');
|
|
4283
|
+
expect(oneBunFile.size).toBe(content.length);
|
|
4284
|
+
expect(oneBunFile.type).toStartWith('text/plain');
|
|
4285
|
+
|
|
4286
|
+
const base64 = await oneBunFile.toBase64();
|
|
4287
|
+
expect(base64).toBe(btoa(content));
|
|
4288
|
+
|
|
4289
|
+
const buffer = await oneBunFile.toBuffer();
|
|
4290
|
+
expect(buffer.toString()).toBe(content);
|
|
4291
|
+
|
|
4292
|
+
const blob = oneBunFile.toBlob();
|
|
4293
|
+
expect(blob.size).toBe(content.length);
|
|
4294
|
+
});
|
|
4295
|
+
|
|
4296
|
+
it('should create OneBunFile from base64', async () => {
|
|
4297
|
+
const content = 'base64 content';
|
|
4298
|
+
const base64 = btoa(content);
|
|
4299
|
+
const file = OneBunFile.fromBase64(base64, 'decoded.txt', 'text/plain');
|
|
4300
|
+
|
|
4301
|
+
expect(file.name).toBe('decoded.txt');
|
|
4302
|
+
expect(file.type).toStartWith('text/plain');
|
|
4303
|
+
|
|
4304
|
+
const roundTripped = await file.toBase64();
|
|
4305
|
+
expect(roundTripped).toBe(base64);
|
|
4306
|
+
});
|
|
4307
|
+
});
|
|
4308
|
+
|
|
4309
|
+
/**
|
|
4310
|
+
* @source docs/api/decorators.md#mimetype-enum
|
|
4311
|
+
*/
|
|
4312
|
+
describe('MimeType Enum (docs/api/decorators.md#mimetype-enum)', () => {
|
|
4313
|
+
it('should provide common MIME type constants', () => {
|
|
4314
|
+
// Wildcards
|
|
4315
|
+
expect(String(MimeType.ANY)).toBe('*/*');
|
|
4316
|
+
expect(String(MimeType.ANY_IMAGE)).toBe('image/*');
|
|
4317
|
+
expect(String(MimeType.ANY_VIDEO)).toBe('video/*');
|
|
4318
|
+
expect(String(MimeType.ANY_AUDIO)).toBe('audio/*');
|
|
4319
|
+
|
|
4320
|
+
// Specific types
|
|
4321
|
+
expect(String(MimeType.PNG)).toBe('image/png');
|
|
4322
|
+
expect(String(MimeType.PDF)).toBe('application/pdf');
|
|
4323
|
+
expect(String(MimeType.MP4)).toBe('video/mp4');
|
|
4324
|
+
|
|
4325
|
+
// Wildcard matching
|
|
4326
|
+
expect(matchMimeType('image/png', MimeType.ANY_IMAGE)).toBe(true);
|
|
4327
|
+
expect(matchMimeType('video/mp4', MimeType.ANY_IMAGE)).toBe(false);
|
|
4328
|
+
});
|
|
4329
|
+
});
|
|
4330
|
+
|
|
4331
|
+
/**
|
|
4332
|
+
* @source docs/api/decorators.md#json-base64-upload-format
|
|
4333
|
+
*/
|
|
4334
|
+
describe('JSON Base64 Upload (docs/api/decorators.md#json-base64-upload-format)', () => {
|
|
4335
|
+
it('should parse full JSON base64 format', () => {
|
|
4336
|
+
const base64 = btoa('png image data');
|
|
4337
|
+
const file = OneBunFile.fromBase64(base64, 'photo.png', 'image/png');
|
|
4338
|
+
|
|
4339
|
+
expect(file.name).toBe('photo.png');
|
|
4340
|
+
expect(file.type).toBe('image/png');
|
|
4341
|
+
});
|
|
4342
|
+
|
|
4343
|
+
it('should parse data URI format', () => {
|
|
4344
|
+
const base64 = btoa('svg data');
|
|
4345
|
+
const dataUri = `data:image/svg+xml;base64,${base64}`;
|
|
4346
|
+
const file = OneBunFile.fromBase64(dataUri, 'icon.svg');
|
|
4347
|
+
|
|
4348
|
+
expect(file.type).toBe('image/svg+xml');
|
|
4349
|
+
expect(file.name).toBe('icon.svg');
|
|
4350
|
+
});
|
|
4351
|
+
});
|
|
4352
|
+
});
|
|
4353
|
+
|
|
4354
|
+
describe('File Upload API Documentation (docs/api/controllers.md)', () => {
|
|
4355
|
+
/**
|
|
4356
|
+
* @source docs/api/controllers.md#single-file-upload
|
|
4357
|
+
*/
|
|
4358
|
+
it('should define single file upload controller', () => {
|
|
4359
|
+
@Controller('/api/files')
|
|
4360
|
+
class FileController extends BaseController {
|
|
4361
|
+
@Post('/avatar')
|
|
4362
|
+
async uploadAvatar(
|
|
4363
|
+
@UploadedFile('avatar', {
|
|
4364
|
+
maxSize: 5 * 1024 * 1024,
|
|
4365
|
+
mimeTypes: [MimeType.ANY_IMAGE],
|
|
4366
|
+
}) file: OneBunFile,
|
|
4367
|
+
): Promise<Response> {
|
|
4368
|
+
await file.writeTo(`./uploads/${file.name}`);
|
|
4369
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4370
|
+
const _base64 = await file.toBase64();
|
|
4371
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4372
|
+
const _buffer = await file.toBuffer();
|
|
4373
|
+
|
|
4374
|
+
return this.success({
|
|
4375
|
+
filename: file.name,
|
|
4376
|
+
size: file.size,
|
|
4377
|
+
type: file.type,
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
expect(FileController).toBeDefined();
|
|
4383
|
+
});
|
|
4384
|
+
|
|
4385
|
+
/**
|
|
4386
|
+
* @source docs/api/controllers.md#multiple-file-upload
|
|
4387
|
+
*/
|
|
4388
|
+
it('should define multiple file upload controller', () => {
|
|
4389
|
+
@Controller('/api/files')
|
|
4390
|
+
class FileController extends BaseController {
|
|
4391
|
+
@Post('/documents')
|
|
4392
|
+
async uploadDocuments(
|
|
4393
|
+
@UploadedFiles('docs', {
|
|
4394
|
+
maxCount: 10,
|
|
4395
|
+
maxSize: 10 * 1024 * 1024,
|
|
4396
|
+
mimeTypes: [MimeType.PDF, MimeType.DOCX],
|
|
4397
|
+
}) files: OneBunFile[],
|
|
4398
|
+
): Promise<Response> {
|
|
4399
|
+
for (const file of files) {
|
|
4400
|
+
await file.writeTo(`./uploads/${file.name}`);
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
return this.success({ uploaded: files.length });
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
expect(FileController).toBeDefined();
|
|
4408
|
+
});
|
|
4409
|
+
|
|
4410
|
+
/**
|
|
4411
|
+
* @source docs/api/controllers.md#file-with-form-fields
|
|
4412
|
+
*/
|
|
4413
|
+
it('should define file with form fields controller', () => {
|
|
4414
|
+
@Controller('/api/files')
|
|
4415
|
+
class FileController extends BaseController {
|
|
4416
|
+
@Post('/profile')
|
|
4417
|
+
async createProfile(
|
|
4418
|
+
@UploadedFile('avatar', { mimeTypes: [MimeType.ANY_IMAGE] }) avatar: OneBunFile,
|
|
4419
|
+
@FormField('name', { required: true }) name: string,
|
|
4420
|
+
@FormField('email') email: string,
|
|
4421
|
+
): Promise<Response> {
|
|
4422
|
+
await avatar.writeTo(`./uploads/${avatar.name}`);
|
|
4423
|
+
|
|
4424
|
+
return this.success({ name, email, avatar: avatar.name });
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
expect(FileController).toBeDefined();
|
|
4429
|
+
});
|
|
4430
|
+
});
|