@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.
@@ -41,6 +41,85 @@ import type {
41
41
  } from './types';
42
42
  import type { ServerWebSocket } from 'bun';
43
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
+
44
123
 
45
124
  /**
46
125
  * @source docs/index.md#minimal-working-example
@@ -3421,79 +3500,6 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
3421
3500
  // SSE (Server-Sent Events) Documentation Tests
3422
3501
  // ============================================================================
3423
3502
 
3424
- import { Sse, getSseMetadata } from './decorators/decorators';
3425
- import { formatSseEvent, createSseStream } from './module/controller';
3426
-
3427
- import {
3428
- Controller,
3429
- Get,
3430
- Post,
3431
- Put,
3432
- Delete,
3433
- Patch,
3434
- Param,
3435
- Query,
3436
- Body,
3437
- Header,
3438
- Req,
3439
- Cookie,
3440
- Module,
3441
- Service,
3442
- BaseService,
3443
- BaseController,
3444
- UseMiddleware,
3445
- getServiceTag,
3446
- getControllerMetadata,
3447
- HttpStatusCode,
3448
- ParamType,
3449
- NotFoundError,
3450
- InternalServerError,
3451
- OneBunBaseError,
3452
- Env,
3453
- validate,
3454
- validateOrThrow,
3455
- MultiServiceApplication,
3456
- OneBunApplication,
3457
- createServiceDefinition,
3458
- createServiceClient,
3459
- WebSocketGateway,
3460
- BaseWebSocketGateway,
3461
- OnConnect,
3462
- OnDisconnect,
3463
- OnJoinRoom,
3464
- OnLeaveRoom,
3465
- OnMessage,
3466
- Client,
3467
- Socket,
3468
- MessageData,
3469
- RoomName,
3470
- PatternParams,
3471
- WsServer,
3472
- UseWsGuards,
3473
- WsAuthGuard,
3474
- WsPermissionGuard,
3475
- WsAnyPermissionGuard,
3476
- createGuard,
3477
- createInMemoryWsStorage,
3478
- SharedRedisProvider,
3479
- createWsServiceDefinition,
3480
- createWsClient,
3481
- createNativeWsClient,
3482
- matchPattern,
3483
- makeMockLoggerLayer,
3484
- hasOnModuleInit,
3485
- hasOnApplicationInit,
3486
- hasOnModuleDestroy,
3487
- hasBeforeApplicationDestroy,
3488
- hasOnApplicationDestroy,
3489
- callOnModuleInit,
3490
- callOnApplicationInit,
3491
- callOnModuleDestroy,
3492
- callBeforeApplicationDestroy,
3493
- callOnApplicationDestroy,
3494
- } from './';
3495
-
3496
-
3497
3503
  describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)', () => {
3498
3504
  describe('SseEvent Type (docs/api/controllers.md)', () => {
3499
3505
  /**
@@ -4128,3 +4134,297 @@ describe('Working with Cookies (docs/api/controllers.md)', () => {
4128
4134
  expect(AuthController).toBeDefined();
4129
4135
  });
4130
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
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * File Upload Module
3
+ *
4
+ * Provides OneBunFile class, MimeType enum, and file validation helpers.
5
+ */
6
+ export {
7
+ OneBunFile, MimeType, matchMimeType, validateFile, type FileValidationOptions,
8
+ } from './onebun-file';