@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appweaver/core",
3
- "version": "1.0.15",
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
  }
@@ -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
- const fileName = await this._storage.store(generatedName, data.file);
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
@@ -1,3 +1,4 @@
1
1
  export * from './export-util';
2
2
  export * from './file-util';
3
+ export * from './image-util';
3
4
  export * from './schema-util';
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);