@flusys/nestjs-storage 0.1.0-beta.3 → 1.0.0-rc

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.
Files changed (67) hide show
  1. package/README.md +131 -19
  2. package/cjs/config/storage-config.service.js +5 -0
  3. package/cjs/config/storage.constants.js +0 -8
  4. package/cjs/controllers/file-manager.controller.js +50 -5
  5. package/cjs/controllers/folder.controller.js +46 -4
  6. package/cjs/controllers/storage-config.controller.js +46 -4
  7. package/cjs/controllers/upload.controller.js +6 -12
  8. package/cjs/dtos/file-manager.dto.js +8 -5
  9. package/cjs/dtos/storage-config.dto.js +41 -1
  10. package/cjs/dtos/upload.dto.js +7 -0
  11. package/cjs/entities/storage-config-base.entity.js +31 -2
  12. package/cjs/interfaces/index.js +0 -1
  13. package/cjs/middlewares/file-serve.middleware.js +6 -0
  14. package/cjs/modules/storage.module.js +2 -4
  15. package/cjs/providers/local-provider.js +52 -2
  16. package/cjs/providers/storage-factory.service.js +2 -2
  17. package/cjs/services/file-manager.service.js +37 -24
  18. package/cjs/services/folder.service.js +18 -52
  19. package/cjs/services/storage-datasource.provider.js +10 -16
  20. package/cjs/services/storage-provider-config.service.js +28 -63
  21. package/cjs/services/upload.service.js +39 -27
  22. package/cjs/utils/file-validator.util.js +470 -0
  23. package/cjs/utils/image-compressor.util.js +1 -3
  24. package/config/storage-config.service.d.ts +5 -2
  25. package/config/storage.constants.d.ts +0 -2
  26. package/controllers/file-manager.controller.d.ts +1 -1
  27. package/controllers/upload.controller.d.ts +2 -6
  28. package/dtos/file-manager.dto.d.ts +2 -4
  29. package/dtos/folder.dto.d.ts +2 -4
  30. package/dtos/storage-config.dto.d.ts +9 -6
  31. package/entities/storage-config-base.entity.d.ts +2 -0
  32. package/fesm/config/storage-config.service.js +5 -0
  33. package/fesm/config/storage.constants.js +0 -2
  34. package/fesm/controllers/file-manager.controller.js +51 -6
  35. package/fesm/controllers/folder.controller.js +49 -7
  36. package/fesm/controllers/storage-config.controller.js +49 -7
  37. package/fesm/controllers/upload.controller.js +7 -13
  38. package/fesm/dtos/file-manager.dto.js +8 -5
  39. package/fesm/dtos/storage-config.dto.js +45 -11
  40. package/fesm/dtos/upload.dto.js +8 -1
  41. package/fesm/entities/index.js +1 -5
  42. package/fesm/entities/storage-config-base.entity.js +33 -7
  43. package/fesm/interfaces/index.js +0 -1
  44. package/fesm/interfaces/storage-config.interface.js +1 -3
  45. package/fesm/middlewares/file-serve.middleware.js +7 -1
  46. package/fesm/modules/storage.module.js +2 -4
  47. package/fesm/providers/local-provider.js +52 -2
  48. package/fesm/providers/storage-factory.service.js +2 -2
  49. package/fesm/services/file-manager.service.js +38 -25
  50. package/fesm/services/folder.service.js +19 -53
  51. package/fesm/services/storage-datasource.provider.js +10 -16
  52. package/fesm/services/storage-provider-config.service.js +28 -63
  53. package/fesm/services/upload.service.js +40 -28
  54. package/fesm/utils/file-validator.util.js +463 -0
  55. package/fesm/utils/image-compressor.util.js +1 -3
  56. package/interfaces/file-manager.interface.d.ts +7 -4
  57. package/interfaces/index.d.ts +0 -1
  58. package/interfaces/storage-config.interface.d.ts +2 -20
  59. package/package.json +6 -6
  60. package/providers/local-provider.d.ts +2 -0
  61. package/services/file-manager.service.d.ts +2 -2
  62. package/services/folder.service.d.ts +1 -2
  63. package/services/storage-provider-config.service.d.ts +1 -2
  64. package/utils/file-validator.util.d.ts +16 -0
  65. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  66. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  67. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -32,6 +32,29 @@ import { v4 as uuidv4 } from 'uuid';
32
32
  * Uses Node.js built-in fs module - no external dependencies
