@flusys/nestjs-storage 6.4.1 → 7.0.0-beta.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/cjs/config/event-actions.js +34 -0
- package/cjs/config/index.js +2 -1
- package/cjs/config/message-keys.js +6 -0
- package/cjs/modules/storage.module.js +1 -0
- package/cjs/services/storage-provider-config.service.js +25 -0
- package/cjs/services/storage-provider-config.service.spec.js +18 -0
- package/cjs/services/upload.service.js +35 -0
- package/cjs/services/upload.service.spec.js +54 -0
- package/config/event-actions.d.ts +10 -0
- package/config/index.d.ts +2 -1
- package/config/message-keys.d.ts +3 -0
- package/fesm/config/event-actions.js +13 -0
- package/fesm/config/index.js +2 -1
- package/fesm/config/message-keys.js +3 -0
- package/fesm/modules/storage.module.js +2 -1
- package/fesm/services/storage-provider-config.service.js +26 -1
- package/fesm/services/storage-provider-config.service.spec.js +18 -0
- package/fesm/services/upload.service.js +36 -1
- package/fesm/services/upload.service.spec.js +54 -0
- package/interfaces/storage-module-options.interface.d.ts +2 -0
- package/package.json +3 -3
- package/services/storage-provider-config.service.d.ts +3 -1
- package/services/upload.service.d.ts +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the storage module, alongside the CRUD
|
|
3
|
+
* lifecycle actions every ApiService emits.
|
|
4
|
+
*/ "use strict";
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
function _export(target, all) {
|
|
9
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
_export(exports, {
|
|
15
|
+
get STORAGE_EVENT_ACTIONS () {
|
|
16
|
+
return STORAGE_EVENT_ACTIONS;
|
|
17
|
+
},
|
|
18
|
+
get STORAGE_EVENT_ENTITIES () {
|
|
19
|
+
return STORAGE_EVENT_ENTITIES;
|
|
20
|
+
},
|
|
21
|
+
get STORAGE_EVENT_MODULE () {
|
|
22
|
+
return STORAGE_EVENT_MODULE;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const STORAGE_EVENT_ACTIONS = {
|
|
26
|
+
UPLOADED: 'uploaded',
|
|
27
|
+
REMOVED: 'removed'
|
|
28
|
+
};
|
|
29
|
+
const STORAGE_EVENT_ENTITIES = {
|
|
30
|
+
FILE: 'file_manager',
|
|
31
|
+
FOLDER: 'folder',
|
|
32
|
+
STORAGE_CONFIG: 'storageConfig'
|
|
33
|
+
};
|
|
34
|
+
const STORAGE_EVENT_MODULE = 'storage';
|
package/cjs/config/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
_export_star(require("./
|
|
5
|
+
_export_star(require("./event-actions"), exports);
|
|
6
6
|
_export_star(require("./message-keys"), exports);
|
|
7
|
+
_export_star(require("./storage.constants"), exports);
|
|
7
8
|
function _export_star(from, to) {
|
|
8
9
|
Object.keys(from).forEach(function(k) {
|
|
9
10
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
@@ -16,6 +16,9 @@ _export(exports, {
|
|
|
16
16
|
get FOLDER_MESSAGES () {
|
|
17
17
|
return FOLDER_MESSAGES;
|
|
18
18
|
},
|
|
19
|
+
get STORAGE_CONFIG_MESSAGES () {
|
|
20
|
+
return STORAGE_CONFIG_MESSAGES;
|
|
21
|
+
},
|
|
19
22
|
get UPLOAD_MESSAGES () {
|
|
20
23
|
return UPLOAD_MESSAGES;
|
|
21
24
|
}
|
|
@@ -25,6 +28,9 @@ const FILE_MESSAGES = {
|
|
|
25
28
|
DELETE_SUCCESS: 'file.delete.success',
|
|
26
29
|
NOT_FOUND: 'file.not.found'
|
|
27
30
|
};
|
|
31
|
+
const STORAGE_CONFIG_MESSAGES = {
|
|
32
|
+
DEFAULT_DELETE_FORBIDDEN: 'storage.config.default.delete.forbidden'
|
|
33
|
+
};
|
|
28
34
|
const FOLDER_MESSAGES = {
|
|
29
35
|
NOT_FOUND: 'folder.not.found'
|
|
30
36
|
};
|
|
@@ -121,6 +121,7 @@ let StorageModule = class StorageModule {
|
|
|
121
121
|
useValue: options
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
providers.push((0, _modules.createModuleEventsProvider)('storage', _storageconstants.STORAGE_MODULE_OPTIONS));
|
|
124
125
|
return providers;
|
|
125
126
|
}
|
|
126
127
|
static createAsyncProviders(options) {
|
|
@@ -12,6 +12,8 @@ const _classes = require("@flusys/nestjs-shared/classes");
|
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
13
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
14
|
const _common = require("@nestjs/common");
|
|
15
|
+
const _typeorm = require("typeorm");
|
|
16
|
+
const _config = require("../config");
|
|
15
17
|
const _entities = require("../entities");
|
|
16
18
|
const _storageconfigservice = require("./storage-config.service");
|
|
17
19
|
const _storagedatasourceprovider = require("./storage-datasource.provider");
|
|
@@ -46,6 +48,29 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
46
48
|
resolveEntity() {
|
|
47
49
|
return this.storageConfig.isCompanyFeatureEnabled() ? _entities.StorageConfigWithCompany : _entities.StorageConfig;
|
|
48
50
|
}
|
|
51
|
+
async beforeDeleteOperation(dto, _user, _queryRunner) {
|
|
52
|
+
const ids = Array.isArray(dto.id) ? dto.id : [
|
|
53
|
+
dto.id
|
|
54
|
+
];
|
|
55
|
+
await this.ensureDataSourceRepository();
|
|
56
|
+
const defaultConfigs = await this.repository.find({
|
|
57
|
+
where: {
|
|
58
|
+
id: (0, _typeorm.In)(ids),
|
|
59
|
+
isDefault: true,
|
|
60
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (defaultConfigs.length > 0) {
|
|
64
|
+
const names = defaultConfigs.map((c)=>c.name).join(', ');
|
|
65
|
+
throw new _common.ForbiddenException({
|
|
66
|
+
message: `Cannot delete the default storage config(s): ${names}. Set another config as default first.`,
|
|
67
|
+
messageKey: _config.STORAGE_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN,
|
|
68
|
+
messageVariables: {
|
|
69
|
+
names
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
49
74
|
// ─── Override Methods ───────────────────────────────────────────────────────
|
|
50
75
|
async convertSingleDtoToEntity(dto, user) {
|
|
51
76
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
@@ -240,4 +240,22 @@ describe('StorageProviderConfigService', ()=>{
|
|
|
240
240
|
expect(result).toEqual([]);
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
|
+
describe('beforeDeleteOperation', ()=>{
|
|
244
|
+
it('rejects deleting the default storage config', async ()=>{
|
|
245
|
+
mockRepo.find.mockResolvedValue([
|
|
246
|
+
buildStorageConfig({
|
|
247
|
+
isDefault: true
|
|
248
|
+
})
|
|
249
|
+
]);
|
|
250
|
+
await expect(service.beforeDeleteOperation({
|
|
251
|
+
id: 'config-uuid-1'
|
|
252
|
+
}, null, {})).rejects.toThrow('Cannot delete the default storage config');
|
|
253
|
+
});
|
|
254
|
+
it('allows deleting a non-default storage config', async ()=>{
|
|
255
|
+
mockRepo.find.mockResolvedValue([]);
|
|
256
|
+
await expect(service.beforeDeleteOperation({
|
|
257
|
+
id: 'config-uuid-1'
|
|
258
|
+
}, null, {})).resolves.toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
243
261
|
});
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "UploadService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
12
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
13
|
const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
13
14
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
15
|
const _common = require("@nestjs/common");
|
|
@@ -172,6 +173,14 @@ let UploadService = class UploadService {
|
|
|
172
173
|
this.validateFile(file);
|
|
173
174
|
const { provider, location, configId } = await this.getStorageProviderWithConfig(options.storageConfigId, user);
|
|
174
175
|
const result = await provider.uploadFile(file, options);
|
|
176
|
+
await this.publishFileEvent(_config.STORAGE_EVENT_ACTIONS.UPLOADED, [
|
|
177
|
+
result.key
|
|
178
|
+
], user, {
|
|
179
|
+
location,
|
|
180
|
+
storageConfigId: configId,
|
|
181
|
+
totalSize: result.size,
|
|
182
|
+
count: 1
|
|
183
|
+
});
|
|
175
184
|
return {
|
|
176
185
|
...result,
|
|
177
186
|
location,
|
|
@@ -184,6 +193,12 @@ let UploadService = class UploadService {
|
|
|
184
193
|
}
|
|
185
194
|
const { provider, location, configId } = await this.getStorageProviderWithConfig(options.storageConfigId, user);
|
|
186
195
|
const results = await provider.uploadMultipleFiles(files, options);
|
|
196
|
+
await this.publishFileEvent(_config.STORAGE_EVENT_ACTIONS.UPLOADED, results.map((result)=>result.key), user, {
|
|
197
|
+
location,
|
|
198
|
+
storageConfigId: configId,
|
|
199
|
+
totalSize: results.reduce((sum, result)=>sum + (result.size ?? 0), 0),
|
|
200
|
+
count: results.length
|
|
201
|
+
});
|
|
187
202
|
return results.map((result)=>({
|
|
188
203
|
...result,
|
|
189
204
|
location,
|
|
@@ -197,6 +212,12 @@ let UploadService = class UploadService {
|
|
|
197
212
|
});
|
|
198
213
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
199
214
|
await provider.deleteFile(key);
|
|
215
|
+
await this.publishFileEvent(_config.STORAGE_EVENT_ACTIONS.REMOVED, [
|
|
216
|
+
key
|
|
217
|
+
], user, {
|
|
218
|
+
storageConfigId,
|
|
219
|
+
count: 1
|
|
220
|
+
});
|
|
200
221
|
return true;
|
|
201
222
|
}
|
|
202
223
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
@@ -206,8 +227,22 @@ let UploadService = class UploadService {
|
|
|
206
227
|
});
|
|
207
228
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
208
229
|
await provider.deleteMultipleFiles(keys);
|
|
230
|
+
await this.publishFileEvent(_config.STORAGE_EVENT_ACTIONS.REMOVED, keys, user, {
|
|
231
|
+
storageConfigId,
|
|
232
|
+
count: keys.length
|
|
233
|
+
});
|
|
209
234
|
return true;
|
|
210
235
|
}
|
|
236
|
+
async publishFileEvent(action, keys, user, metadata) {
|
|
237
|
+
await (0, _classes.publishDomainEvent)({
|
|
238
|
+
module: _config.STORAGE_EVENT_MODULE,
|
|
239
|
+
entity: _config.STORAGE_EVENT_ENTITIES.FILE,
|
|
240
|
+
action,
|
|
241
|
+
ids: keys,
|
|
242
|
+
metadata,
|
|
243
|
+
user: user ?? null
|
|
244
|
+
});
|
|
245
|
+
}
|
|
211
246
|
bytesToKb(bytes) {
|
|
212
247
|
return Number((bytes * 0.001).toFixed(2));
|
|
213
248
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
6
7
|
const _uploadservice = require("./upload.service");
|
|
7
8
|
const _storagefactoryservice = require("../providers/storage-factory.service");
|
|
8
9
|
const _storageconfigservice = require("./storage-config.service");
|
|
@@ -369,4 +370,57 @@ describe('UploadService', ()=>{
|
|
|
369
370
|
expect(mockProvider.generatePresignedUrl).toHaveBeenCalledWith('uploads/photo.jpg', 3600);
|
|
370
371
|
});
|
|
371
372
|
});
|
|
373
|
+
describe('domain events', ()=>{
|
|
374
|
+
let publish;
|
|
375
|
+
beforeEach(()=>{
|
|
376
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
377
|
+
_classes.EventBusRegistry.reset();
|
|
378
|
+
_classes.EventBusRegistry.setBus({
|
|
379
|
+
publish,
|
|
380
|
+
subscribe: jest.fn(),
|
|
381
|
+
close: jest.fn()
|
|
382
|
+
});
|
|
383
|
+
_classes.EventBusRegistry.configureModule('storage', {
|
|
384
|
+
enabled: true
|
|
385
|
+
});
|
|
386
|
+
mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
|
|
387
|
+
id: 'cfg-default',
|
|
388
|
+
storage: 'local',
|
|
389
|
+
config: {}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
afterEach(()=>{
|
|
393
|
+
_classes.EventBusRegistry.reset();
|
|
394
|
+
});
|
|
395
|
+
it('publishes storage.file_manager.uploaded after a single upload', async ()=>{
|
|
396
|
+
await service.uploadSingleFile(buildMulterFile(), {}, (0, _loggedusermock.buildMockUser)());
|
|
397
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
398
|
+
name: 'storage.file_manager.uploaded',
|
|
399
|
+
payload: expect.objectContaining({
|
|
400
|
+
metadata: expect.objectContaining({
|
|
401
|
+
count: 1,
|
|
402
|
+
storageConfigId: 'cfg-default'
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
}));
|
|
406
|
+
});
|
|
407
|
+
it('publishes storage.file_manager.removed after a delete', async ()=>{
|
|
408
|
+
await service.deleteSingleFile('uploads/photo.jpg', 'cfg-1', (0, _loggedusermock.buildMockUser)());
|
|
409
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
410
|
+
name: 'storage.file_manager.removed',
|
|
411
|
+
payload: expect.objectContaining({
|
|
412
|
+
ids: [
|
|
413
|
+
'uploads/photo.jpg'
|
|
414
|
+
]
|
|
415
|
+
})
|
|
416
|
+
}));
|
|
417
|
+
});
|
|
418
|
+
it('does not publish when the storage module has events disabled', async ()=>{
|
|
419
|
+
_classes.EventBusRegistry.configureModule('storage', {
|
|
420
|
+
enabled: false
|
|
421
|
+
});
|
|
422
|
+
await service.uploadSingleFile(buildMulterFile(), {}, (0, _loggedusermock.buildMockUser)());
|
|
423
|
+
expect(publish).not.toHaveBeenCalled();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
372
426
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const STORAGE_EVENT_ACTIONS: {
|
|
2
|
+
readonly UPLOADED: "uploaded";
|
|
3
|
+
readonly REMOVED: "removed";
|
|
4
|
+
};
|
|
5
|
+
export declare const STORAGE_EVENT_ENTITIES: {
|
|
6
|
+
readonly FILE: "file_manager";
|
|
7
|
+
readonly FOLDER: "folder";
|
|
8
|
+
readonly STORAGE_CONFIG: "storageConfig";
|
|
9
|
+
};
|
|
10
|
+
export declare const STORAGE_EVENT_MODULE = "storage";
|
package/config/index.d.ts
CHANGED
package/config/message-keys.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export declare const FILE_MESSAGES: {
|
|
|
3
3
|
readonly DELETE_SUCCESS: "file.delete.success";
|
|
4
4
|
readonly NOT_FOUND: "file.not.found";
|
|
5
5
|
};
|
|
6
|
+
export declare const STORAGE_CONFIG_MESSAGES: {
|
|
7
|
+
readonly DEFAULT_DELETE_FORBIDDEN: "storage.config.default.delete.forbidden";
|
|
8
|
+
};
|
|
6
9
|
export declare const FOLDER_MESSAGES: {
|
|
7
10
|
readonly NOT_FOUND: "folder.not.found";
|
|
8
11
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the storage module, alongside the CRUD
|
|
3
|
+
* lifecycle actions every ApiService emits.
|
|
4
|
+
*/ export const STORAGE_EVENT_ACTIONS = {
|
|
5
|
+
UPLOADED: 'uploaded',
|
|
6
|
+
REMOVED: 'removed'
|
|
7
|
+
};
|
|
8
|
+
/** Every entity the storage module publishes under, ApiService backed or not */ export const STORAGE_EVENT_ENTITIES = {
|
|
9
|
+
FILE: 'file_manager',
|
|
10
|
+
FOLDER: 'folder',
|
|
11
|
+
STORAGE_CONFIG: 'storageConfig'
|
|
12
|
+
};
|
|
13
|
+
export const STORAGE_EVENT_MODULE = 'storage';
|
package/fesm/config/index.js
CHANGED
|
@@ -4,6 +4,9 @@ export const FILE_MESSAGES = {
|
|
|
4
4
|
DELETE_SUCCESS: 'file.delete.success',
|
|
5
5
|
NOT_FOUND: 'file.not.found'
|
|
6
6
|
};
|
|
7
|
+
export const STORAGE_CONFIG_MESSAGES = {
|
|
8
|
+
DEFAULT_DELETE_FORBIDDEN: 'storage.config.default.delete.forbidden'
|
|
9
|
+
};
|
|
7
10
|
export const FOLDER_MESSAGES = {
|
|
8
11
|
NOT_FOUND: 'folder.not.found'
|
|
9
12
|
};
|
|
@@ -4,7 +4,7 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
4
4
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
|
-
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
7
|
+
import { CacheModule, UtilsModule, createModuleEventsProvider } from '@flusys/nestjs-shared/modules';
|
|
8
8
|
import { Logger, Module, RequestMethod } from '@nestjs/common';
|
|
9
9
|
import { FileServeMiddleware } from '../middlewares';
|
|
10
10
|
import { StorageConfigService } from '../services';
|
|
@@ -112,6 +112,7 @@ export class StorageModule {
|
|
|
112
112
|
useValue: options
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
+
providers.push(createModuleEventsProvider('storage', STORAGE_MODULE_OPTIONS));
|
|
115
116
|
return providers;
|
|
116
117
|
}
|
|
117
118
|
static createAsyncProviders(options) {
|
|
@@ -28,7 +28,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
28
28
|
import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
31
|
-
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
+
import { ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
+
import { In, IsNull } from 'typeorm';
|
|
33
|
+
import { STORAGE_CONFIG_MESSAGES } from '../config';
|
|
32
34
|
import { StorageConfig, StorageConfigWithCompany } from '../entities';
|
|
33
35
|
import { StorageConfigService } from './storage-config.service';
|
|
34
36
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
@@ -36,6 +38,29 @@ export class StorageProviderConfigService extends ApiService {
|
|
|
36
38
|
resolveEntity() {
|
|
37
39
|
return this.storageConfig.isCompanyFeatureEnabled() ? StorageConfigWithCompany : StorageConfig;
|
|
38
40
|
}
|
|
41
|
+
async beforeDeleteOperation(dto, _user, _queryRunner) {
|
|
42
|
+
const ids = Array.isArray(dto.id) ? dto.id : [
|
|
43
|
+
dto.id
|
|
44
|
+
];
|
|
45
|
+
await this.ensureDataSourceRepository();
|
|
46
|
+
const defaultConfigs = await this.repository.find({
|
|
47
|
+
where: {
|
|
48
|
+
id: In(ids),
|
|
49
|
+
isDefault: true,
|
|
50
|
+
deletedAt: IsNull()
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (defaultConfigs.length > 0) {
|
|
54
|
+
const names = defaultConfigs.map((c)=>c.name).join(', ');
|
|
55
|
+
throw new ForbiddenException({
|
|
56
|
+
message: `Cannot delete the default storage config(s): ${names}. Set another config as default first.`,
|
|
57
|
+
messageKey: STORAGE_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN,
|
|
58
|
+
messageVariables: {
|
|
59
|
+
names
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
39
64
|
// ─── Override Methods ───────────────────────────────────────────────────────
|
|
40
65
|
async convertSingleDtoToEntity(dto, user) {
|
|
41
66
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
@@ -236,4 +236,22 @@ describe('StorageProviderConfigService', ()=>{
|
|
|
236
236
|
expect(result).toEqual([]);
|
|
237
237
|
});
|
|
238
238
|
});
|
|
239
|
+
describe('beforeDeleteOperation', ()=>{
|
|
240
|
+
it('rejects deleting the default storage config', async ()=>{
|
|
241
|
+
mockRepo.find.mockResolvedValue([
|
|
242
|
+
buildStorageConfig({
|
|
243
|
+
isDefault: true
|
|
244
|
+
})
|
|
245
|
+
]);
|
|
246
|
+
await expect(service.beforeDeleteOperation({
|
|
247
|
+
id: 'config-uuid-1'
|
|
248
|
+
}, null, {})).rejects.toThrow('Cannot delete the default storage config');
|
|
249
|
+
});
|
|
250
|
+
it('allows deleting a non-default storage config', async ()=>{
|
|
251
|
+
mockRepo.find.mockResolvedValue([]);
|
|
252
|
+
await expect(service.beforeDeleteOperation({
|
|
253
|
+
id: 'config-uuid-1'
|
|
254
|
+
}, null, {})).resolves.toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
239
257
|
});
|
|
@@ -26,10 +26,11 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { LogAction } from '@flusys/nestjs-shared';
|
|
29
|
+
import { publishDomainEvent } from '@flusys/nestjs-shared/classes';
|
|
29
30
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
30
31
|
import { validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
31
32
|
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
32
|
-
import { UPLOAD_MESSAGES } from '../config';
|
|
33
|
+
import { STORAGE_EVENT_ACTIONS, STORAGE_EVENT_ENTITIES, STORAGE_EVENT_MODULE, UPLOAD_MESSAGES } from '../config';
|
|
33
34
|
import { UploadOptionsDto } from '../dtos/upload.dto';
|
|
34
35
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
35
36
|
import { StorageFactoryService } from '../providers/storage-factory.service';
|
|
@@ -162,6 +163,14 @@ export class UploadService {
|
|
|
162
163
|
this.validateFile(file);
|
|
163
164
|
const { provider, location, configId } = await this.getStorageProviderWithConfig(options.storageConfigId, user);
|
|
164
165
|
const result = await provider.uploadFile(file, options);
|
|
166
|
+
await this.publishFileEvent(STORAGE_EVENT_ACTIONS.UPLOADED, [
|
|
167
|
+
result.key
|
|
168
|
+
], user, {
|
|
169
|
+
location,
|
|
170
|
+
storageConfigId: configId,
|
|
171
|
+
totalSize: result.size,
|
|
172
|
+
count: 1
|
|
173
|
+
});
|
|
165
174
|
return {
|
|
166
175
|
...result,
|
|
167
176
|
location,
|
|
@@ -174,6 +183,12 @@ export class UploadService {
|
|
|
174
183
|
}
|
|
175
184
|
const { provider, location, configId } = await this.getStorageProviderWithConfig(options.storageConfigId, user);
|
|
176
185
|
const results = await provider.uploadMultipleFiles(files, options);
|
|
186
|
+
await this.publishFileEvent(STORAGE_EVENT_ACTIONS.UPLOADED, results.map((result)=>result.key), user, {
|
|
187
|
+
location,
|
|
188
|
+
storageConfigId: configId,
|
|
189
|
+
totalSize: results.reduce((sum, result)=>sum + (result.size ?? 0), 0),
|
|
190
|
+
count: results.length
|
|
191
|
+
});
|
|
177
192
|
return results.map((result)=>({
|
|
178
193
|
...result,
|
|
179
194
|
location,
|
|
@@ -187,6 +202,12 @@ export class UploadService {
|
|
|
187
202
|
});
|
|
188
203
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
189
204
|
await provider.deleteFile(key);
|
|
205
|
+
await this.publishFileEvent(STORAGE_EVENT_ACTIONS.REMOVED, [
|
|
206
|
+
key
|
|
207
|
+
], user, {
|
|
208
|
+
storageConfigId,
|
|
209
|
+
count: 1
|
|
210
|
+
});
|
|
190
211
|
return true;
|
|
191
212
|
}
|
|
192
213
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
@@ -196,8 +217,22 @@ export class UploadService {
|
|
|
196
217
|
});
|
|
197
218
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
198
219
|
await provider.deleteMultipleFiles(keys);
|
|
220
|
+
await this.publishFileEvent(STORAGE_EVENT_ACTIONS.REMOVED, keys, user, {
|
|
221
|
+
storageConfigId,
|
|
222
|
+
count: keys.length
|
|
223
|
+
});
|
|
199
224
|
return true;
|
|
200
225
|
}
|
|
226
|
+
async publishFileEvent(action, keys, user, metadata) {
|
|
227
|
+
await publishDomainEvent({
|
|
228
|
+
module: STORAGE_EVENT_MODULE,
|
|
229
|
+
entity: STORAGE_EVENT_ENTITIES.FILE,
|
|
230
|
+
action,
|
|
231
|
+
ids: keys,
|
|
232
|
+
metadata,
|
|
233
|
+
user: user ?? null
|
|
234
|
+
});
|
|
235
|
+
}
|
|
201
236
|
bytesToKb(bytes) {
|
|
202
237
|
return Number((bytes * 0.001).toFixed(2));
|
|
203
238
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Test } from '@nestjs/testing';
|
|
2
|
+
import { EventBusRegistry } from '@flusys/nestjs-shared/classes';
|
|
2
3
|
import { UploadService } from './upload.service';
|
|
3
4
|
import { StorageFactoryService } from '../providers/storage-factory.service';
|
|
4
5
|
import { StorageConfigService } from './storage-config.service';
|
|
@@ -365,4 +366,57 @@ describe('UploadService', ()=>{
|
|
|
365
366
|
expect(mockProvider.generatePresignedUrl).toHaveBeenCalledWith('uploads/photo.jpg', 3600);
|
|
366
367
|
});
|
|
367
368
|
});
|
|
369
|
+
describe('domain events', ()=>{
|
|
370
|
+
let publish;
|
|
371
|
+
beforeEach(()=>{
|
|
372
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
373
|
+
EventBusRegistry.reset();
|
|
374
|
+
EventBusRegistry.setBus({
|
|
375
|
+
publish,
|
|
376
|
+
subscribe: jest.fn(),
|
|
377
|
+
close: jest.fn()
|
|
378
|
+
});
|
|
379
|
+
EventBusRegistry.configureModule('storage', {
|
|
380
|
+
enabled: true
|
|
381
|
+
});
|
|
382
|
+
mockStorageProviderConfigService.getDefaultConfig.mockResolvedValue({
|
|
383
|
+
id: 'cfg-default',
|
|
384
|
+
storage: 'local',
|
|
385
|
+
config: {}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
afterEach(()=>{
|
|
389
|
+
EventBusRegistry.reset();
|
|
390
|
+
});
|
|
391
|
+
it('publishes storage.file_manager.uploaded after a single upload', async ()=>{
|
|
392
|
+
await service.uploadSingleFile(buildMulterFile(), {}, buildMockUser());
|
|
393
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
394
|
+
name: 'storage.file_manager.uploaded',
|
|
395
|
+
payload: expect.objectContaining({
|
|
396
|
+
metadata: expect.objectContaining({
|
|
397
|
+
count: 1,
|
|
398
|
+
storageConfigId: 'cfg-default'
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
}));
|
|
402
|
+
});
|
|
403
|
+
it('publishes storage.file_manager.removed after a delete', async ()=>{
|
|
404
|
+
await service.deleteSingleFile('uploads/photo.jpg', 'cfg-1', buildMockUser());
|
|
405
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
406
|
+
name: 'storage.file_manager.removed',
|
|
407
|
+
payload: expect.objectContaining({
|
|
408
|
+
ids: [
|
|
409
|
+
'uploads/photo.jpg'
|
|
410
|
+
]
|
|
411
|
+
})
|
|
412
|
+
}));
|
|
413
|
+
});
|
|
414
|
+
it('does not publish when the storage module has events disabled', async ()=>{
|
|
415
|
+
EventBusRegistry.configureModule('storage', {
|
|
416
|
+
enabled: false
|
|
417
|
+
});
|
|
418
|
+
await service.uploadSingleFile(buildMulterFile(), {}, buildMockUser());
|
|
419
|
+
expect(publish).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
368
422
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
2
|
+
import { IModuleEventsConfig } from '@flusys/nestjs-shared/interfaces';
|
|
2
3
|
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
3
4
|
export interface IStorageModuleConfig extends IDataSourceServiceOptions {
|
|
4
5
|
maxFileSize?: number;
|
|
@@ -6,6 +7,7 @@ export interface IStorageModuleConfig extends IDataSourceServiceOptions {
|
|
|
6
7
|
defaultStorageProvider?: string;
|
|
7
8
|
localStoragePath?: string;
|
|
8
9
|
appUrl?: string;
|
|
10
|
+
events?: IModuleEventsConfig;
|
|
9
11
|
}
|
|
10
12
|
export interface StorageModuleOptions extends IDynamicModuleConfig {
|
|
11
13
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-storage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-beta.1",
|
|
4
4
|
"description": "Modular storage package with optional AWS S3, Azure Blob, and SFTP providers",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@flusys/nestjs-core": "
|
|
133
|
-
"@flusys/nestjs-shared": "
|
|
132
|
+
"@flusys/nestjs-core": "7.0.0-beta.1",
|
|
133
|
+
"@flusys/nestjs-shared": "7.0.0-beta.1"
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
|
|
2
|
+
import { DeleteDto } from '@flusys/nestjs-shared/dtos';
|
|
2
3
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
3
4
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
4
|
-
import { EntityTarget, SelectQueryBuilder } from 'typeorm';
|
|
5
|
+
import { EntityTarget, QueryRunner, SelectQueryBuilder } from 'typeorm';
|
|
5
6
|
import { CreateStorageConfigDto, UpdateStorageConfigDto } from '../dtos';
|
|
6
7
|
import { StorageConfig, StorageConfigBase } from '../entities';
|
|
7
8
|
import { IStorageConfig } from '../interfaces';
|
|
@@ -13,6 +14,7 @@ export declare class StorageProviderConfigService extends ApiService<CreateStora
|
|
|
13
14
|
private readonly storageConfig;
|
|
14
15
|
constructor(cacheManager: HybridCache, utilsService: UtilsService, storageConfig: StorageConfigService, dataSourceProvider: StorageDataSourceProvider);
|
|
15
16
|
protected resolveEntity(): EntityTarget<StorageConfigBase>;
|
|
17
|
+
protected beforeDeleteOperation(dto: DeleteDto, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
|
|
16
18
|
convertSingleDtoToEntity(dto: CreateStorageConfigDto | UpdateStorageConfigDto, user: ILoggedUserInfo | null): Promise<StorageConfigBase>;
|
|
17
19
|
getSelectQuery(query: SelectQueryBuilder<StorageConfigBase>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
18
20
|
query: SelectQueryBuilder<StorageConfig>;
|
|
@@ -18,6 +18,7 @@ export declare class UploadService {
|
|
|
18
18
|
uploadMultipleFiles(files: Express.Multer.File[], options: UploadOptionsDto, user?: ILoggedUserInfo): Promise<IUploadedFileInfo[]>;
|
|
19
19
|
deleteSingleFile(key: string, storageConfigId?: string, user?: ILoggedUserInfo, locationHint?: string): Promise<boolean>;
|
|
20
20
|
deleteMultipleFile(keys: string[], storageConfigId?: string, user?: ILoggedUserInfo, locationHint?: string): Promise<boolean>;
|
|
21
|
+
private publishFileEvent;
|
|
21
22
|
bytesToKb(bytes: number): number;
|
|
22
23
|
getLocalStorageBasePath(): Promise<string | null>;
|
|
23
24
|
makeFileUrl(key: string, storageConfigId: string, expiresIn?: number, user?: ILoggedUserInfo): Promise<string>;
|