@appweaver/core 1.4.1 → 1.5.1
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/factory/create-model.js +25 -5
- package/package.json +1 -1
- package/prisma/client/internal/class.js +4 -4
- package/prisma/client/internal/prismaNamespace.d.ts +1 -0
- package/prisma/client/internal/prismaNamespace.js +2 -1
- package/prisma/client/internal/prismaNamespaceBrowser.d.ts +1 -0
- package/prisma/client/internal/prismaNamespaceBrowser.js +2 -1
- package/prisma/client/models/ConnectedAccount.d.ts +6 -1
- package/prisma/client/models/File.d.ts +29 -1
- package/resource/resource-loader.js +3 -0
- package/resource/resource-routes.js +0 -1
- package/resource/resource-service.d.ts +35 -8
- package/resource/resource-service.js +110 -23
- package/resource/utils/delete-util.d.ts +120 -0
- package/resource/utils/delete-util.js +385 -0
- package/resource/utils/filter-util.js +11 -5
- package/resource/utils/index.d.ts +1 -0
- package/resource/utils/index.js +1 -0
- package/resource/utils/relation-util.js +19 -6
- package/security/api-key/api-key-auth.js +4 -3
- package/security/oauth2/oauth2-service.js +19 -5
- package/security/resources/api-key/model.js +2 -0
- package/security/resources/connected-account/model.js +3 -1
- package/storage/file-service.d.ts +13 -3
- package/storage/file-service.js +24 -14
- package/storage/resources/file/model.js +5 -1
- package/types/generated.d.ts +1 -0
- package/utils/file-util.d.ts +14 -0
- package/utils/file-util.js +21 -0
- package/utils/model-util.d.ts +24 -0
- package/utils/model-util.js +43 -0
package/storage/file-service.js
CHANGED
|
@@ -27,8 +27,9 @@ class FileService {
|
|
|
27
27
|
this.assertPathNotReserved(fileName);
|
|
28
28
|
let file;
|
|
29
29
|
try {
|
|
30
|
+
// The files a deleted resource keeps are retained, but no longer served
|
|
30
31
|
file = (await this._db.client().file.findFirst({
|
|
31
|
-
where: { name: fileName }
|
|
32
|
+
where: { name: fileName, ...(0, utils_1.liveRecordFilter)('File') }
|
|
32
33
|
}));
|
|
33
34
|
}
|
|
34
35
|
catch (e) {
|
|
@@ -253,25 +254,34 @@ class FileService {
|
|
|
253
254
|
return deletedFiles;
|
|
254
255
|
}
|
|
255
256
|
/**
|
|
256
|
-
* Deletes all files associated with a resource
|
|
257
|
-
*
|
|
258
|
-
* setting (or set to `'keep'`) are left untouched.
|
|
257
|
+
* Deletes all files associated with a resource removed from the database,
|
|
258
|
+
* for the file fields not set to `onResourceDeleted: 'keep'`.
|
|
259
259
|
*
|
|
260
260
|
* @param {string} resourceName - The resource model name.
|
|
261
261
|
* @param {ResourceId} id - The ID of the deleted resource.
|
|
262
262
|
* @return {Promise<File[]>} A promise that resolves to the list of successfully deleted files.
|
|
263
263
|
*/
|
|
264
264
|
async deleteResourceFiles(resourceName, id) {
|
|
265
|
+
return this.deleteResourcesFiles(resourceName, [id]);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Deletes all files associated with the given deleted resources of one model from the storage and the database. A
|
|
269
|
+
* resource removed from the database loses the files of every field not set to `onResourceDeleted: 'keep'`, while a
|
|
270
|
+
* soft deleted resource only loses the files of the fields set to `onResourceSoftDeleted: 'delete'`.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} resourceName - The resource model name.
|
|
273
|
+
* @param {ResourceId[]} ids - The IDs of the deleted resources.
|
|
274
|
+
* @param {boolean} [softDeleted=false] - Whether the resources were soft deleted.
|
|
275
|
+
* @return {Promise<File[]>} A promise that resolves to the list of successfully deleted files.
|
|
276
|
+
*/
|
|
277
|
+
async deleteResourcesFiles(resourceName, ids, softDeleted = false) {
|
|
265
278
|
// Owning ids are stored as text, whatever the model primary key type is
|
|
266
|
-
const
|
|
279
|
+
const resourceIds = ids.map(String);
|
|
267
280
|
const resourceModel = (0, context_1.injectModel)(resourceName, false);
|
|
268
|
-
if (!resourceModel) {
|
|
281
|
+
if (!resourceModel || resourceIds.length === 0) {
|
|
269
282
|
return [];
|
|
270
283
|
}
|
|
271
|
-
const
|
|
272
|
-
const deleteFields = Object.entries(filesConfig)
|
|
273
|
-
.filter(([_, config]) => config.onResourceDeleted !== 'keep')
|
|
274
|
-
.map(([field]) => field);
|
|
284
|
+
const { deleted: deleteFields } = (0, utils_1.deletedResourceFileFields)(resourceModel.config.files, softDeleted);
|
|
275
285
|
if (deleteFields.length === 0) {
|
|
276
286
|
return [];
|
|
277
287
|
}
|
|
@@ -280,13 +290,13 @@ class FileService {
|
|
|
280
290
|
files = (await this._db.client().file.findMany({
|
|
281
291
|
where: {
|
|
282
292
|
resourceName,
|
|
283
|
-
resourceId,
|
|
293
|
+
resourceId: { in: resourceIds },
|
|
284
294
|
resourceField: { in: deleteFields }
|
|
285
295
|
}
|
|
286
296
|
}));
|
|
287
297
|
}
|
|
288
298
|
catch (e) {
|
|
289
|
-
common_1.logger.error({ resourceName,
|
|
299
|
+
common_1.logger.error({ resourceName, resourceIds, error: e }, 'Error finding resource files for deletion');
|
|
290
300
|
return [];
|
|
291
301
|
}
|
|
292
302
|
const deletedFiles = [];
|
|
@@ -296,11 +306,11 @@ class FileService {
|
|
|
296
306
|
deletedFiles.push(file);
|
|
297
307
|
}
|
|
298
308
|
else {
|
|
299
|
-
common_1.logger.error({ resourceName, resourceId, fileName: file.name }, 'Failed to delete file');
|
|
309
|
+
common_1.logger.error({ resourceName, resourceId: file.resourceId, fileName: file.name }, 'Failed to delete file');
|
|
300
310
|
}
|
|
301
311
|
}
|
|
302
312
|
if (deletedFiles.length > 0) {
|
|
303
|
-
common_1.logger.debug({ resourceName,
|
|
313
|
+
common_1.logger.debug({ resourceName, resourceIds, deletedCount: deletedFiles.length }, 'Resource files deleted');
|
|
304
314
|
}
|
|
305
315
|
return deletedFiles;
|
|
306
316
|
}
|
|
@@ -4,6 +4,9 @@ const factory_1 = require("../../../factory");
|
|
|
4
4
|
const utils_1 = require("../../../utils");
|
|
5
5
|
exports.default = (0, factory_1.createModel)({
|
|
6
6
|
name: 'File',
|
|
7
|
+
// The files a deleted resource keeps are retained for audit, but no longer
|
|
8
|
+
// served, by marking their rows deleted together with the resource
|
|
9
|
+
softDelete: true,
|
|
7
10
|
scalars: {
|
|
8
11
|
name: {
|
|
9
12
|
type: 'string',
|
|
@@ -72,5 +75,6 @@ exports.default = (0, factory_1.createModel)({
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
},
|
|
75
|
-
|
|
78
|
+
// Leads with the exact match columns, so the prefix also serves lookups
|
|
79
|
+
index: [['resourceName', 'resourceId', 'resourceField']]
|
|
76
80
|
});
|
package/types/generated.d.ts
CHANGED
package/utils/file-util.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ import { File } from '../types';
|
|
|
9
9
|
* @return {string} The absolute URL for accessing the file.
|
|
10
10
|
*/
|
|
11
11
|
export declare function buildFileUrl(file: File): string;
|
|
12
|
+
/**
|
|
13
|
+
* Splits the file fields of a deleted resource by what happens to their files. On a delete removing the resource from
|
|
14
|
+
* the database each field follows its `onResourceDeleted` option, which defaults to `'delete'`, while on a soft delete
|
|
15
|
+
* it follows its `onResourceSoftDeleted` option, which defaults to `'keep'`.
|
|
16
|
+
*
|
|
17
|
+
* @param {FilesConfig} [files] - The file fields of the deleted resource's model.
|
|
18
|
+
* @param {boolean} softDeleted - Whether the resource was soft deleted.
|
|
19
|
+
* @return {{deleted: string[], kept: string[]}} The fields whose files are removed from storage, and the fields whose
|
|
20
|
+
* files are retained, but no longer served.
|
|
21
|
+
*/
|
|
22
|
+
export declare function deletedResourceFileFields(files: FilesConfig | undefined, softDeleted: boolean): {
|
|
23
|
+
deleted: string[];
|
|
24
|
+
kept: string[];
|
|
25
|
+
};
|
|
12
26
|
/**
|
|
13
27
|
* Parses a range string and converts it into an object with start and end values.
|
|
14
28
|
*
|
package/utils/file-util.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildFileUrl = buildFileUrl;
|
|
4
|
+
exports.deletedResourceFileFields = deletedResourceFileFields;
|
|
4
5
|
exports.parseRange = parseRange;
|
|
5
6
|
exports.maxFileSize = maxFileSize;
|
|
6
7
|
exports.sizeInBytes = sizeInBytes;
|
|
@@ -26,6 +27,26 @@ function buildFileUrl(file) {
|
|
|
26
27
|
const routePrefix = `/${common_1.config.STORAGE_FILES_ROUTE_PREFIX}/`.replace(/\/+/g, '/');
|
|
27
28
|
return `${common_1.config.APP_HOSTNAME}${routePrefix}${pathPrefix}/${file.name}`;
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Splits the file fields of a deleted resource by what happens to their files. On a delete removing the resource from
|
|
32
|
+
* the database each field follows its `onResourceDeleted` option, which defaults to `'delete'`, while on a soft delete
|
|
33
|
+
* it follows its `onResourceSoftDeleted` option, which defaults to `'keep'`.
|
|
34
|
+
*
|
|
35
|
+
* @param {FilesConfig} [files] - The file fields of the deleted resource's model.
|
|
36
|
+
* @param {boolean} softDeleted - Whether the resource was soft deleted.
|
|
37
|
+
* @return {{deleted: string[], kept: string[]}} The fields whose files are removed from storage, and the fields whose
|
|
38
|
+
* files are retained, but no longer served.
|
|
39
|
+
*/
|
|
40
|
+
function deletedResourceFileFields(files = {}, softDeleted) {
|
|
41
|
+
const fields = { deleted: [], kept: [] };
|
|
42
|
+
for (const [field, fileField] of Object.entries(files)) {
|
|
43
|
+
const action = softDeleted
|
|
44
|
+
? (fileField.onResourceSoftDeleted ?? 'keep')
|
|
45
|
+
: (fileField.onResourceDeleted ?? 'delete');
|
|
46
|
+
fields[action === 'delete' ? 'deleted' : 'kept'].push(field);
|
|
47
|
+
}
|
|
48
|
+
return fields;
|
|
49
|
+
}
|
|
29
50
|
/**
|
|
30
51
|
* Parses a range string and converts it into an object with start and end values.
|
|
31
52
|
*
|
package/utils/model-util.d.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { ResourceModel } from '@appweaver/common';
|
|
2
|
+
/**
|
|
3
|
+
* Checks whether the model with the given name soft deletes its records.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} [resourceName] - The name of the model to check.
|
|
6
|
+
* @return {boolean} True if the model is loaded and has `softDelete` enabled.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSoftDeleteModel(resourceName?: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Builds the condition matching only the records of a model that are not soft
|
|
11
|
+
* deleted, for the queries reading its records outside the resource service.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} resourceName - The name of the model the condition is built for.
|
|
14
|
+
* @return {Object} The `deletedAt: null` condition for a soft deleted model, or
|
|
15
|
+
* an empty object otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function liveRecordFilter(resourceName: string): Record<string, any>;
|
|
18
|
+
/**
|
|
19
|
+
* Validates that every relation cascading on delete from a model with
|
|
20
|
+
* `softDelete` enabled belongs to a model that soft deletes its records too.
|
|
21
|
+
*
|
|
22
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
23
|
+
* @throws {Error} When a soft deleted model cascades into a model without soft delete.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateSoftDeleteCascades(models: Record<string, ResourceModel>): void;
|
|
2
26
|
/**
|
|
3
27
|
* Validates that the default value of every scalar and virtual field satisfies
|
|
4
28
|
* the constraints declared on that same field. A default outside its own
|
package/utils/model-util.js
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSoftDeleteModel = isSoftDeleteModel;
|
|
4
|
+
exports.liveRecordFilter = liveRecordFilter;
|
|
5
|
+
exports.validateSoftDeleteCascades = validateSoftDeleteCascades;
|
|
3
6
|
exports.validateScalarDefaults = validateScalarDefaults;
|
|
4
7
|
const common_1 = require("@appweaver/common");
|
|
8
|
+
const context_1 = require("../context");
|
|
9
|
+
/**
|
|
10
|
+
* Checks whether the model with the given name soft deletes its records.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} [resourceName] - The name of the model to check.
|
|
13
|
+
* @return {boolean} True if the model is loaded and has `softDelete` enabled.
|
|
14
|
+
*/
|
|
15
|
+
function isSoftDeleteModel(resourceName) {
|
|
16
|
+
if (!resourceName) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const model = (0, context_1.injectModel)((0, common_1.capitalize)(resourceName), false);
|
|
20
|
+
return (0, common_1.hasSoftDelete)(model?.config);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds the condition matching only the records of a model that are not soft
|
|
24
|
+
* deleted, for the queries reading its records outside the resource service.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} resourceName - The name of the model the condition is built for.
|
|
27
|
+
* @return {Object} The `deletedAt: null` condition for a soft deleted model, or
|
|
28
|
+
* an empty object otherwise.
|
|
29
|
+
*/
|
|
30
|
+
function liveRecordFilter(resourceName) {
|
|
31
|
+
return isSoftDeleteModel(resourceName) ? { deletedAt: null } : {};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validates that every relation cascading on delete from a model with
|
|
35
|
+
* `softDelete` enabled belongs to a model that soft deletes its records too.
|
|
36
|
+
*
|
|
37
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
38
|
+
* @throws {Error} When a soft deleted model cascades into a model without soft delete.
|
|
39
|
+
*/
|
|
40
|
+
function validateSoftDeleteCascades(models) {
|
|
41
|
+
const errors = (0, common_1.softDeleteCascadeErrors)(models);
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
throw new Error(`Invalid resource model soft delete cascades:\n${errors
|
|
44
|
+
.map((error) => ` - ${error}`)
|
|
45
|
+
.join('\n')}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
5
48
|
/**
|
|
6
49
|
* Validates that the default value of every scalar and virtual field satisfies
|
|
7
50
|
* the constraints declared on that same field. A default outside its own
|