bunsane 0.1.0 → 0.1.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.
Files changed (82) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +168 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +193 -14
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +503 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +89 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +5 -5
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/gql/Generator.ts +58 -35
  52. package/gql/decorators/Upload.ts +176 -0
  53. package/gql/helpers.ts +67 -0
  54. package/gql/index.ts +65 -31
  55. package/gql/types.ts +1 -1
  56. package/index.ts +79 -11
  57. package/package.json +19 -10
  58. package/rest/Generator.ts +3 -0
  59. package/rest/index.ts +22 -0
  60. package/service/Service.ts +1 -1
  61. package/service/ServiceRegistry.ts +10 -6
  62. package/service/index.ts +12 -1
  63. package/tests/bench/insert.bench.ts +59 -0
  64. package/tests/bench/relations.bench.ts +269 -0
  65. package/tests/bench/sorting.bench.ts +415 -0
  66. package/tests/component-hooks.test.ts +1409 -0
  67. package/tests/component.test.ts +338 -0
  68. package/tests/errorHandling.test.ts +155 -0
  69. package/tests/hooks.test.ts +666 -0
  70. package/tests/query-sorting.test.ts +101 -0
  71. package/tests/relations.test.ts +169 -0
  72. package/tests/scheduler.test.ts +724 -0
  73. package/tsconfig.json +35 -34
  74. package/types/graphql.types.ts +87 -0
  75. package/types/hooks.types.ts +141 -0
  76. package/types/scheduler.types.ts +165 -0
  77. package/types/upload.types.ts +184 -0
  78. package/upload/index.ts +140 -0
  79. package/utils/UploadHelper.ts +305 -0
  80. package/utils/cronParser.ts +366 -0
  81. package/utils/errorMessages.ts +151 -0
  82. package/core/Events.ts +0 -0
