@appweaver/core 1.0.15 → 1.0.17
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 +3 -2
- package/resource/resource-routes.js +1 -0
- package/storage/file-service.d.ts +10 -0
- package/storage/file-service.js +55 -1
- package/utils/image-util.d.ts +18 -0
- package/utils/image-util.js +53 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appweaver/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Appweaver - the backend framework for AI-first development (@core)",
|
|
5
5
|
"author": "Luka Matosevic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"date-fns": "4.4.0",
|
|
49
49
|
"fastify": "5.8.5",
|
|
50
50
|
"fastify-plugin": "6.0.0",
|
|
51
|
-
"flatted": "3.4.2"
|
|
51
|
+
"flatted": "3.4.2",
|
|
52
|
+
"sharp": "0.35.3"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@appweaver/common": "^1.0.0",
|
|
@@ -123,6 +123,7 @@ function resourceRoutes(name, routesConfig = {}) {
|
|
|
123
123
|
config: deleteConfig
|
|
124
124
|
}, async (request, reply) => {
|
|
125
125
|
const response = await service.delete(request.params.id);
|
|
126
|
+
await (0, context_1.inject)(storage_1.FileService).deleteResourceFiles(name, request.params.id);
|
|
126
127
|
return reply.send(response);
|
|
127
128
|
});
|
|
128
129
|
}
|
|
@@ -78,4 +78,14 @@ export declare class FileService {
|
|
|
78
78
|
* an error if no files could be deleted.
|
|
79
79
|
*/
|
|
80
80
|
deleteFiles(fileNames: Record<string, string | string[]>, resource: Resource, client: ResourceClient): Promise<File[]>;
|
|
81
|
+
/**
|
|
82
|
+
* Deletes all files associated with a resource for file fields configured
|
|
83
|
+
* with `onResourceDeleted: 'delete'`. Files belonging to fields without this
|
|
84
|
+
* setting (or set to `'keep'`) are left untouched.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} resourceName - The resource model name.
|
|
87
|
+
* @param {number} resourceId - The ID of the deleted resource.
|
|
88
|
+
* @return {Promise<File[]>} A promise that resolves to the list of successfully deleted files.
|
|
89
|
+
*/
|
|
90
|
+
deleteResourceFiles(resourceName: string, resourceId: number): Promise<File[]>;
|
|
81
91
|
}
|
package/storage/file-service.js
CHANGED
|
@@ -165,7 +165,11 @@ class FileService {
|
|
|
165
165
|
policy.canCreate?.(identity, resource, createFile) === false) {
|
|
166
166
|
throw new errors_1.HttpError('Creating file is forbidden', 403);
|
|
167
167
|
}
|
|
168
|
-
|
|
168
|
+
let fileStream = data.file;
|
|
169
|
+
if (fileConfig.image && (0, utils_1.isProcessableImage)(data.mimetype)) {
|
|
170
|
+
fileStream = (0, utils_1.processImage)(data.file, data.mimetype, fileConfig.image);
|
|
171
|
+
}
|
|
172
|
+
const fileName = await this._storage.store(generatedName, fileStream);
|
|
169
173
|
if (!fileName) {
|
|
170
174
|
throw new errors_1.HttpError('Error saving file to storage', 500);
|
|
171
175
|
}
|
|
@@ -327,6 +331,56 @@ class FileService {
|
|
|
327
331
|
await this._cacheService.invalidateCache(client.name, 'deleteFiles');
|
|
328
332
|
return deletedFiles;
|
|
329
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Deletes all files associated with a resource for file fields configured
|
|
336
|
+
* with `onResourceDeleted: 'delete'`. Files belonging to fields without this
|
|
337
|
+
* setting (or set to `'keep'`) are left untouched.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} resourceName - The resource model name.
|
|
340
|
+
* @param {number} resourceId - The ID of the deleted resource.
|
|
341
|
+
* @return {Promise<File[]>} A promise that resolves to the list of successfully deleted files.
|
|
342
|
+
*/
|
|
343
|
+
async deleteResourceFiles(resourceName, resourceId) {
|
|
344
|
+
const resourceModel = (0, context_1.injectModel)(resourceName, false);
|
|
345
|
+
if (!resourceModel) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
const filesConfig = resourceModel.config.files ?? {};
|
|
349
|
+
const deleteFields = Object.entries(filesConfig)
|
|
350
|
+
.filter(([_, config]) => config.onResourceDeleted !== 'keep')
|
|
351
|
+
.map(([field]) => field);
|
|
352
|
+
if (deleteFields.length === 0) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
let files;
|
|
356
|
+
try {
|
|
357
|
+
files = (await this._db.client().file.findMany({
|
|
358
|
+
where: {
|
|
359
|
+
resourceName,
|
|
360
|
+
resourceId,
|
|
361
|
+
resourceField: { in: deleteFields }
|
|
362
|
+
}
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
common_1.logger.error({ resourceName, resourceId, error: e }, 'Error finding resource files for deletion');
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
const deletedFiles = [];
|
|
370
|
+
for (const file of files) {
|
|
371
|
+
const success = await this.deleteSafe(file.name);
|
|
372
|
+
if (success) {
|
|
373
|
+
deletedFiles.push(file);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
common_1.logger.error({ resourceName, resourceId, fileName: file.name }, 'Failed to delete file');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (deletedFiles.length > 0) {
|
|
380
|
+
common_1.logger.debug({ resourceName, resourceId, deletedCount: deletedFiles.length }, 'Resource files deleted');
|
|
381
|
+
}
|
|
382
|
+
return deletedFiles;
|
|
383
|
+
}
|
|
330
384
|
/** @internal */
|
|
331
385
|
buildFileUrl(file, policy) {
|
|
332
386
|
return `${common_1.config.APP_HOSTNAME}/files/${policy.accessType === 'public' ? 'public' : 'protected'}/${file.name}`;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ImageConfig } from '@appweaver/common';
|
|
3
|
+
/**
|
|
4
|
+
* Determines if the provided MIME type corresponds to a processable image format.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} mimeType - The MIME type of the image to check.
|
|
7
|
+
* @return {boolean} Returns true if the MIME type is in the list of processable image formats, otherwise false.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isProcessableImage(mimeType: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Processes an image stream by resizing it and compressing it based on the provided configuration.
|
|
12
|
+
*
|
|
13
|
+
* @param {Readable} stream - The readable stream representing the input image.
|
|
14
|
+
* @param {string} mimeType - The MIME type of the input image (e.g., "image/jpeg", "image/png").
|
|
15
|
+
* @param {ImageConfig} config - Configuration for image processing, including dimensions, quality, and other options.
|
|
16
|
+
* @return {Readable} A readable stream of the processed image.
|
|
17
|
+
*/
|
|
18
|
+
export declare function processImage(stream: Readable, mimeType: string, config: ImageConfig): Readable;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isProcessableImage = isProcessableImage;
|
|
7
|
+
exports.processImage = processImage;
|
|
8
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
9
|
+
const IMAGE_MIME_FORMATS = {
|
|
10
|
+
'image/jpeg': 'jpeg',
|
|
11
|
+
'image/png': 'png',
|
|
12
|
+
'image/webp': 'webp',
|
|
13
|
+
'image/avif': 'avif',
|
|
14
|
+
'image/tiff': 'tiff'
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Determines if the provided MIME type corresponds to a processable image format.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} mimeType - The MIME type of the image to check.
|
|
20
|
+
* @return {boolean} Returns true if the MIME type is in the list of processable image formats, otherwise false.
|
|
21
|
+
*/
|
|
22
|
+
function isProcessableImage(mimeType) {
|
|
23
|
+
return mimeType in IMAGE_MIME_FORMATS;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Processes an image stream by resizing it and compressing it based on the provided configuration.
|
|
27
|
+
*
|
|
28
|
+
* @param {Readable} stream - The readable stream representing the input image.
|
|
29
|
+
* @param {string} mimeType - The MIME type of the input image (e.g., "image/jpeg", "image/png").
|
|
30
|
+
* @param {ImageConfig} config - Configuration for image processing, including dimensions, quality, and other options.
|
|
31
|
+
* @return {Readable} A readable stream of the processed image.
|
|
32
|
+
*/
|
|
33
|
+
function processImage(stream, mimeType, config) {
|
|
34
|
+
const format = IMAGE_MIME_FORMATS[mimeType];
|
|
35
|
+
if (!format) {
|
|
36
|
+
return stream;
|
|
37
|
+
}
|
|
38
|
+
let pipeline = (0, sharp_1.default)();
|
|
39
|
+
const width = config.width ?? config.maxWidth;
|
|
40
|
+
const height = config.height ?? config.maxHeight;
|
|
41
|
+
const fit = config.fit ?? 'inside';
|
|
42
|
+
if (width || height) {
|
|
43
|
+
pipeline = pipeline.resize({
|
|
44
|
+
width: width ?? undefined,
|
|
45
|
+
height: height ?? undefined,
|
|
46
|
+
fit,
|
|
47
|
+
withoutEnlargement: !config.width && !config.height
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const opts = config.quality ? { quality: config.quality } : {};
|
|
51
|
+
pipeline = pipeline.toFormat(format, opts);
|
|
52
|
+
return stream.pipe(pipeline);
|
|
53
|
+
}
|
package/utils/index.d.ts
CHANGED
package/utils/index.js
CHANGED
|
@@ -16,4 +16,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./export-util"), exports);
|
|
18
18
|
__exportStar(require("./file-util"), exports);
|
|
19
|
+
__exportStar(require("./image-util"), exports);
|
|
19
20
|
__exportStar(require("./schema-util"), exports);
|