@onebun/core 0.2.0 → 0.2.2

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.
@@ -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';
@@ -0,0 +1,315 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ } from 'bun:test';
6
+
7
+ import {
8
+ OneBunFile,
9
+ MimeType,
10
+ matchMimeType,
11
+ validateFile,
12
+ } from './onebun-file';
13
+
14
+ // =============================================================================
15
+ // OneBunFile Class Tests
16
+ // =============================================================================
17
+
18
+ describe('OneBunFile', () => {
19
+ describe('constructor', () => {
20
+ it('should create from File object', () => {
21
+ const file = new File(['hello world'], 'test.txt', { type: 'text/plain' });
22
+ const oneBunFile = new OneBunFile(file);
23
+
24
+ expect(oneBunFile.name).toBe('test.txt');
25
+ expect(oneBunFile.size).toBe(11);
26
+ expect(oneBunFile.type).toStartWith('text/plain');
27
+ expect(oneBunFile.lastModified).toBeGreaterThan(0);
28
+ });
29
+
30
+ it('should preserve file metadata', () => {
31
+ const file = new File(['data'], 'image.png', {
32
+ type: 'image/png',
33
+ lastModified: 1700000000000,
34
+ });
35
+ const oneBunFile = new OneBunFile(file);
36
+
37
+ expect(oneBunFile.name).toBe('image.png');
38
+ expect(oneBunFile.type).toBe('image/png');
39
+ expect(oneBunFile.lastModified).toBe(1700000000000);
40
+ });
41
+ });
42
+
43
+ describe('fromBase64', () => {
44
+ it('should create from raw base64 string', () => {
45
+ const base64 = btoa('hello world');
46
+ const file = OneBunFile.fromBase64(base64, 'test.txt', 'text/plain');
47
+
48
+ expect(file.name).toBe('test.txt');
49
+ expect(file.size).toBe(11);
50
+ expect(file.type).toStartWith('text/plain');
51
+ });
52
+
53
+ it('should create with default name and mime type', () => {
54
+ const base64 = btoa('data');
55
+ const file = OneBunFile.fromBase64(base64);
56
+
57
+ expect(file.name).toBe('upload');
58
+ expect(file.type).toBe('application/octet-stream');
59
+ });
60
+
61
+ it('should handle data URI format', () => {
62
+ const base64 = btoa('png-data');
63
+ const dataUri = `data:image/png;base64,${base64}`;
64
+ const file = OneBunFile.fromBase64(dataUri, 'photo.png');
65
+
66
+ expect(file.type).toBe('image/png');
67
+ expect(file.name).toBe('photo.png');
68
+ });
69
+
70
+ it('should detect mime type from data URI even when explicit type is given', () => {
71
+ const base64 = btoa('svg-data');
72
+ const dataUri = `data:image/svg+xml;base64,${base64}`;
73
+ // Data URI mime type should override the explicit one
74
+ const file = OneBunFile.fromBase64(dataUri, 'image.svg', 'text/plain');
75
+
76
+ expect(file.type).toBe('image/svg+xml');
77
+ });
78
+ });
79
+
80
+ describe('toBase64', () => {
81
+ it('should convert file to base64', async () => {
82
+ const content = 'hello world';
83
+ const file = new File([content], 'test.txt', { type: 'text/plain' });
84
+ const oneBunFile = new OneBunFile(file);
85
+
86
+ const base64 = await oneBunFile.toBase64();
87
+ expect(base64).toBe(btoa(content));
88
+ });
89
+
90
+ it('should cache base64 result', async () => {
91
+ const file = new File(['test'], 'test.txt', { type: 'text/plain' });
92
+ const oneBunFile = new OneBunFile(file);
93
+
94
+ const first = await oneBunFile.toBase64();
95
+ const second = await oneBunFile.toBase64();
96
+ expect(first).toBe(second);
97
+ });
98
+
99
+ it('should return cached base64 for fromBase64 files', async () => {
100
+ const original = btoa('cached data');
101
+ const file = OneBunFile.fromBase64(original, 'test.txt');
102
+
103
+ const result = await file.toBase64();
104
+ expect(result).toBe(original);
105
+ });
106
+ });
107
+
108
+ describe('toBuffer', () => {
109
+ it('should convert file to Buffer', async () => {
110
+ const content = 'buffer test';
111
+ const file = new File([content], 'test.txt', { type: 'text/plain' });
112
+ const oneBunFile = new OneBunFile(file);
113
+
114
+ const buffer = await oneBunFile.toBuffer();
115
+ expect(buffer).toBeInstanceOf(Buffer);
116
+ expect(buffer.toString()).toBe(content);
117
+ });
118
+ });
119
+
120
+ describe('toArrayBuffer', () => {
121
+ it('should convert file to ArrayBuffer', async () => {
122
+ const content = 'arraybuffer test';
123
+ const file = new File([content], 'test.txt', { type: 'text/plain' });
124
+ const oneBunFile = new OneBunFile(file);
125
+
126
+ const ab = await oneBunFile.toArrayBuffer();
127
+ expect(ab).toBeInstanceOf(ArrayBuffer);
128
+ expect(ab.byteLength).toBe(content.length);
129
+ });
130
+ });
131
+
132
+ describe('toBlob', () => {
133
+ it('should return underlying Blob', () => {
134
+ const file = new File(['blob test'], 'test.txt', { type: 'text/plain' });
135
+ const oneBunFile = new OneBunFile(file);
136
+
137
+ const blob = oneBunFile.toBlob();
138
+ expect(blob).toBeInstanceOf(Blob);
139
+ expect(blob.size).toBe(9);
140
+ });
141
+ });
142
+ });
143
+
144
+ // =============================================================================
145
+ // MimeType Enum Tests
146
+ // =============================================================================
147
+
148
+ describe('MimeType', () => {
149
+ it('should have wildcard values', () => {
150
+ expect(String(MimeType.ANY)).toBe('*/*');
151
+ expect(String(MimeType.ANY_IMAGE)).toBe('image/*');
152
+ expect(String(MimeType.ANY_VIDEO)).toBe('video/*');
153
+ expect(String(MimeType.ANY_AUDIO)).toBe('audio/*');
154
+ expect(String(MimeType.ANY_TEXT)).toBe('text/*');
155
+ expect(String(MimeType.ANY_APPLICATION)).toBe('application/*');
156
+ });
157
+
158
+ it('should have image types', () => {
159
+ expect(String(MimeType.PNG)).toBe('image/png');
160
+ expect(String(MimeType.JPEG)).toBe('image/jpeg');
161
+ expect(String(MimeType.GIF)).toBe('image/gif');
162
+ expect(String(MimeType.WEBP)).toBe('image/webp');
163
+ expect(String(MimeType.SVG)).toBe('image/svg+xml');
164
+ expect(String(MimeType.AVIF)).toBe('image/avif');
165
+ });
166
+
167
+ it('should have document types', () => {
168
+ expect(String(MimeType.PDF)).toBe('application/pdf');
169
+ expect(String(MimeType.JSON)).toBe('application/json');
170
+ expect(String(MimeType.CSV)).toBe('text/csv');
171
+ expect(String(MimeType.ZIP)).toBe('application/zip');
172
+ });
173
+
174
+ it('should have media types', () => {
175
+ expect(String(MimeType.MP4)).toBe('video/mp4');
176
+ expect(String(MimeType.MP3)).toBe('audio/mpeg');
177
+ expect(String(MimeType.WAV)).toBe('audio/wav');
178
+ });
179
+
180
+ it('should have text types', () => {
181
+ expect(String(MimeType.PLAIN)).toBe('text/plain');
182
+ expect(String(MimeType.HTML)).toBe('text/html');
183
+ expect(String(MimeType.CSS)).toBe('text/css');
184
+ });
185
+
186
+ it('should have binary type', () => {
187
+ expect(String(MimeType.OCTET_STREAM)).toBe('application/octet-stream');
188
+ });
189
+ });
190
+
191
+ // =============================================================================
192
+ // matchMimeType Tests
193
+ // =============================================================================
194
+
195
+ describe('matchMimeType', () => {
196
+ it('should match exact MIME types', () => {
197
+ expect(matchMimeType('image/png', 'image/png')).toBe(true);
198
+ expect(matchMimeType('text/plain', 'text/plain')).toBe(true);
199
+ expect(matchMimeType('application/pdf', 'application/pdf')).toBe(true);
200
+ });
201
+
202
+ it('should not match different MIME types', () => {
203
+ expect(matchMimeType('image/png', 'image/jpeg')).toBe(false);
204
+ expect(matchMimeType('text/plain', 'text/html')).toBe(false);
205
+ expect(matchMimeType('image/png', 'video/mp4')).toBe(false);
206
+ });
207
+
208
+ it('should match wildcard subtypes', () => {
209
+ expect(matchMimeType('image/png', 'image/*')).toBe(true);
210
+ expect(matchMimeType('image/jpeg', 'image/*')).toBe(true);
211
+ expect(matchMimeType('image/webp', 'image/*')).toBe(true);
212
+ expect(matchMimeType('video/mp4', 'video/*')).toBe(true);
213
+ expect(matchMimeType('audio/mpeg', 'audio/*')).toBe(true);
214
+ });
215
+
216
+ it('should not match wrong type with wildcard', () => {
217
+ expect(matchMimeType('video/mp4', 'image/*')).toBe(false);
218
+ expect(matchMimeType('text/plain', 'image/*')).toBe(false);
219
+ });
220
+
221
+ it('should match universal wildcard', () => {
222
+ expect(matchMimeType('image/png', '*/*')).toBe(true);
223
+ expect(matchMimeType('video/mp4', '*/*')).toBe(true);
224
+ expect(matchMimeType('application/json', '*/*')).toBe(true);
225
+ });
226
+
227
+ it('should work with MimeType enum values', () => {
228
+ expect(matchMimeType('image/png', MimeType.ANY_IMAGE)).toBe(true);
229
+ expect(matchMimeType('image/png', MimeType.PNG)).toBe(true);
230
+ expect(matchMimeType('image/png', MimeType.JPEG)).toBe(false);
231
+ expect(matchMimeType('application/pdf', MimeType.ANY_APPLICATION)).toBe(true);
232
+ });
233
+ });
234
+
235
+ // =============================================================================
236
+ // validateFile Tests
237
+ // =============================================================================
238
+
239
+ describe('validateFile', () => {
240
+ it('should pass validation when no constraints', () => {
241
+ const file = new File(['data'], 'test.txt', { type: 'text/plain' });
242
+ const oneBunFile = new OneBunFile(file);
243
+
244
+ expect(() => validateFile(oneBunFile, {})).not.toThrow();
245
+ });
246
+
247
+ it('should pass when file is within size limit', () => {
248
+ const file = new File(['small'], 'test.txt', { type: 'text/plain' });
249
+ const oneBunFile = new OneBunFile(file);
250
+
251
+ expect(() => validateFile(oneBunFile, { maxSize: 1024 })).not.toThrow();
252
+ });
253
+
254
+ it('should throw when file exceeds size limit', () => {
255
+ const file = new File(['x'.repeat(100)], 'test.txt', { type: 'text/plain' });
256
+ const oneBunFile = new OneBunFile(file);
257
+
258
+ expect(() => validateFile(oneBunFile, { maxSize: 10 })).toThrow(
259
+ /exceeds maximum size/,
260
+ );
261
+ });
262
+
263
+ it('should include field name in size error', () => {
264
+ const file = new File(['x'.repeat(100)], 'test.txt', { type: 'text/plain' });
265
+ const oneBunFile = new OneBunFile(file);
266
+
267
+ expect(() => validateFile(oneBunFile, { maxSize: 10 }, 'avatar')).toThrow(
268
+ /File "avatar" exceeds maximum size/,
269
+ );
270
+ });
271
+
272
+ it('should pass when MIME type matches', () => {
273
+ const file = new File(['data'], 'test.png', { type: 'image/png' });
274
+ const oneBunFile = new OneBunFile(file);
275
+
276
+ expect(() => validateFile(oneBunFile, { mimeTypes: ['image/*'] })).not.toThrow();
277
+ });
278
+
279
+ it('should pass when MIME type matches one of multiple', () => {
280
+ const file = new File(['data'], 'test.png', { type: 'image/png' });
281
+ const oneBunFile = new OneBunFile(file);
282
+
283
+ expect(() =>
284
+ validateFile(oneBunFile, { mimeTypes: ['application/pdf', 'image/png'] }),
285
+ ).not.toThrow();
286
+ });
287
+
288
+ it('should throw when MIME type does not match', () => {
289
+ const file = new File(['data'], 'test.txt', { type: 'text/plain' });
290
+ const oneBunFile = new OneBunFile(file);
291
+
292
+ expect(() => validateFile(oneBunFile, { mimeTypes: ['image/*'] })).toThrow(
293
+ /invalid MIME type/,
294
+ );
295
+ });
296
+
297
+ it('should include field name in MIME error', () => {
298
+ const file = new File(['data'], 'test.txt', { type: 'text/plain' });
299
+ const oneBunFile = new OneBunFile(file);
300
+
301
+ expect(() =>
302
+ validateFile(oneBunFile, { mimeTypes: ['image/*'] }, 'document'),
303
+ ).toThrow(/File "document" has invalid MIME type/);
304
+ });
305
+
306
+ it('should validate both size and MIME type', () => {
307
+ const file = new File(['x'.repeat(100)], 'big.txt', { type: 'text/plain' });
308
+ const oneBunFile = new OneBunFile(file);
309
+
310
+ // Fails on size first
311
+ expect(() =>
312
+ validateFile(oneBunFile, { maxSize: 10, mimeTypes: ['image/*'] }),
313
+ ).toThrow(/exceeds maximum size/);
314
+ });
315
+ });
@@ -0,0 +1,304 @@
1
+ /**
2
+ * OneBunFile - Unified file wrapper for file uploads
3
+ *
4
+ * Provides a consistent interface for files uploaded via:
5
+ * - multipart/form-data (standard file upload)
6
+ * - JSON body with base64-encoded data
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // Access file metadata
11
+ * console.log(file.name); // "photo.png"
12
+ * console.log(file.size); // 1024
13
+ * console.log(file.type); // "image/png"
14
+ *
15
+ * // Convert to base64
16
+ * const base64 = await file.toBase64();
17
+ *
18
+ * // Write to disk
19
+ * await file.writeTo('./uploads/photo.png');
20
+ *
21
+ * // Get as Buffer
22
+ * const buffer = await file.toBuffer();
23
+ * ```
24
+ */
25
+ export class OneBunFile {
26
+ /** File name (e.g., "photo.png") */
27
+ readonly name: string;
28
+ /** File size in bytes */
29
+ readonly size: number;
30
+ /** MIME type (e.g., "image/png") */
31
+ readonly type: string;
32
+ /** Last modified timestamp */
33
+ readonly lastModified: number;
34
+
35
+ private readonly _blob: Blob;
36
+ private _base64Cache?: string;
37
+
38
+ /**
39
+ * Create from a Web API File (typically from multipart/form-data)
40
+ */
41
+ constructor(file: File) {
42
+ this.name = file.name;
43
+ this.size = file.size;
44
+ this.type = file.type;
45
+ this.lastModified = file.lastModified;
46
+ this._blob = file;
47
+ }
48
+
49
+ /**
50
+ * Create OneBunFile from a base64-encoded string
51
+ *
52
+ * @param data - Base64-encoded file data (with or without data URI prefix)
53
+ * @param filename - File name (default: "upload")
54
+ * @param mimeType - MIME type (default: "application/octet-stream")
55
+ * @returns OneBunFile instance
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // From raw base64
60
+ * const file = OneBunFile.fromBase64('iVBORw0KGgo...', 'photo.png', 'image/png');
61
+ *
62
+ * // From data URI
63
+ * const file = OneBunFile.fromBase64('data:image/png;base64,iVBORw0KGgo...', 'photo.png');
64
+ * ```
65
+ */
66
+ static fromBase64(
67
+ data: string,
68
+ filename: string = 'upload',
69
+ mimeType: string = MimeType.OCTET_STREAM,
70
+ ): OneBunFile {
71
+ // Handle data URI format: "data:image/png;base64,iVBORw0KGgo..."
72
+ let base64Data = data;
73
+ let detectedMimeType = mimeType;
74
+
75
+ const dataUriMatch = data.match(/^data:([^;]+);base64,(.+)$/);
76
+ if (dataUriMatch) {
77
+ detectedMimeType = dataUriMatch[1];
78
+ base64Data = dataUriMatch[2];
79
+ }
80
+
81
+ const binaryString = atob(base64Data);
82
+ const bytes = new Uint8Array(binaryString.length);
83
+ for (let i = 0; i < binaryString.length; i++) {
84
+ bytes[i] = binaryString.charCodeAt(i);
85
+ }
86
+
87
+ const file = new File([bytes], filename, {
88
+ type: detectedMimeType,
89
+ lastModified: Date.now(),
90
+ });
91
+
92
+ const oneBunFile = new OneBunFile(file);
93
+ // Cache the base64 since we already have it
94
+ oneBunFile._base64Cache = base64Data;
95
+
96
+ return oneBunFile;
97
+ }
98
+
99
+ /**
100
+ * Convert file content to a base64-encoded string.
101
+ * Result is cached after the first call.
102
+ *
103
+ * @returns Base64-encoded string (without data URI prefix)
104
+ */
105
+ async toBase64(): Promise<string> {
106
+ if (this._base64Cache) {
107
+ return this._base64Cache;
108
+ }
109
+
110
+ const arrayBuffer = await this._blob.arrayBuffer();
111
+ const bytes = new Uint8Array(arrayBuffer);
112
+ let binary = '';
113
+ for (let i = 0; i < bytes.length; i++) {
114
+ binary += String.fromCharCode(bytes[i]);
115
+ }
116
+ this._base64Cache = btoa(binary);
117
+
118
+ return this._base64Cache;
119
+ }
120
+
121
+ /**
122
+ * Convert file content to a Buffer
123
+ */
124
+ async toBuffer(): Promise<Buffer> {
125
+ const arrayBuffer = await this._blob.arrayBuffer();
126
+
127
+ return Buffer.from(arrayBuffer);
128
+ }
129
+
130
+ /**
131
+ * Convert file content to an ArrayBuffer
132
+ */
133
+ async toArrayBuffer(): Promise<ArrayBuffer> {
134
+ return await this._blob.arrayBuffer();
135
+ }
136
+
137
+ /**
138
+ * Get the underlying Blob
139
+ */
140
+ toBlob(): Blob {
141
+ return this._blob;
142
+ }
143
+
144
+ /**
145
+ * Write file to disk using Bun.write()
146
+ *
147
+ * @param path - Destination file path
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * await file.writeTo('./uploads/photo.png');
152
+ * await file.writeTo(`./uploads/${file.name}`);
153
+ * ```
154
+ */
155
+ async writeTo(path: string): Promise<void> {
156
+ await Bun.write(path, this._blob);
157
+ }
158
+ }
159
+
160
+ // =============================================================================
161
+ // MimeType Enum
162
+ // =============================================================================
163
+
164
+ /**
165
+ * Common MIME types for use with file upload decorators.
166
+ * Includes wildcard groups for matching entire categories.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * // Accept only images
171
+ * \@UploadedFile('avatar', { mimeTypes: [MimeType.ANY_IMAGE] })
172
+ *
173
+ * // Accept specific types
174
+ * \@UploadedFile('avatar', { mimeTypes: [MimeType.PNG, MimeType.JPEG, MimeType.WEBP] })
175
+ *
176
+ * // Mix enum values and string literals
177
+ * \@UploadedFile('doc', { mimeTypes: [MimeType.PDF, 'application/msword'] })
178
+ * ```
179
+ */
180
+ export enum MimeType {
181
+ // Wildcard groups
182
+ ANY = '*/*',
183
+ ANY_IMAGE = 'image/*',
184
+ ANY_VIDEO = 'video/*',
185
+ ANY_AUDIO = 'audio/*',
186
+ ANY_TEXT = 'text/*',
187
+ ANY_APPLICATION = 'application/*',
188
+
189
+ // Images
190
+ PNG = 'image/png',
191
+ JPEG = 'image/jpeg',
192
+ GIF = 'image/gif',
193
+ WEBP = 'image/webp',
194
+ SVG = 'image/svg+xml',
195
+ ICO = 'image/x-icon',
196
+ BMP = 'image/bmp',
197
+ AVIF = 'image/avif',
198
+
199
+ // Documents
200
+ PDF = 'application/pdf',
201
+
202
+ JSON = 'application/json',
203
+ XML = 'application/xml',
204
+ ZIP = 'application/zip',
205
+ GZIP = 'application/gzip',
206
+ CSV = 'text/csv',
207
+ XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
208
+ DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
209
+
210
+ // Video
211
+ MP4 = 'video/mp4',
212
+ WEBM = 'video/webm',
213
+
214
+ // Audio
215
+ MP3 = 'audio/mpeg',
216
+ WAV = 'audio/wav',
217
+ OGG = 'audio/ogg',
218
+
219
+ // Text
220
+ PLAIN = 'text/plain',
221
+ HTML = 'text/html',
222
+ CSS = 'text/css',
223
+ JAVASCRIPT = 'text/javascript',
224
+
225
+ // Binary
226
+ OCTET_STREAM = 'application/octet-stream',
227
+ }
228
+
229
+ // =============================================================================
230
+ // File Validation Helpers
231
+ // =============================================================================
232
+
233
+ /**
234
+ * Check if a MIME type matches a pattern (supports wildcards).
235
+ *
236
+ * @param actual - The actual MIME type of the file (e.g., "image/png")
237
+ * @param pattern - The pattern to match against (e.g., "image/*", "image/png", "*\/*")
238
+ * @returns true if the actual MIME type matches the pattern
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * matchMimeType('image/png', 'image/*') // true
243
+ * matchMimeType('image/png', 'image/png') // true
244
+ * matchMimeType('image/png', 'video/*') // false
245
+ * matchMimeType('image/png', '*\/*') // true
246
+ * ```
247
+ */
248
+ export function matchMimeType(actual: string, pattern: string): boolean {
249
+ if (pattern === '*/*') {
250
+ return true;
251
+ }
252
+
253
+ const [patternType, patternSubtype] = pattern.split('/');
254
+ const [actualType, actualSubtype] = actual.split('/');
255
+
256
+ if (patternSubtype === '*') {
257
+ return patternType === actualType;
258
+ }
259
+
260
+ return patternType === actualType && patternSubtype === actualSubtype;
261
+ }
262
+
263
+ /**
264
+ * Options for file validation
265
+ */
266
+ export interface FileValidationOptions {
267
+ /** Maximum file size in bytes */
268
+ maxSize?: number;
269
+ /** Allowed MIME types (supports wildcards like 'image/*') */
270
+ mimeTypes?: string[];
271
+ }
272
+
273
+ /**
274
+ * Validate a file against size and MIME type constraints.
275
+ * Throws an Error with a descriptive message if validation fails.
276
+ *
277
+ * @param file - The OneBunFile to validate
278
+ * @param options - Validation options
279
+ * @param fieldName - Field name for error messages
280
+ */
281
+ export function validateFile(
282
+ file: OneBunFile,
283
+ options: FileValidationOptions,
284
+ fieldName?: string,
285
+ ): void {
286
+ const prefix = fieldName ? `File "${fieldName}"` : 'File';
287
+
288
+ // Validate file size
289
+ if (options.maxSize !== undefined && file.size > options.maxSize) {
290
+ throw new Error(
291
+ `${prefix} exceeds maximum size. Got ${file.size} bytes, max is ${options.maxSize} bytes`,
292
+ );
293
+ }
294
+
295
+ // Validate MIME type
296
+ if (options.mimeTypes && options.mimeTypes.length > 0) {
297
+ const matches = options.mimeTypes.some((pattern) => matchMimeType(file.type, pattern));
298
+ if (!matches) {
299
+ throw new Error(
300
+ `${prefix} has invalid MIME type "${file.type}". Allowed: ${options.mimeTypes.join(', ')}`,
301
+ );
302
+ }
303
+ }
304
+ }
package/src/index.ts CHANGED
@@ -36,8 +36,14 @@ export {
36
36
  type ParamDecoratorOptions,
37
37
  type ParamMetadata,
38
38
  type ResponseSchemaMetadata,
39
+ type RouteOptions,
39
40
  type RouteMetadata,
40
41
  type ControllerMetadata,
42
+ type MiddlewareClass,
43
+ type OnModuleConfigure,
44
+ // File upload types
45
+ type FileUploadOptions,
46
+ type FilesUploadOptions,
41
47
  // WebSocket types are exported from ./websocket
42
48
  type WsStorageType,
43
49
  type WsStorageOptions,
@@ -53,6 +59,11 @@ export {
53
59
  // Decorators and Metadata (exports Controller decorator, Module decorator, etc.)
54
60
  export * from './decorators';
55
61
 
62
+ // File Upload (OneBunFile class, MimeType enum, helpers)
63
+ export {
64
+ OneBunFile, MimeType, matchMimeType, validateFile,
65
+ } from './file';
66
+
56
67
  // Module System - explicitly re-export to avoid Controller conflict
57
68
  export {
58
69
  OneBunModule,
@@ -78,6 +89,8 @@ export {
78
89
  type OnModuleDestroy,
79
90
  type BeforeApplicationDestroy,
80
91
  type OnApplicationDestroy,
92
+ // Middleware
93
+ BaseMiddleware,
81
94
  // Lifecycle hooks helper functions
82
95
  hasOnModuleInit,
83
96
  hasOnApplicationInit,
@@ -89,6 +102,13 @@ export {
89
102
  callOnModuleDestroy,
90
103
  callBeforeApplicationDestroy,
91
104
  callOnApplicationDestroy,
105
+ // SSE helpers
106
+ formatSseEvent,
107
+ createSseStream,
108
+ // Server & SSE default constants
109
+ DEFAULT_IDLE_TIMEOUT,
110
+ DEFAULT_SSE_HEARTBEAT_MS,
111
+ DEFAULT_SSE_TIMEOUT,
92
112
  } from './module';
93
113
 
94
114
  // Application