33
33
  */ export class LocalProvider {
34
34
  /**
35
+ * SECURITY: Validates that a target path does not escape the base directory
36
+ * Prevents path traversal attacks using ../ sequences
37
+ * @throws Error if path traversal is detected
38
+ */ validatePathWithinBase(targetPath) {
39
+ const normalizedBasePath = path.resolve(this.basePath);
40
+ const normalizedTargetPath = path.resolve(targetPath);
41
+ if (!normalizedTargetPath.startsWith(normalizedBasePath + path.sep) && normalizedTargetPath !== normalizedBasePath) {
42
+ this.logger.warn(`Path traversal attempt detected: ${targetPath}`);
43
+ throw new Error('Invalid path: Path traversal attempt detected');
44
+ }
45
+ }
46
+ /**
47
+ * SECURITY: Validates file key format to prevent malicious input
48
+ * @throws Error if key contains suspicious patterns
49
+ */ validateKeyFormat(key) {
50
+ if (!key || typeof key !== 'string' || key.trim().length === 0) {
51
+ throw new Error('Invalid file key: empty or invalid');
52
+ }
53
+ if (key.includes('\0')) {
54
+ throw new Error('Invalid file key: contains null bytes');
55
+ }
56
+ }
57
+ /**
35
58
  * Initialize Local File System provider with configuration
36
59
  * @param config.basePath - Base path for file storage (default: './uploads')
37
60
  * @param config.baseUrl - Optional base URL for generating file URLs
@@ -64,7 +87,11 @@ import { v4 as uuidv4 } from 'uuid';
64
87
  }
65
88
  // Build file path
66
89
  const folderPath = options.folderPath ? path.join(this.basePath, options.folderPath) : this.basePath;
90
+ // SECURITY: Validate path does not escape base directory
91
+ this.validatePathWithinBase(folderPath);
67
92
  const filePath = path.join(folderPath, fileName);
93
+ // SECURITY: Double-check final file path
94
+ this.validatePathWithinBase(filePath);
68
95
  // Ensure directory exists
69
96
  await fs.mkdir(folderPath, {
70
97
  recursive: true
@@ -87,14 +114,20 @@ import { v4 as uuidv4 } from 'uuid';
87
114
  }
88
115
  async deleteFile(key) {
89
116
  try {
117
+ // SECURITY: Validate key format first
118
+ this.validateKeyFormat(key);
90
119
  // Key now includes the basePath, resolve from cwd
91
120
  let filePath = path.resolve(key);
121
+ // SECURITY: Validate resolved path is within base directory
122
+ this.validatePathWithinBase(filePath);
92
123
  // Check if file exists at the resolved path
93
124
  try {
94
125
  await fs.access(filePath);
95
126
  } catch {
96
127
  // Fallback: try with basePath prefix (for old keys without basePath)
97
128
  const fallbackPath = path.join(this.basePath, key);
129
+ // SECURITY: Validate fallback path as well
130
+ this.validatePathWithinBase(fallbackPath);
98
131
  try {
99
132
  await fs.access(fallbackPath);
100
133
  filePath = fallbackPath;
@@ -107,7 +140,11 @@ import { v4 as uuidv4 } from 'uuid';
107
140
  }
108
141
  await fs.unlink(filePath);
109
142
  this.logger.log(`Deleted file from local storage: ${key}`);
110
- } catch (_error) {
143
+ } catch (error) {
144
+ // Re-throw security errors
145
+ if (error instanceof Error && error.message.includes('Invalid')) {
146
+ throw error;
147
+ }
111
148
  this.logger.warn(`Failed to delete file from local storage: ${key}`);
112
149
  }
113
150
  }
@@ -138,14 +175,23 @@ import { v4 as uuidv4 } from 'uuid';
138
175
  * Get the absolute path for a file key
139
176
  * Key now includes basePath, so resolve from cwd
140
177
  */ getAbsolutePath(key) {
141
- return path.resolve(key);
178
+ // SECURITY: Validate key format
179
+ this.validateKeyFormat(key);
180
+ const filePath = path.resolve(key);
181
+ // SECURITY: Validate path is within base directory
182
+ this.validatePathWithinBase(filePath);
183
+ return filePath;
142
184
  }
143
185
  /**
144
186
  * Check if a file exists
145
187
  */ async fileExists(key) {
146
188
  try {
189
+ // SECURITY: Validate key format
190
+ this.validateKeyFormat(key);
147
191
  // Key now includes basePath, resolve from cwd
148
192
  const filePath = path.resolve(key);
193
+ // SECURITY: Validate path is within base directory
194
+ this.validatePathWithinBase(filePath);
149
195
  await fs.access(filePath);
150
196
  return true;
151
197
  } catch {
@@ -155,8 +201,12 @@ import { v4 as uuidv4 } from 'uuid';
155
201
  /**
156
202
  * Get file stats
157
203
  */ async getFileStats(key) {
204
+ // SECURITY: Validate key format
205
+ this.validateKeyFormat(key);
158
206
  // Key now includes basePath, resolve from cwd
159
207
  const filePath = path.resolve(key);
208
+ // SECURITY: Validate path is within base directory
209
+ this.validatePathWithinBase(filePath);
160
210
  const stats = await fs.stat(filePath);
161
211
  return {
162
212
  size: stats.size,
@@ -79,8 +79,8 @@ export class StorageFactoryService {
79
79
  } catch (error) {
80
80
  this.logger.error(`Failed to create provider ${config.provider}:`, error);
81
81
  // Preserve original error message for better debugging
82
- const originalMessage = error?.message || 'Unknown error';
83
- throw new Error(`Failed to initialize storage provider '${config.provider}': ${originalMessage}`);
82
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
83
+ throw new Error(`Failed to initialize storage provider '${config.provider}': ${errorMessage}`);
84
84
  }
85
85
  }
86
86
  /**
@@ -25,8 +25,9 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
28
+ import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
+ import { applyCompanyFilter, ErrorHandler } from '@flusys/nestjs-shared/utils';
30
31
  import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
31
32
  import { Brackets, In } from 'typeorm';
32
33
  import { StorageConfigService } from '../config';
@@ -103,8 +104,9 @@ export class FileManagerService extends RequestScopedApiService {
103
104
  const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
104
105
  const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
105
106
  const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
107
+ const storageConfigId = dto.storageConfigId;
106
108
  const whereCondition = {
107
- id: dto.storageConfigId
109
+ id: storageConfigId
108
110
  };
109
111
  // Filter by company if company feature is enabled
110
112
  if (enableCompanyFeature && user?.companyId) {
@@ -114,25 +116,30 @@ export class FileManagerService extends RequestScopedApiService {
114
116
  where: whereCondition
115
117
  });
116
118
  if (storageConfig) {
117
- storageLocation = storageConfig.storage;
119
+ const typedConfig = storageConfig;
120
+ if (typedConfig.storage) {
121
+ storageLocation = typedConfig.storage;
122
+ }
118
123
  }
119
124
  } catch (error) {
120
- this.logger.warn(`Failed to get storage location from config: ${error}`);
125
+ const errorMessage = ErrorHandler.getErrorMessage(error);
126
+ this.logger.warn(`Failed to get storage location from config ${dto.storageConfigId}: ${errorMessage}`);
121
127
  // Fall back to DTO location or default
122
128
  }
123
129
  }
124
- // Set basic fields
125
- fileManager = {
130
+ // Set basic fields - merge existing data with DTO
131
+ const mergedFileManager = {
126
132
  ...fileManager,
127
133
  ...dto,
128
134
  location: storageLocation,
129
135
  folder: validatedFolder
130
136
  };
131
- // Only set company fields if they exist on the entity (when company feature is enabled)
132
- if ('companyId' in fileManager) {
133
- fileManager.companyId = user?.companyId ?? null;
137
+ // Only set company fields if company feature is enabled
138
+ const enableCompanyFeatureForEntity = this.storageConfig.isCompanyFeatureEnabled();
139
+ if (enableCompanyFeatureForEntity) {
140
+ mergedFileManager.companyId = user?.companyId ?? null;
134
141
  }
135
- return fileManager;
142
+ return mergedFileManager;
136
143
  }
137
144
  async getSelectQuery(query, _user, select) {
138
145
  if (!select || !select.length) {
@@ -209,7 +216,8 @@ export class FileManagerService extends RequestScopedApiService {
209
216
  this.logger.debug(`enrichWithProviderNames: First enriched item: ${JSON.stringify(enrichedItems[0])}`);
210
217
  return enrichedItems;
211
218
  } catch (error) {
212
- this.logger.warn(`Failed to fetch provider names: ${error}`);
219
+ const errorMessage = ErrorHandler.getErrorMessage(error);
220
+ this.logger.warn(`Failed to fetch provider names: ${errorMessage}`);
213
221
  return items.map((item)=>({
214
222
  ...item,
215
223
  providerName: undefined
@@ -247,13 +255,12 @@ export class FileManagerService extends RequestScopedApiService {
247
255
  * Override: Extra query manipulation - Auto-filter by user's company and private file permissions
248
256
  */ async getExtraManipulateQuery(query, filterDto, user) {
249
257
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
250
- // If company feature enabled and user has companyId, filter by user's company
258
+ // Apply company filter using shared utility
251
259
  const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
252
- if (enableCompanyFeature && user?.companyId) {
253
- query.andWhere('file_manager.companyId = :companyId', {
254
- companyId: user.companyId
255
- });
256
- }
260
+ applyCompanyFilter(query, {
261
+ isCompanyFeatureEnabled: enableCompanyFeature,
262
+ entityAlias: 'file_manager'
263
+ }, user);
257
264
  // Check if user has permission to see private files
258
265
  if (user) {
259
266
  const cacheKey = enableCompanyFeature ? `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}_${user.companyId}` : `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}`;
@@ -316,8 +323,9 @@ export class FileManagerService extends RequestScopedApiService {
316
323
  'config'
317
324
  ]
318
325
  });
319
- if (config && config.config?.basePath) {
320
- const basePath = config.config.basePath.replace(/^\.\//, '');
326
+ const typedConfig = config;
327
+ if (typedConfig?.config?.basePath) {
328
+ const basePath = typedConfig.config.basePath.replace(/^\.\//, '');
321
329
  // Convert old keys to new format
322
330
  deleteKeys = keys.map((key)=>{
323
331
  if (!key.includes('/')) {
@@ -327,7 +335,8 @@ export class FileManagerService extends RequestScopedApiService {
327
335
  });
328
336
  }
329
337
  } catch (error) {
330
- this.logger.warn(`Failed to get basePath for delete: ${error}`);
338
+ const errorMessage = ErrorHandler.getErrorMessage(error);
339
+ this.logger.warn(`Failed to get basePath for delete: ${errorMessage}`);
331
340
  }
332
341
  }
333
342
  await this.uploadService.deleteMultipleFile(deleteKeys, configId === 'default' ? undefined : configId, user ?? undefined, location);
@@ -361,7 +370,8 @@ export class FileManagerService extends RequestScopedApiService {
361
370
  file.expiresAt = now + expiresIn * 1000;
362
371
  shouldUpdate = true;
363
372
  } catch (error) {
364
- this.logger.error(`Failed to generate URL for file ${file.id}: ${error?.message || 'Unknown error'}`);
373
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
374
+ this.logger.error(`Failed to generate URL for file ${file.id}: ${errorMessage}`);
365
375
  // Use fallback URL with appUrl from config
366
376
  const baseUrl = this.getFileBaseUrl(protocol, host);
367
377
  file.url = `${baseUrl}/storage/upload/file/${file.key}`;
@@ -388,13 +398,15 @@ export class FileManagerService extends RequestScopedApiService {
388
398
  'config'
389
399
  ]
390
400
  });
391
- if (config && config.config?.basePath) {
392
- const basePath = config.config.basePath.replace(/^\.\//, ''); // Remove leading ./
401
+ const typedConfig = config;
402
+ if (typedConfig?.config?.basePath) {
403
+ const basePath = typedConfig.config.basePath.replace(/^\.\//, ''); // Remove leading ./
393
404
  fileKey = `${basePath}/${file.key}`;
394
405
  this.logger.debug(`Prefixed old key with basePath: ${fileKey}`);
395
406
  }
396
407
  } catch (error) {
397
- this.logger.warn(`Failed to get basePath for file ${file.id}: ${error}`);
408
+ const errorMessage = ErrorHandler.getErrorMessage(error);
409
+ this.logger.warn(`Failed to get basePath for file ${file.id}: ${errorMessage}`);
398
410
  }
399
411
  }
400
412
  const baseUrl = this.getFileBaseUrl(protocol, host);
@@ -419,7 +431,8 @@ export class FileManagerService extends RequestScopedApiService {
419
431
  try {
420
432
  await this.repository.save(updatedFiles);
421
433
  } catch (error) {
422
- this.logger.error(`Failed to save updated file URLs: ${error?.message || 'Unknown error'}`);
434
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
435
+ this.logger.error(`Failed to save updated file URLs: ${errorMessage}`);
423
436
  }
424
437
  }
425
438
  return responses;
@@ -27,51 +27,27 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
- import { Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
30
+ import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
31
+ import { Inject, Injectable, Scope } from '@nestjs/common';
31
32
  import { StorageConfigService } from '../config';
32
33
  import { Folder, FolderWithCompany } from '../entities';
33
34
  import { StorageDataSourceProvider } from './storage-datasource.provider';
34
35
  export class FolderService extends RequestScopedApiService {
35
- /**
36
- * Resolve entity class for this service
37
- * @returns Folder or FolderWithCompany based on configuration
38
- */ resolveEntity() {
39
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
40
- return enableCompanyFeature ? FolderWithCompany : Folder;
36
+ resolveEntity() {
37
+ return this.storageConfig.isCompanyFeatureEnabled() ? FolderWithCompany : Folder;
41
38
  }
42
- /**
43
- * Get DataSource provider for this service
44
- * @returns StorageDataSourceProvider instance
45
- */ getDataSourceProvider() {
39
+ getDataSourceProvider() {
46
40
  return this.dataSourceProvider;
47
41
  }
48
42
  async convertSingleDtoToEntity(dto, user) {
49
- let folder = {};
50
- // NOTE: Using 'id' in dto check instead of instanceof - instanceof may not work after esbuild bundling
51
- if ('id' in dto && dto.id && typeof dto.id === 'string') {
52
- const dbData = await this.repository.findOne({
53
- where: {
54
- id: dto.id
55
- }
56
- });
57
- if (!dbData) {
58
- throw new NotFoundException('No such entity data found for update! Please, Try Again.');
59
- }
60
- folder = dbData;
61
- }
62
- // Set company/branch IDs if company feature is enabled
63
- folder = {
64
- ...folder,
65
- ...dto
66
- };
67
- // Only set company fields if they exist on the entity (when company feature is enabled)
68
- if ('companyId' in folder) {
69
- folder.companyId = user?.companyId ?? null;
43
+ const entity = await super.convertSingleDtoToEntity(dto, user);
44
+ if (this.storageConfig.isCompanyFeatureEnabled()) {
45
+ entity.companyId = user?.companyId ?? null;
70
46
  }
71
- return folder;
47
+ return entity;
72
48
  }
73
49
  async getSelectQuery(query, _user, select) {
74
- if (!select || !select.length) {
50
+ if (!select?.length) {
75
51
  select = [
76
52
  'id',
77
53
  'name',
@@ -79,35 +55,25 @@ export class FolderService extends RequestScopedApiService {
79
55
  'createdAt',
80
56
  'deletedAt'
81
57
  ];
58
+ if (this.storageConfig.isCompanyFeatureEnabled()) {
59
+ select.push('companyId');
60
+ }
82
61
  }
83
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
84
- // Add company context fields if company feature is enabled
85
- // The entity will have these fields only if company feature is enabled
86
- if (this.storageConfig.isCompanyFeatureEnabled()) {
87
- selectFields.push('folder.companyId');
88
- }
89
- query.select(selectFields);
62
+ query.select(select.map((f)=>`${this.entityName}.${f}`));
90
63
  return {
91
64
  query,
92
65
  isRaw: false
93
66
  };
94
67
  }
95
- /**
96
- * Override: Extra query manipulation - Auto-filter by user's company
97
- */ async getExtraManipulateQuery(query, filterDto, user) {
68
+ async getExtraManipulateQuery(query, filterDto, user) {
98
69
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
99
- // If company feature enabled and user has companyId, filter by user's company
100
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
101
- if (enableCompanyFeature && user?.companyId) {
102
- query.andWhere('folder.companyId = :companyId', {
103
- companyId: user.companyId
104
- });
105
- }
70
+ applyCompanyFilter(query, {
71
+ isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
72
+ entityAlias: 'folder'
73
+ }, user);
106
74
  return result;
107
75
  }
108
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
109
76
  constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
110
- // Repository will be set asynchronously by RequestScopedApiService
111
77
  super('folder', null, cacheManager, utilsService, FolderService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "storageConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.storageConfig = storageConfig, this.dataSourceProvider = dataSourceProvider;
112
78
  }
113
79
  }
@@ -32,10 +32,8 @@ import { Request } from 'express';
32
32
  import { StorageModuleOptions } from '../interfaces';
33
33
  import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
34
34
  export class StorageDataSourceProvider extends MultiTenantDataSourceService {
35
- // ==================== Factory Methods ====================
36
- /**
37
- * Build parent options from StorageModuleOptions
38
- */ static buildParentOptions(options) {
35
+ // Factory Methods
36
+ /** Build parent options from StorageModuleOptions */ static buildParentOptions(options) {
39
37
  return {
40
38
  bootstrapAppConfig: options.bootstrapAppConfig,
41
39
  defaultDatabaseConfig: options.config?.defaultDatabaseConfig,
@@ -43,10 +41,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
43
41
  tenants: options.config?.tenants
44
42
  };
45
43
  }
46
- // ==================== Feature Flags ====================
47
- /**
48
- * Get global enable company feature flag
49
- */ getEnableCompanyFeature() {
44
+ // Feature Flags
45
+ /** Get global enable company feature flag */ getEnableCompanyFeature() {
50
46
  return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
51
47
  }
52
48
  /**
@@ -60,11 +56,11 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
60
56
  */ getEnableCompanyFeatureForCurrentTenant() {
61
57
  return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
62
58
  }
63
- // ==================== Entity Management ====================
59
+ // Entity Management
64
60
  /**
65
- * Get storage entities for migrations based on company feature flag
66
- * Note: For TypeORM repositories, we always use the base entities (FileManager, etc.)
67
- * But for migrations, we need the correct entity based on the feature flag
61
+ * Get storage entities for migrations based on company feature flag.
62
+ * For TypeORM repositories, we always use the base entities (FileManager, etc.)
63
+ * but for migrations, we need the correct entity based on the feature flag.
68
64
  */ async getStorageEntities(enableCompanyFeature) {
69
65
  const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
70
66
  const { FileManager, Folder, StorageConfig } = await import('../entities');
@@ -84,10 +80,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
84
80
  StorageConfig
85
81
  ];
86
82
  }
87
- // ==================== Overrides ====================
88
- /**
89
- * Override to dynamically set entities based on tenant config
90
- */ async createDataSourceFromConfig(config) {
83
+ // Overrides
84
+ /** Override to dynamically set entities based on tenant config */ async createDataSourceFromConfig(config) {
91
85
  const currentTenant = this.getCurrentTenant();
92
86
  const enableCompanyFeature = this.getEnableCompanyFeatureForTenant(currentTenant ?? undefined);
93
87
  const entities = await this.getStorageEntities(enableCompanyFeature);
@@ -27,78 +27,57 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
+ import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
30
31
  import { Inject, Injectable, Scope } from '@nestjs/common';
31
32
  import { StorageConfigService } from '../config';
32
33
  import { StorageConfig, StorageConfigWithCompany } from '../entities';
33
34
  import { StorageDataSourceProvider } from './storage-datasource.provider';
34
35
  export class StorageProviderConfigService extends RequestScopedApiService {
35
- /**
36
- * Resolve entity class for this service
37
- * @returns StorageConfig or StorageConfigWithCompany based on configuration
38
- */ resolveEntity() {
39
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
40
- return enableCompanyFeature ? StorageConfigWithCompany : StorageConfig;
36
+ resolveEntity() {
37
+ return this.storageConfig.isCompanyFeatureEnabled() ? StorageConfigWithCompany : StorageConfig;
41
38
  }
42
- /**
43
- * Get DataSource provider for this service
44
- * @returns StorageDataSourceProvider instance
45
- */ getDataSourceProvider() {
39
+ getDataSourceProvider() {
46
40
  return this.dataSourceProvider;
47
41
  }
48
42
  async convertSingleDtoToEntity(dto, user) {
49
- let storageConfig = {};
50
- // Set basic fields
51
- storageConfig = {
52
- ...storageConfig,
53
- ...dto
54
- };
55
- // Only set company fields if they exist on the entity (when company feature is enabled)
56
- if ('companyId' in storageConfig) {
57
- storageConfig.companyId = user?.companyId ?? null;
43
+ const entity = await super.convertSingleDtoToEntity(dto, user);
44
+ if (this.storageConfig.isCompanyFeatureEnabled()) {
45
+ entity.companyId = user?.companyId ?? null;
58
46
  }
59
- return storageConfig;
47
+ return entity;
60
48
  }
61
49
  async getSelectQuery(query, _user, select) {
62
- if (!select || !select.length) {
50
+ if (!select?.length) {
63
51
  select = [
64
52
  'id',
65
53
  'name',
66
54
  'storage',
67
55
  'config',
56
+ 'isActive',
57
+ 'isDefault',
68
58
  'createdAt',
69
59
  'updatedAt'
70
60
  ];
71
- // Add company fields if company feature is enabled
72
61
  if (this.storageConfig.isCompanyFeatureEnabled()) {
73
62
  select.push('companyId');
74
63
  }
75
64
  }
76
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
77
- query.select(selectFields);
65
+ query.select(select.map((f)=>`${this.entityName}.${f}`));
78
66
  return {
79
67
  query,
80
68
  isRaw: false
81
69
  };
82
70
  }
83
- /**
84
- * Override: Extra query manipulation - Auto-filter by user's company
85
- */ async getExtraManipulateQuery(query, filterDto, user) {
71
+ async getExtraManipulateQuery(query, filterDto, user) {
86
72
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
87
- // If company feature enabled and user has companyId, filter by user's company
88
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
89
- if (enableCompanyFeature && user?.companyId) {
90
- query.andWhere('storageConfig.companyId = :companyId', {
91
- companyId: user.companyId
92
- });
93
- }
73
+ applyCompanyFilter(query, {
74
+ isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
75
+ entityAlias: 'storageConfig'
76
+ }, user);
94
77
  query.orderBy(`${this.entityName}.createdAt`, 'DESC');
95
78
  return result;
96
79
  }
97
- /**
98
- * Find storage config by ID without throwing (returns null if not found)
99
- * Uses direct repository query - bypasses company filtering
100
- * Use for internal operations like file deletion where config ID is already known
101
- */ async findByIdDirect(id) {
80
+ async findByIdDirect(id) {
102
81
  await this.ensureRepositoryInitialized();
103
82
  return await this.repository.findOne({
104
83
  where: {
@@ -106,21 +85,15 @@ export class StorageProviderConfigService extends RequestScopedApiService {
106
85
  }
107
86
  });
108
87
  }
109
- /**
110
- * Get default storage configuration (scoped to user's company if enabled)
111
- * Falls back to any available config if 'default' not found
112
- */ async getDefaultConfig(user) {
88
+ async getDefaultConfig(user) {
113
89
  await this.ensureRepositoryInitialized();
114
- const baseWhere = {};
115
- // Filter by company only if company feature is enabled and user is provided
116
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
117
- baseWhere.companyId = user.companyId;
118
- }
119
- // First try to find config named 'default'
90
+ const baseWhere = buildCompanyWhereCondition({
91
+ isActive: true
92
+ }, this.storageConfig.isCompanyFeatureEnabled(), user);
120
93
  const defaultConfig = await this.repository.findOne({
121
94
  where: {
122
95
  ...baseWhere,
123
- name: 'default'
96
+ isDefault: true
124
97
  },
125
98
  order: {
126
99
  createdAt: 'ASC'
@@ -129,7 +102,6 @@ export class StorageProviderConfigService extends RequestScopedApiService {
129
102
  if (defaultConfig) {
130
103
  return defaultConfig;
131
104
  }
132
- // Fall back to any available config for this company/user
133
105
  return await this.repository.findOne({
134
106
  where: baseWhere,
135
107
  order: {
@@ -137,24 +109,17 @@ export class StorageProviderConfigService extends RequestScopedApiService {
137
109
  }
138
110
  });
139
111
  }
140
- /**
141
- * Get storage configuration by type (scoped to user's company if enabled)
142
- */ async getConfigByType(storage, user) {
112
+ async getConfigByType(storage, user) {
143
113
  await this.ensureRepositoryInitialized();
144
- const where = {
145
- storage
146
- };
147
- // Filter by company only if company feature is enabled and user is provided
148
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
149
- where.companyId = user.companyId;
150
- }
114
+ const where = buildCompanyWhereCondition({
115
+ storage,
116
+ isActive: true
117
+ }, this.storageConfig.isCompanyFeatureEnabled(), user);
151
118
  return await this.repository.find({
152
119
  where
153
120
  });
154
121
  }
155
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
156
122
  constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
157
- // Repository will be set asynchronously by RequestScopedApiService
158
123
  super('storageConfig', null, cacheManager, utilsService, StorageProviderConfigService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "storageConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.storageConfig = storageConfig, this.dataSourceProvider = dataSourceProvider;
159
124
  }
160
125
  }