bunsane 0.1.2 → 0.1.4
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/TODO.md +1 -1
- package/bun.lock +156 -150
- package/core/App.ts +188 -31
- package/core/ArcheType.ts +1044 -26
- package/core/ComponentRegistry.ts +172 -29
- package/core/Components.ts +102 -24
- package/core/Decorators.ts +0 -1
- package/core/Entity.ts +55 -7
- package/core/EntityInterface.ts +4 -0
- package/core/EntityManager.ts +4 -4
- package/core/Query.ts +169 -3
- package/core/RequestLoaders.ts +101 -12
- package/core/SchedulerManager.ts +3 -4
- package/core/metadata/definitions/ArcheType.ts +9 -0
- package/core/metadata/definitions/Component.ts +16 -0
- package/core/metadata/definitions/gqlObject.ts +10 -0
- package/core/metadata/getMetadataStorage.ts +14 -0
- package/core/metadata/index.ts +17 -0
- package/core/metadata/metadata-storage.ts +81 -0
- package/database/DatabaseHelper.ts +22 -20
- package/database/index.ts +6 -1
- package/database/sqlHelpers.ts +0 -2
- package/gql/ArchetypeOperations.ts +281 -0
- package/gql/Generator.ts +252 -62
- package/gql/helpers.ts +5 -5
- package/gql/index.ts +19 -17
- package/gql/types.ts +58 -11
- package/index.ts +93 -82
- package/package.json +39 -37
- package/plugins/index.ts +13 -0
- package/scheduler/index.ts +87 -0
- package/service/Service.ts +4 -0
- package/service/ServiceRegistry.ts +5 -1
- package/service/index.ts +1 -1
- package/swagger/decorators.ts +65 -0
- package/swagger/generator.ts +100 -0
- package/swagger/index.ts +2 -0
- package/tests/bench/insert.bench.ts +1 -0
- package/tests/bench/relations.bench.ts +1 -0
- package/tests/bench/sorting.bench.ts +1 -0
- package/tests/component-hooks-simple.test.ts +117 -0
- package/tests/component-hooks.test.ts +83 -31
- package/tests/component.test.ts +1 -0
- package/tests/hooks.test.ts +1 -0
- package/tests/query.test.ts +46 -4
- package/tests/relations.test.ts +1 -0
- package/types/app.types.ts +0 -0
- package/upload/index.ts +0 -2
- package/core/processors/ImageProcessor.ts +0 -423
|
@@ -1,423 +0,0 @@
|
|
|
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
|
-
}
|