@@ -0,0 +1,83 @@
1
+ import { ScheduleInterval } from "../../types/scheduler.types";
2
+ import type { ScheduledTaskOptions } from "../../types/scheduler.types";
3
+ import { SchedulerManager } from "../SchedulerManager";
4
+ import { logger } from "../Logger";
5
+
6
+ const loggerInstance = logger.child({ scope: "ScheduledTaskDecorator" });
7
+
8
+ /**
9
+ * Decorator for registering scheduled tasks
10
+ * @param options Task configuration options including interval and component target
11
+ */
12
+ export function ScheduledTask(
13
+ options: ScheduledTaskOptions & {
14
+ interval: ScheduleInterval;
15
+ componentTarget?: new (...args: any[]) => any
16
+ }
17
+ ) {
18
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
19
+ const originalMethod = descriptor.value;
20
+
21
+ // Generate task ID if not provided
22
+ const taskId = options.id || `${target.constructor.name}.${propertyKey}`;
23
+
24
+ // Store task info for later registration
25
+ if (!target.constructor.__scheduledTasks) {
26
+ target.constructor.__scheduledTasks = [];
27
+ }
28
+
29
+ const taskInfo = {
30
+ id: taskId,
31
+ name: options.name || `${target.constructor.name}.${propertyKey}`,
32
+ componentTarget: options.componentTarget, // Legacy support
33
+ interval: options.interval,
34
+ options: {
35
+ runOnStart: options.runOnStart ?? false,
36
+ timeout: options.timeout ?? 30000,
37
+ enableLogging: options.enableLogging ?? true,
38
+ ...options
39
+ },
40
+ service: null, // Will be set when service is instantiated
41
+ methodName: propertyKey,
42
+ nextExecution: new Date(),
43
+ executionCount: 0,
44
+ isRunning: false,
45
+ enabled: true
46
+ };
47
+
48
+ target.constructor.__scheduledTasks.push(taskInfo);
49
+
50
+ // Return the original descriptor to maintain method functionality
51
+ return descriptor;
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Function to manually register decorated tasks for a service instance
57
+ * This is useful when services are instantiated outside the normal decorator flow
58
+ */
59
+ export function registerScheduledTasks(service: any): void {
60
+ const constructor = service.constructor;
61
+
62
+ if (!constructor.__scheduledTasks) {
63
+ return;
64
+ }
65
+
66
+ const scheduler = SchedulerManager.getInstance();
67
+
68
+ for (const task of constructor.__scheduledTasks) {
69
+ const taskWithService = {
70
+ ...task,
71
+ service: service
72
+ };
73
+
74
+ try {
75
+ scheduler.registerTask(taskWithService);
76
+ if (loggerInstance.isLevelEnabled('info')) {
77
+ loggerInstance.info(`Manually registered scheduled task: ${task.name} (${task.id})`);
78
+ }
79
+ } catch (error) {
80
+ loggerInstance.error(`Failed to manually register scheduled task ${task.name}: ${error instanceof Error ? error.message : String(error)}`);
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,177 @@
1
+ import type { Entity } from "../Entity";
2
+ import type { BaseComponent } from "../Components";
3
+
4
+ /**
5
+ * Base class for all entity lifecycle events
6
+ * Provides common properties and methods for lifecycle events
7
+ */
8
+ export abstract class EntityLifecycleEvent {
9
+ public readonly timestamp: Date;
10
+ public readonly entity: Entity;
11
+ public readonly eventType: string;
12
+
13
+ constructor(entity: Entity, eventType: string) {
14
+ this.timestamp = new Date();
15
+ this.entity = entity;
16
+ this.eventType = eventType;
17
+ }
18
+
19
+ /**
20
+ * Get the event type identifier
21
+ */
22
+ public getEventType(): string {
23
+ return this.eventType;
24
+ }
25
+
26
+ /**
27
+ * Get the entity associated with this event
28
+ */
29
+ public getEntity(): Entity {
30
+ return this.entity;
31
+ }
32
+
33
+ /**
34
+ * Get the timestamp when the event occurred
35
+ */
36
+ public getTimestamp(): Date {
37
+ return this.timestamp;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Event fired when an entity is created (first time saved)
43
+ */
44
+ export class EntityCreatedEvent extends EntityLifecycleEvent {
45
+ public readonly isNew: boolean = true;
46
+
47
+ constructor(entity: Entity) {
48
+ super(entity, "entity.created");
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Event fired when an entity is updated (subsequent saves)
54
+ */
55
+ export class EntityUpdatedEvent extends EntityLifecycleEvent {
56
+ public readonly isNew: boolean = false;
57
+ public readonly changedComponents: string[] = [];
58
+
59
+ constructor(entity: Entity, changedComponents: string[] = []) {
60
+ super(entity, "entity.updated");
61
+ this.changedComponents = changedComponents;
62
+ }
63
+
64
+ /**
65
+ * Get the list of component types that were changed
66
+ */
67
+ public getChangedComponents(): string[] {
68
+ return this.changedComponents;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Event fired when an entity is deleted
74
+ */
75
+ export class EntityDeletedEvent extends EntityLifecycleEvent {
76
+ public readonly isSoftDelete: boolean = true;
77
+
78
+ constructor(entity: Entity, isSoftDelete: boolean = true) {
79
+ super(entity, "entity.deleted");
80
+ this.isSoftDelete = isSoftDelete;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Base class for component lifecycle events
86
+ */
87
+ export abstract class ComponentLifecycleEvent extends EntityLifecycleEvent {
88
+ public readonly component: BaseComponent;
89
+ public readonly componentType: string;
90
+
91
+ constructor(entity: Entity, component: BaseComponent, eventType: string) {
92
+ super(entity, eventType);
93
+ this.component = component;
94
+ this.componentType = component.getTypeID();
95
+ }
96
+
97
+ /**
98
+ * Get the component associated with this event
99
+ */
100
+ public getComponent(): BaseComponent {
101
+ return this.component;
102
+ }
103
+
104
+ /**
105
+ * Get the component type identifier
106
+ */
107
+ public getComponentType(): string {
108
+ return this.componentType;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Event fired when a component is added to an entity
114
+ */
115
+ export class ComponentAddedEvent extends ComponentLifecycleEvent {
116
+ constructor(entity: Entity, component: BaseComponent) {
117
+ super(entity, component, "component.added");
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Event fired when a component is updated on an entity
123
+ */
124
+ export class ComponentUpdatedEvent extends ComponentLifecycleEvent {
125
+ public readonly oldData?: any;
126
+ public readonly newData?: any;
127
+
128
+ constructor(entity: Entity, component: BaseComponent, oldData?: any, newData?: any) {
129
+ super(entity, component, "component.updated");
130
+ this.oldData = oldData;
131
+ this.newData = newData;
132
+ }
133
+
134
+ /**
135
+ * Get the old component data before the update
136
+ */
137
+ public getOldData(): any {
138
+ return this.oldData;
139
+ }
140
+
141
+ /**
142
+ * Get the new component data after the update
143
+ */
144
+ public getNewData(): any {
145
+ return this.newData;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Event fired when a component is removed from an entity
151
+ */
152
+ export class ComponentRemovedEvent extends ComponentLifecycleEvent {
153
+ constructor(entity: Entity, component: BaseComponent) {
154
+ super(entity, component, "component.removed");
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Union type for all entity lifecycle events
160
+ */
161
+ export type EntityEvent =
162
+ | EntityCreatedEvent
163
+ | EntityUpdatedEvent
164
+ | EntityDeletedEvent;
165
+
166
+ /**
167
+ * Union type for all component lifecycle events
168
+ */
169
+ export type ComponentEvent =
170
+ | ComponentAddedEvent
171
+ | ComponentUpdatedEvent
172
+ | ComponentRemovedEvent;
173
+
174
+ /**
175
+ * Union type for all lifecycle events
176
+ */
177
+ export type LifecycleEvent = EntityEvent | ComponentEvent;
@@ -0,0 +1,423 @@
1
+ import type { ImageProcessingOptions } from "../../types/upload.types";
2
+ import { logger as MainLogger } from "../Logger";
3
+
4
+ const logger = MainLogger.child({ scope: "ImageProcessor" });
5
+
6
+ /**
7
+ * ImageProcessor - Handle image manipulation and processing
8
+ * Note: This is a basic implementation. For production use, consider integrating
9
+ * with Sharp, Jimp, or similar image processing libraries.
10
+ */
11
+ export class ImageProcessor {
12
+ /**
13
+ * Process image according to configuration
14
+ */
15
+ public static async processImage(
16
+ file: File,
17
+ options: ImageProcessingOptions
18
+ ): Promise<{
19
+ processedFile: File;
20
+ metadata: {
21
+ width: number;
22
+ height: number;
23
+ format: string;
24
+ size: number;
25
+ };
26
+ thumbnails?: Array<{
27
+ size: string;
28
+ file: File;
29
+ width: number;
30
+ height: number;
31
+ }>;
32
+ }> {
33
+ logger.info(`Processing image: ${file.name}`);
34
+
35
+ try {
36
+ // Get image metadata
37
+ const metadata = await this.getImageMetadata(file);
38
+
39
+ let processedFile = file;
40
+
41
+ // Resize if needed
42
+ if (options.maxDimensions) {
43
+ processedFile = await this.resizeImage(
44
+ processedFile,
45
+ options.maxDimensions.width,
46
+ options.maxDimensions.height
47
+ );
48
+ }
49
+
50
+ // Compress if needed
51
+ if (options.compress && options.quality) {
52
+ processedFile = await this.compressImage(processedFile, options.quality);
53
+ }
54
+
55
+ // Convert format if needed
56
+ if (options.convertTo) {
57
+ processedFile = await this.convertFormat(processedFile, options.convertTo);
58
+ }
59
+
60
+ // Generate thumbnails
61
+ let thumbnails: Array<{
62
+ size: string;
63
+ file: File;
64
+ width: number;
65
+ height: number;
66
+ }> | undefined;
67
+
68
+ if (options.generateThumbnails && options.thumbnailSizes) {
69
+ thumbnails = await this.generateThumbnails(processedFile, options.thumbnailSizes);
70
+ }
71
+
72
+ const finalMetadata = await this.getImageMetadata(processedFile);
73
+
74
+ logger.info(`Image processing completed for: ${file.name}`);
75
+
76
+ return {
77
+ processedFile,
78
+ metadata: finalMetadata,
79
+ thumbnails
80
+ };
81
+
82
+ } catch (error) {
83
+ logger.error(`Image processing failed for ${file.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get image metadata (basic implementation)
90
+ * In production, use a proper image processing library
91
+ */
92
+ public static async getImageMetadata(file: File): Promise<{
93
+ width: number;
94
+ height: number;
95
+ format: string;
96
+ size: number;
97
+ }> {
98
+ return new Promise((resolve, reject) => {
99
+ const img = new Image();
100
+ const url = URL.createObjectURL(file);
101
+
102
+ img.onload = () => {
103
+ URL.revokeObjectURL(url);
104
+ resolve({
105
+ width: img.width,
106
+ height: img.height,
107
+ format: file.type.split('/')[1] || 'unknown',
108
+ size: file.size
109
+ });
110
+ };
111
+
112
+ img.onerror = () => {
113
+ URL.revokeObjectURL(url);
114
+ reject(new Error('Failed to load image for metadata extraction'));
115
+ };
116
+
117
+ img.src = url;
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Resize image (basic canvas implementation)
123
+ * For production, use Sharp or similar library
124
+ */
125
+ public static async resizeImage(
126
+ file: File,
127
+ maxWidth: number,
128
+ maxHeight: number
129
+ ): Promise<File> {
130
+ return new Promise((resolve, reject) => {
131
+ const img = new Image();
132
+ const canvas = document.createElement('canvas');
133
+ const ctx = canvas.getContext('2d');
134
+
135
+ if (!ctx) {
136
+ reject(new Error('Canvas context not available'));
137
+ return;
138
+ }
139
+
140
+ img.onload = () => {
141
+ // Calculate new dimensions
142
+ const { width: newWidth, height: newHeight } = this.calculateDimensions(
143
+ img.width,
144
+ img.height,
145
+ maxWidth,
146
+ maxHeight
147
+ );
148
+
149
+ canvas.width = newWidth;
150
+ canvas.height = newHeight;
151
+
152
+ // Draw resized image
153
+ ctx.drawImage(img, 0, 0, newWidth, newHeight);
154
+
155
+ // Convert to file
156
+ canvas.toBlob((blob) => {
157
+ if (blob) {
158
+ const resizedFile = new File([blob], file.name, {
159
+ type: file.type,
160
+ lastModified: Date.now()
161
+ });
162
+ resolve(resizedFile);
163
+ } else {
164
+ reject(new Error('Failed to create resized image blob'));
165
+ }
166
+ }, file.type);
167
+ };
168
+
169
+ img.onerror = () => {
170
+ reject(new Error('Failed to load image for resizing'));
171
+ };
172
+
173
+ img.src = URL.createObjectURL(file);
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Compress image (basic implementation)
179
+ */
180
+ public static async compressImage(file: File, quality: number): Promise<File> {
181
+ if (!file.type.startsWith('image/')) {
182
+ return file;
183
+ }
184
+
185
+ return new Promise((resolve, reject) => {
186
+ const img = new Image();
187
+ const canvas = document.createElement('canvas');
188
+ const ctx = canvas.getContext('2d');
189
+
190
+ if (!ctx) {
191
+ reject(new Error('Canvas context not available'));
192
+ return;
193
+ }
194
+
195
+ img.onload = () => {
196
+ canvas.width = img.width;
197
+ canvas.height = img.height;
198
+
199
+ ctx.drawImage(img, 0, 0);
200
+
201
+ // Convert with quality
202
+ canvas.toBlob((blob) => {
203
+ if (blob) {
204
+ const compressedFile = new File([blob], file.name, {
205
+ type: file.type,
206
+ lastModified: Date.now()
207
+ });
208
+ resolve(compressedFile);
209
+ } else {
210
+ reject(new Error('Failed to create compressed image blob'));
211
+ }
212
+ }, file.type, quality / 100);
213
+ };
214
+
215
+ img.onerror = () => {
216
+ reject(new Error('Failed to load image for compression'));
217
+ };
218
+
219
+ img.src = URL.createObjectURL(file);
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Convert image format
225
+ */
226
+ public static async convertFormat(
227
+ file: File,
228
+ targetFormat: "jpeg" | "png" | "webp"
229
+ ): Promise<File> {
230
+ const mimeType = `image/${targetFormat}`;
231
+ const extension = targetFormat === 'jpeg' ? 'jpg' : targetFormat;
232
+ const newName = file.name.replace(/\.[^/.]+$/, `.${extension}`);
233
+
234
+ return new Promise((resolve, reject) => {
235
+ const img = new Image();
236
+ const canvas = document.createElement('canvas');
237
+ const ctx = canvas.getContext('2d');
238
+
239
+ if (!ctx) {
240
+ reject(new Error('Canvas context not available'));
241
+ return;
242
+ }
243
+
244
+ img.onload = () => {
245
+ canvas.width = img.width;
246
+ canvas.height = img.height;
247
+
248
+ // Set white background for JPEG conversion
249
+ if (targetFormat === 'jpeg') {
250
+ ctx.fillStyle = '#FFFFFF';
251
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
252
+ }
253
+
254
+ ctx.drawImage(img, 0, 0);
255
+
256
+ canvas.toBlob((blob) => {
257
+ if (blob) {
258
+ const convertedFile = new File([blob], newName, {
259
+ type: mimeType,
260
+ lastModified: Date.now()
261
+ });
262
+ resolve(convertedFile);
263
+ } else {
264
+ reject(new Error('Failed to create converted image blob'));
265
+ }
266
+ }, mimeType);
267
+ };
268
+
269
+ img.onerror = () => {
270
+ reject(new Error('Failed to load image for format conversion'));
271
+ };
272
+
273
+ img.src = URL.createObjectURL(file);
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Generate thumbnails
279
+ */
280
+ public static async generateThumbnails(
281
+ file: File,
282
+ sizes: Array<{ width: number; height: number; suffix: string }>
283
+ ): Promise<Array<{
284
+ size: string;
285
+ file: File;
286
+ width: number;
287
+ height: number;
288
+ }>> {
289
+ const thumbnails: Array<{
290
+ size: string;
291
+ file: File;
292
+ width: number;
293
+ height: number;
294
+ }> = [];
295
+
296
+ for (const size of sizes) {
297
+ try {
298
+ const thumbnail = await this.createThumbnail(file, size.width, size.height, size.suffix);
299
+ thumbnails.push({
300
+ size: `${size.width}x${size.height}`,
301
+ file: thumbnail,
302
+ width: size.width,
303
+ height: size.height
304
+ });
305
+ } catch (error) {
306
+ logger.warn(`Failed to generate ${size.width}x${size.height} thumbnail: ${error instanceof Error ? error.message : 'Unknown error'}`);
307
+ }
308
+ }
309
+
310
+ return thumbnails;
311
+ }
312
+
313
+ /**
314
+ * Create a single thumbnail
315
+ */
316
+ private static async createThumbnail(
317
+ file: File,
318
+ width: number,
319
+ height: number,
320
+ suffix: string
321
+ ): Promise<File> {
322
+ return new Promise((resolve, reject) => {
323
+ const img = new Image();
324
+ const canvas = document.createElement('canvas');
325
+ const ctx = canvas.getContext('2d');
326
+
327
+ if (!ctx) {
328
+ reject(new Error('Canvas context not available'));
329
+ return;
330
+ }
331
+
332
+ img.onload = () => {
333
+ // Calculate dimensions maintaining aspect ratio
334
+ const { width: newWidth, height: newHeight } = this.calculateDimensions(
335
+ img.width,
336
+ img.height,
337
+ width,
338
+ height
339
+ );
340
+
341
+ canvas.width = newWidth;
342
+ canvas.height = newHeight;
343
+
344
+ ctx.drawImage(img, 0, 0, newWidth, newHeight);
345
+
346
+ const fileName = file.name.replace(/(\.[^.]+)$/, `${suffix}$1`);
347
+
348
+ canvas.toBlob((blob) => {
349
+ if (blob) {
350
+ const thumbnailFile = new File([blob], fileName, {
351
+ type: file.type,
352
+ lastModified: Date.now()
353
+ });
354
+ resolve(thumbnailFile);
355
+ } else {
356
+ reject(new Error('Failed to create thumbnail blob'));
357
+ }
358
+ }, file.type);
359
+ };
360
+
361
+ img.onerror = () => {
362
+ reject(new Error('Failed to load image for thumbnail creation'));
363
+ };
364
+
365
+ img.src = URL.createObjectURL(file);
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Calculate new dimensions maintaining aspect ratio
371
+ */
372
+ private static calculateDimensions(
373
+ originalWidth: number,
374
+ originalHeight: number,
375
+ maxWidth: number,
376
+ maxHeight: number
377
+ ): { width: number; height: number } {
378
+ const aspectRatio = originalWidth / originalHeight;
379
+
380
+ let newWidth = originalWidth;
381
+ let newHeight = originalHeight;
382
+
383
+ if (originalWidth > maxWidth) {
384
+ newWidth = maxWidth;
385
+ newHeight = newWidth / aspectRatio;
386
+ }
387
+
388
+ if (newHeight > maxHeight) {
389
+ newHeight = maxHeight;
390
+ newWidth = newHeight * aspectRatio;
391
+ }
392
+
393
+ return {
394
+ width: Math.round(newWidth),
395
+ height: Math.round(newHeight)
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Validate if file is a processable image
401
+ */
402
+ public static isProcessableImage(file: File): boolean {
403
+ const processableTypes = [
404
+ 'image/jpeg',
405
+ 'image/png',
406
+ 'image/gif',
407
+ 'image/webp'
408
+ ];
409
+
410
+ return processableTypes.includes(file.type);
411
+ }
412
+
413
+ /**
414
+ * Get optimal quality based on file size
415
+ */
416
+ public static getOptimalQuality(fileSize: number): number {
417
+ // Reduce quality for larger files
418
+ if (fileSize > 5 * 1024 * 1024) return 70; // >5MB
419
+ if (fileSize > 2 * 1024 * 1024) return 80; // >2MB
420
+ if (fileSize > 1 * 1024 * 1024) return 85; // >1MB
421
+ return 90; // Default quality
422
+ }
423
+ }