@flusys/nestjs-shared 1.1.0-beta → 1.1.0

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 (113) hide show
  1. package/README.md +504 -724
  2. package/cjs/classes/api-controller.class.js +9 -24
  3. package/cjs/classes/api-service.class.js +59 -92
  4. package/cjs/classes/index.js +1 -0
  5. package/cjs/classes/winston-logger-adapter.class.js +23 -40
  6. package/cjs/constants/index.js +14 -0
  7. package/cjs/constants/permissions.js +184 -0
  8. package/cjs/decorators/api-response.decorator.js +1 -1
  9. package/cjs/decorators/index.js +1 -0
  10. package/cjs/decorators/sanitize-html.decorator.js +36 -0
  11. package/cjs/dtos/delete.dto.js +10 -0
  12. package/cjs/dtos/filter-and-pagination.dto.js +24 -34
  13. package/cjs/dtos/pagination.dto.js +4 -8
  14. package/cjs/dtos/response-payload.dto.js +0 -116
  15. package/cjs/entities/identity.js +4 -4
  16. package/cjs/entities/user-root.js +13 -14
  17. package/cjs/guards/permission.guard.js +51 -105
  18. package/cjs/interceptors/index.js +1 -3
  19. package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
  20. package/cjs/interceptors/slug.interceptor.js +30 -9
  21. package/cjs/interfaces/datasource.interface.js +4 -0
  22. package/cjs/interfaces/index.js +2 -1
  23. package/cjs/interfaces/module-config.interface.js +4 -0
  24. package/cjs/middlewares/logger.middleware.js +50 -89
  25. package/cjs/modules/cache/cache.module.js +3 -3
  26. package/cjs/modules/datasource/datasource.module.js +11 -14
  27. package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
  28. package/cjs/modules/utils/utils.service.js +40 -203
  29. package/cjs/utils/error-handler.util.js +35 -12
  30. package/cjs/utils/html-sanitizer.util.js +64 -0
  31. package/cjs/utils/index.js +4 -0
  32. package/cjs/utils/query-helpers.util.js +53 -0
  33. package/cjs/utils/request.util.js +71 -0
  34. package/cjs/utils/string.util.js +63 -0
  35. package/classes/api-controller.class.d.ts +5 -5
  36. package/classes/api-service.class.d.ts +7 -5
  37. package/classes/index.d.ts +1 -0
  38. package/classes/request-scoped-api.service.d.ts +3 -2
  39. package/classes/winston-logger-adapter.class.d.ts +2 -0
  40. package/constants/index.d.ts +1 -0
  41. package/constants/permissions.d.ts +179 -0
  42. package/decorators/index.d.ts +1 -0
  43. package/decorators/sanitize-html.decorator.d.ts +2 -0
  44. package/dtos/delete.dto.d.ts +1 -0
  45. package/dtos/filter-and-pagination.dto.d.ts +0 -2
  46. package/dtos/response-payload.dto.d.ts +0 -20
  47. package/fesm/classes/api-controller.class.js +9 -24
  48. package/fesm/classes/api-service.class.js +59 -92
  49. package/fesm/classes/index.js +2 -0
  50. package/fesm/classes/winston-logger-adapter.class.js +23 -40
  51. package/fesm/constants/index.js +2 -0
  52. package/fesm/constants/permissions.js +128 -0
  53. package/fesm/decorators/api-response.decorator.js +1 -1
  54. package/fesm/decorators/index.js +1 -0
  55. package/fesm/decorators/sanitize-html.decorator.js +45 -0
  56. package/fesm/dtos/delete.dto.js +12 -2
  57. package/fesm/dtos/filter-and-pagination.dto.js +26 -47
  58. package/fesm/dtos/pagination.dto.js +4 -8
  59. package/fesm/dtos/response-payload.dto.js +0 -107
  60. package/fesm/entities/identity.js +4 -4
  61. package/fesm/entities/user-root.js +13 -14
  62. package/fesm/guards/permission.guard.js +51 -105
  63. package/fesm/interceptors/index.js +1 -3
  64. package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
  65. package/fesm/interceptors/slug.interceptor.js +31 -10
  66. package/fesm/interfaces/datasource.interface.js +20 -0
  67. package/fesm/interfaces/index.js +2 -1
  68. package/fesm/interfaces/module-config.interface.js +5 -0
  69. package/fesm/middlewares/logger.middleware.js +50 -83
  70. package/fesm/modules/cache/cache.module.js +2 -2
  71. package/fesm/modules/datasource/datasource.module.js +11 -14
  72. package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
  73. package/fesm/modules/utils/utils.service.js +41 -204
  74. package/fesm/utils/error-handler.util.js +36 -13
  75. package/fesm/utils/html-sanitizer.util.js +69 -0
  76. package/fesm/utils/index.js +4 -0
  77. package/fesm/utils/query-helpers.util.js +78 -0
  78. package/fesm/utils/request.util.js +59 -0
  79. package/fesm/utils/string.util.js +71 -0
  80. package/guards/permission.guard.d.ts +2 -0
  81. package/interceptors/index.d.ts +1 -3
  82. package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
  83. package/interceptors/slug.interceptor.d.ts +2 -1
  84. package/interfaces/api.interface.d.ts +2 -2
  85. package/interfaces/datasource.interface.d.ts +5 -0
  86. package/interfaces/identity.interface.d.ts +4 -4
  87. package/interfaces/index.d.ts +2 -1
  88. package/interfaces/logged-user-info.interface.d.ts +0 -2
  89. package/interfaces/module-config.interface.d.ts +6 -0
  90. package/interfaces/permission.interface.d.ts +0 -1
  91. package/middlewares/logger.middleware.d.ts +2 -2
  92. package/modules/datasource/datasource.module.d.ts +1 -0
  93. package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
  94. package/modules/utils/utils.service.d.ts +4 -14
  95. package/package.json +4 -4
  96. package/utils/error-handler.util.d.ts +14 -19
  97. package/utils/html-sanitizer.util.d.ts +2 -0
  98. package/utils/index.d.ts +4 -0
  99. package/utils/query-helpers.util.d.ts +16 -0
  100. package/utils/request.util.d.ts +4 -0
  101. package/utils/string.util.d.ts +2 -0
  102. package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
  103. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
  104. package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
  105. package/cjs/interfaces/base-query.interface.js +0 -6
  106. package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
  107. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
  108. package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
  109. package/fesm/interfaces/base-query.interface.js +0 -3
  110. package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
  111. package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
  112. package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
  113. package/interfaces/base-query.interface.d.ts +0 -7
@@ -0,0 +1,69 @@
1
+ /**
2
+ * HTML Sanitizer Utilities
3
+ *
4
+ * Provides functions for escaping HTML content to prevent XSS attacks.
5
+ * Use these utilities when interpolating user-provided variables into HTML content.
6
+ */ /**
7
+ * HTML entity mapping for escaping special characters
8
+ */ const HTML_ESCAPE_MAP = {
9
+ '&': '&',
10
+ '<': '&lt;',
11
+ '>': '&gt;',
12
+ '"': '&quot;',
13
+ "'": '&#x27;',
14
+ '/': '&#x2F;',
15
+ '`': '&#x60;',
16
+ '=': '&#x3D;'
17
+ };
18
+ /**
19
+ * Regex pattern matching characters that need HTML escaping
20
+ */ const HTML_ESCAPE_REGEX = /[&<>"'`=/]/g;
21
+ /**
22
+ * Escapes HTML special characters to prevent XSS attacks.
23
+ *
24
+ * @param str - The string to escape
25
+ * @returns The escaped string safe for HTML insertion
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * escapeHtml('<script>alert("xss")</script>')
30
+ * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
31
+ * ```
32
+ */ export function escapeHtml(str) {
33
+ if (!str || typeof str !== 'string') {
34
+ return str ?? '';
35
+ }
36
+ return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]);
37
+ }
38
+ /**
39
+ * Escapes all string values in a variables object for safe HTML interpolation.
40
+ * Non-string values are converted to strings and escaped.
41
+ *
42
+ * @param variables - Object containing template variables
43
+ * @returns New object with all string values HTML-escaped
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * escapeHtmlVariables({ name: '<script>xss</script>', count: 5 })
48
+ * // Returns: { name: '&lt;script&gt;xss&lt;/script&gt;', count: '5' }
49
+ * ```
50
+ */ export function escapeHtmlVariables(variables) {
51
+ if (!variables || typeof variables !== 'object') {
52
+ return {};
53
+ }
54
+ const escaped = {};
55
+ for (const [key, value] of Object.entries(variables)){
56
+ if (value === null || value === undefined) {
57
+ escaped[key] = '';
58
+ } else if (typeof value === 'string') {
59
+ escaped[key] = escapeHtml(value);
60
+ } else if (typeof value === 'object') {
61
+ // For objects/arrays, stringify and escape
62
+ escaped[key] = escapeHtml(JSON.stringify(value));
63
+ } else {
64
+ // For numbers, booleans, etc., convert to string (no escaping needed)
65
+ escaped[key] = String(value);
66
+ }
67
+ }
68
+ return escaped;
69
+ }
@@ -1 +1,5 @@
1
1
  export * from './error-handler.util';
2
+ export * from './html-sanitizer.util';
3
+ export * from './query-helpers.util';
4
+ export * from './request.util';
5
+ export * from './string.util';
@@ -0,0 +1,78 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ /**
3
+ * Apply company filter to a query builder if company feature is enabled.
4
+ * Centralizes the common pattern of filtering by user's company.
5
+ *
6
+ * @param query - TypeORM SelectQueryBuilder
7
+ * @param config - Company filter configuration
8
+ * @param user - Current logged user (may be null)
9
+ * @returns The modified query builder
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * applyCompanyFilter(query, {
14
+ * isCompanyFeatureEnabled: this.config.isCompanyFeatureEnabled(),
15
+ * entityAlias: 'file_manager',
16
+ * }, user);
17
+ * ```
18
+ */ export function applyCompanyFilter(query, config, user) {
19
+ const columnName = config.columnName ?? 'companyId';
20
+ if (config.isCompanyFeatureEnabled && user?.companyId) {
21
+ query.andWhere(`${config.entityAlias}.${columnName} = :companyId`, {
22
+ companyId: user.companyId
23
+ });
24
+ }
25
+ return query;
26
+ }
27
+ /**
28
+ * Build a where condition object with optional company filter.
29
+ *
30
+ * @param baseWhere - Base where condition object
31
+ * @param isCompanyFeatureEnabled - Whether company feature is enabled
32
+ * @param user - Current logged user
33
+ * @returns Where condition with companyId added if applicable
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const where = buildCompanyWhereCondition(
38
+ * { id: dto.id },
39
+ * this.config.isCompanyFeatureEnabled(),
40
+ * user
41
+ * );
42
+ * // Returns { id: dto.id, companyId: user.companyId } if company feature enabled
43
+ * ```
44
+ */ export function buildCompanyWhereCondition(baseWhere, isCompanyFeatureEnabled, user) {
45
+ if (isCompanyFeatureEnabled && user?.companyId) {
46
+ return {
47
+ ...baseWhere,
48
+ companyId: user.companyId
49
+ };
50
+ }
51
+ return baseWhere;
52
+ }
53
+ /**
54
+ * Type guard to check if entity has companyId property
55
+ */ export function hasCompanyId(entity) {
56
+ return entity !== null && typeof entity === 'object' && 'companyId' in entity;
57
+ }
58
+ /**
59
+ * Validates that an entity with companyId belongs to the user's company.
60
+ * Throws BadRequestException if the entity belongs to another company.
61
+ *
62
+ * @param entity - Entity to validate
63
+ * @param user - Current logged user
64
+ * @param isCompanyFeatureEnabled - Whether company feature is enabled
65
+ * @param entityName - Name of entity for error message (e.g., 'Email configuration')
66
+ * @throws BadRequestException if entity belongs to another company
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * validateCompanyOwnership(config, user, this.config.isCompanyFeatureEnabled(), 'Email configuration');
71
+ * ```
72
+ */ export function validateCompanyOwnership(entity, user, isCompanyFeatureEnabled, entityName) {
73
+ if (isCompanyFeatureEnabled && user?.companyId && hasCompanyId(entity)) {
74
+ if (entity.companyId && entity.companyId !== user.companyId) {
75
+ throw new BadRequestException(`${entityName} belongs to another company`);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,59 @@
1
+ import { envConfig } from '@flusys/nestjs-core/config';
2
+ import { CLIENT_TYPE_HEADER } from '../constants';
3
+ /** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
4
+ s: 1000,
5
+ m: 60 * 1000,
6
+ h: 60 * 60 * 1000,
7
+ d: 24 * 60 * 60 * 1000,
8
+ w: 7 * 24 * 60 * 60 * 1000
9
+ };
10
+ /** Get normalized client type from request header */ function getClientType(req) {
11
+ const clientType = req.headers[CLIENT_TYPE_HEADER];
12
+ return clientType ? clientType.toLowerCase() : null;
13
+ }
14
+ /**
15
+ * Detect if request is from a browser client.
16
+ * Uses x-client-type header first, then falls back to user-agent detection.
17
+ */ export function isBrowserRequest(req) {
18
+ const clientType = getClientType(req);
19
+ if (clientType) {
20
+ return clientType === 'browser' || clientType === 'web';
21
+ }
22
+ const accept = req.headers['accept'] || '';
23
+ if (accept.includes('text/html')) return true;
24
+ const userAgent = req.headers['user-agent'] || '';
25
+ const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
26
+ return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
27
+ }
28
+ /**
29
+ * Build secure cookie options based on request context.
30
+ * Handles HTTPS detection for TLS-terminating proxies.
31
+ */ export function buildCookieOptions(req) {
32
+ const hostname = req.hostname || '';
33
+ const origin = req.headers.origin || '';
34
+ const isProduction = envConfig.isProduction();
35
+ const forwardedProto = req.headers['x-forwarded-proto'];
36
+ const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
37
+ let domain;
38
+ const domainParts = hostname.split('.');
39
+ if (domainParts.length > 2 && !hostname.includes('localhost')) {
40
+ domain = '.' + domainParts.slice(-2).join('.');
41
+ }
42
+ return {
43
+ secure: isHttps,
44
+ sameSite: isHttps ? 'strict' : 'lax',
45
+ ...domain && {
46
+ domain
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Parse duration string to milliseconds.
52
+ * @example '7d' → 604800000, '24h' → 86400000, '30m' → 1800000
53
+ */ export function parseDurationToMs(duration, defaultMs = TIME_UNIT_MS.w) {
54
+ const match = duration.match(/^(\d+)(s|m|h|d|w)$/);
55
+ if (!match) return defaultMs;
56
+ const value = parseInt(match[1], 10);
57
+ const unit = match[2];
58
+ return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
59
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * String utility functions
3
+ */ /**
4
+ * Generate URL-friendly slug from a string
5
+ * @param text - The text to convert to slug
6
+ * @param maxLength - Maximum length of the slug (default: 100)
7
+ * @returns URL-friendly slug
8
+ *
9
+ * @example
10
+ * generateSlug('My Company Name') // 'my-company-name'
11
+ * generateSlug('Hello World!') // 'hello-world'
12
+ */ export function generateSlug(text, maxLength = 100) {
13
+ return text.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '') // Remove special characters
14
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
15
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
16
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
17
+ .substring(0, maxLength);
18
+ }
19
+ /**
20
+ * Generate unique slug using single database query (optimized for large datasets)
21
+ * Appends numeric suffix if collision exists (e.g., 'my-company-1', 'my-company-2')
22
+ *
23
+ * Performance: O(1) database query regardless of collision count
24
+ * - Works efficiently with millions of records
25
+ * - Requires index on slug column for best performance
26
+ *
27
+ * @param text - The text to convert to slug
28
+ * @param findMatchingSlugs - Async function that returns all slugs matching base pattern
29
+ * Should query: WHERE slug = baseSlug OR slug LIKE 'baseSlug-%'
30
+ * @param maxLength - Maximum length of the base slug (default: 100)
31
+ * @returns Unique URL-friendly slug
32
+ *
33
+ * @example
34
+ * const slug = await generateUniqueSlug('My Company', async (baseSlug) => {
35
+ * return repo
36
+ * .createQueryBuilder('c')
37
+ * .select('c.slug')
38
+ * .where('c.slug = :base OR c.slug LIKE :pattern', {
39
+ * base: baseSlug,
40
+ * pattern: `${baseSlug}-%`,
41
+ * })
42
+ * .getMany()
43
+ * .then(rows => rows.map(r => r.slug));
44
+ * });
45
+ */ export async function generateUniqueSlug(text, findMatchingSlugs, maxLength = 100) {
46
+ const baseSlug = generateSlug(text, maxLength);
47
+ // Single query to get all matching slugs
48
+ const existingSlugs = await findMatchingSlugs(baseSlug);
49
+ // No collisions - base slug is available
50
+ if (existingSlugs.length === 0) {
51
+ return baseSlug;
52
+ }
53
+ // Build set for O(1) lookup
54
+ const existingSet = new Set(existingSlugs);
55
+ // Base slug not taken (only suffixed versions exist)
56
+ if (!existingSet.has(baseSlug)) {
57
+ return baseSlug;
58
+ }
59
+ // Find next available suffix in memory (fast)
60
+ let counter = 1;
61
+ while(counter < 10000){
62
+ const slugWithSuffix = `${baseSlug}-${counter}`;
63
+ if (!existingSet.has(slugWithSuffix)) {
64
+ return slugWithSuffix;
65
+ }
66
+ counter++;
67
+ }
68
+ // Fallback: append random string (extremely rare edge case)
69
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
70
+ return `${baseSlug}-${randomSuffix}`;
71
+ }
@@ -11,8 +11,10 @@ export declare class PermissionGuard implements CanActivate {
11
11
  constructor(reflector: Reflector, cache?: HybridCache, config?: PermissionGuardConfig, logger?: ILogger);
12
12
  canActivate(context: ExecutionContext): Promise<boolean>;
13
13
  private normalizePermissionConfig;
14
+ private validateSimplePermissions;
14
15
  private isNestedCondition;
15
16
  private evaluateCondition;
16
17
  private getUserPermissions;
18
+ private buildPermissionCacheKey;
17
19
  private hasPermission;
18
20
  }
@@ -2,7 +2,5 @@ export * from './delete-empty-id-from-body.interceptor';
2
2
  export * from './idempotency.interceptor';
3
3
  export * from './query-performance.interceptor';
4
4
  export * from './response-meta.interceptor';
5
- export * from './set-create-by-on-body.interceptor';
6
- export * from './set-delete-by-on-body.interceptor';
7
- export * from './set-update-by-on-body.interceptor';
5
+ export * from './set-user-field-on-body.interceptor';
8
6
  export * from './slug.interceptor';
@@ -0,0 +1,5 @@
1
+ import { NestInterceptor, Type } from '@nestjs/common';
2
+ export declare function createSetUserFieldInterceptor(fieldName: string): Type<NestInterceptor>;
3
+ export declare const SetCreatedByOnBody: Type<NestInterceptor<any, any>>;
4
+ export declare const SetUpdateByOnBody: Type<NestInterceptor<any, any>>;
5
+ export declare const SetDeletedByOnBody: Type<NestInterceptor<any, any>>;
@@ -2,6 +2,7 @@ import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2
2
  import { Observable } from 'rxjs';
3
3
  import { UtilsService } from '../modules/utils/utils.service';
4
4
  export declare class Slug implements NestInterceptor {
5
- utilsService: UtilsService;
5
+ private readonly utilsService;
6
+ constructor(utilsService: UtilsService);
6
7
  intercept<T = any>(context: ExecutionContext, next: CallHandler<T>): Observable<T>;
7
8
  }
@@ -1,5 +1,5 @@
1
- import { DeleteDto, FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
2
- import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
1
+ import { DeleteDto, FilterAndPaginationDto } from '../dtos';
2
+ import { ILoggedUserInfo } from './logged-user-info.interface';
3
3
  export interface IService<CreateDtoT, UpdateDtoT, InterfaceT> {
4
4
  insert(addDto: CreateDtoT, user: ILoggedUserInfo | null): Promise<InterfaceT>;
5
5
  insertMany(addDto: Array<CreateDtoT>, user: ILoggedUserInfo | null): Promise<Array<InterfaceT>>;
@@ -0,0 +1,5 @@
1
+ import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
2
+ export interface IDataSourceProvider {
3
+ getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>>;
4
+ getDataSource(): Promise<DataSource>;
5
+ }
@@ -2,8 +2,8 @@ export interface IIdentity {
2
2
  id: string;
3
3
  createdAt: Date;
4
4
  updatedAt: Date;
5
- deletedAt: Date;
6
- createdById: string;
7
- updatedById: string;
8
- deletedById: string;
5
+ deletedAt: Date | null;
6
+ createdById: string | null;
7
+ updatedById: string | null;
8
+ deletedById: string | null;
9
9
  }
@@ -1,6 +1,7 @@
1
1
  export * from './api.interface';
2
- export * from './base-query.interface';
2
+ export * from './datasource.interface';
3
3
  export * from './identity.interface';
4
4
  export * from './logged-user-info.interface';
5
5
  export * from './logger.interface';
6
+ export * from './module-config.interface';
6
7
  export * from './permission.interface';
@@ -6,6 +6,4 @@ export interface ILoggedUserInfo {
6
6
  profilePictureId?: string;
7
7
  companyId?: string;
8
8
  branchId?: string;
9
- companyLogoId?: string;
10
- branchLogoId?: string;
11
9
  }
@@ -0,0 +1,6 @@
1
+ import { DatabaseMode } from '@flusys/nestjs-core';
2
+ export interface IModuleConfigService {
3
+ getDatabaseMode(): DatabaseMode;
4
+ isMultiTenant(): boolean;
5
+ isCompanyFeatureEnabled(): boolean;
6
+ }
@@ -11,7 +11,6 @@ export interface LegacyPermissionConfig {
11
11
  export type PermissionConfig = PermissionCondition | LegacyPermissionConfig | string[];
12
12
  export interface PermissionGuardConfig {
13
13
  enableCompanyFeature?: boolean;
14
- cacheKeyPrefix?: string;
15
14
  userPermissionKeyFormat?: string;
16
15
  companyPermissionKeyFormat?: string;
17
16
  }
@@ -13,11 +13,11 @@ export declare const getRequestId: () => string | undefined;
13
13
  export declare const getTenantId: () => string | undefined;
14
14
  export declare const getUserId: () => string | undefined;
15
15
  export declare const getCompanyId: () => string | undefined;
16
- export declare const setUserId: (userId: string) => void;
17
- export declare const setCompanyId: (companyId: string) => void;
18
16
  export declare class LoggerMiddleware implements NestMiddleware {
19
17
  private readonly logger;
20
18
  use(req: Request, res: Response, next: NextFunction): void;
19
+ private setupResponseLogging;
21
20
  private logRequest;
22
21
  private logResponse;
22
+ private buildBaseLogData;
23
23
  }
@@ -15,4 +15,5 @@ export declare class DataSourceModule {
15
15
  static forRootAsync(asyncOptions: DataSourceModuleAsyncOptions): DynamicModule;
16
16
  static forFeature(): DynamicModule;
17
17
  private static createAsyncProviders;
18
+ private static createFactoryProvider;
18
19
  }
@@ -28,7 +28,6 @@ export declare class MultiTenantDataSourceService implements OnModuleDestroy {
28
28
  getDataSourceForTenant(tenantId: string): Promise<DataSource>;
29
29
  setDataSource(dataSource: DataSource): void;
30
30
  getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>>;
31
- getRepositoryForTenant<T extends ObjectLiteral>(entity: EntityTarget<T>, tenantId: string): Promise<Repository<T>>;
32
31
  withTenant<T>(tenantId: string, callback: (ds: DataSource) => Promise<T>): Promise<T>;
33
32
  forAllTenants<T>(callback: (tenant: ITenantDatabaseConfig, ds: DataSource) => Promise<T>): Promise<Map<string, T>>;
34
33
  registerTenant(tenant: ITenantDatabaseConfig): void;
@@ -1,21 +1,11 @@
1
- import { HybridCache } from '@flusys/nestjs-shared/classes';
1
+ import { HybridCache } from '../../classes/hybrid-cache.class';
2
2
  export declare class UtilsService {
3
- constructor();
3
+ private readonly logger;
4
4
  getCacheKey(entityName: string, params: any, entityId?: string, tenantId?: string): string;
5
5
  trackCacheKey(cacheKey: string, entityName: string, cacheManager: HybridCache, entityId?: string, tenantId?: string): Promise<void>;
6
6
  clearCache(entityName: string, cacheManager: HybridCache, entityId?: string, tenantId?: string): Promise<void>;
7
- checkPhoneOrEmail(value: string): {
8
- value: string | null;
9
- type: 'phone' | 'email' | null;
10
- };
11
7
  transformToSlug(value: string, salt?: boolean): string;
12
8
  getRandomInt(min: number, max: number): number;
13
- generateRandomId(length: number): string;
14
- getRandomOtpCode(): number;
15
- extractColumnNameFromError(detail: string): {
16
- columnName: string;
17
- value: string;
18
- };
19
- getOtpEmailFormat(otp: number, userName?: string | null | undefined): string;
20
- getResetPasswordEmailFormat(resetLink: string, userName?: string | null | undefined): string;
9
+ private buildTenantPrefix;
10
+ private buildTrackingKey;
21
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-shared",
3
- "version": "1.1.0-beta",
3
+ "version": "1.1.0",
4
4
  "description": "Common shared utilities for Flusys NestJS applications",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -88,7 +88,7 @@
88
88
  }
89
89
  },
90
90
  "peerDependencies": {
91
- "@keyv/redis": "^3.0.0",
91
+ "@keyv/redis": "^5.0.0",
92
92
  "@nestjs/common": "^10.0.0 || ^11.0.0",
93
93
  "@nestjs/config": "^3.0.0 || ^4.0.0",
94
94
  "@nestjs/core": "^10.0.0 || ^11.0.0",
@@ -96,7 +96,7 @@
96
96
  "@nestjs/passport": "^10.0.0 || ^11.0.0",
97
97
  "@nestjs/swagger": "^7.0.0 || ^11.0.0",
98
98
  "@nestjs/typeorm": "^10.0.0 || ^11.0.0",
99
- "cacheable": "^1.0.0",
99
+ "cacheable": "^2.0.0",
100
100
  "class-transformer": "^0.5.0",
101
101
  "class-validator": "^0.14.0",
102
102
  "keyv": "^5.0.0",
@@ -105,6 +105,6 @@
105
105
  "winston-daily-rotate-file": "^5.0.0"
106
106
  },
107
107
  "dependencies": {
108
- "@flusys/nestjs-core": "1.1.0-beta"
108
+ "@flusys/nestjs-core": "1.1.0"
109
109
  }
110
110
  }
@@ -1,24 +1,19 @@
1
1
  import { Logger } from '@nestjs/common';
2
+ export interface IErrorContext {
3
+ operation?: string;
4
+ entity?: string;
5
+ userId?: string;
6
+ id?: string;
7
+ companyId?: string;
8
+ branchId?: string;
9
+ sectionId?: string;
10
+ data?: Record<string, unknown>;
11
+ }
2
12
  export declare class ErrorHandler {
3
13
  static getErrorMessage(error: unknown): string;
4
- static getErrorStack(error: unknown): string | undefined;
5
- static createErrorContext(error: unknown, context?: {
6
- operation?: string;
7
- entity?: string;
8
- userId?: string;
9
- [key: string]: unknown;
10
- }): {
11
- error: {
12
- message: string;
13
- stack?: string;
14
- name?: string;
15
- };
16
- context?: Record<string, unknown>;
17
- };
18
- static logError(logger: Logger, error: unknown, operation: string, context?: {
19
- entity?: string;
20
- userId?: string;
21
- [key: string]: unknown;
22
- }): void;
14
+ private static sanitizeContextForLogging;
15
+ private static createErrorContext;
16
+ static logError(logger: Logger, error: unknown, operation: string, context?: Omit<IErrorContext, 'operation'>): void;
23
17
  static rethrowError(error: unknown): never;
18
+ static logAndRethrow(logger: Logger, error: unknown, operation: string, context?: Omit<IErrorContext, 'operation'>): never;
24
19
  }
@@ -0,0 +1,2 @@
1
+ export declare function escapeHtml(str: string): string;
2
+ export declare function escapeHtmlVariables(variables: Record<string, unknown>): Record<string, string>;
package/utils/index.d.ts CHANGED
@@ -1 +1,5 @@
1
1
  export * from './error-handler.util';
2
+ export * from './html-sanitizer.util';
3
+ export * from './query-helpers.util';
4
+ export * from './request.util';
5
+ export * from './string.util';
@@ -0,0 +1,16 @@
1
+ import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
2
+ import { ILoggedUserInfo } from '../interfaces';
3
+ export interface ICompanyFilterConfig {
4
+ isCompanyFeatureEnabled: boolean;
5
+ entityAlias: string;
6
+ columnName?: string;
7
+ }
8
+ export declare function applyCompanyFilter<T extends ObjectLiteral>(query: SelectQueryBuilder<T>, config: ICompanyFilterConfig, user: ILoggedUserInfo | null | undefined): SelectQueryBuilder<T>;
9
+ export declare function buildCompanyWhereCondition<T extends Record<string, unknown>>(baseWhere: T, isCompanyFeatureEnabled: boolean, user: ILoggedUserInfo | null | undefined): T & {
10
+ companyId?: string;
11
+ };
12
+ export interface ICompanyEnabled {
13
+ companyId?: string | null;
14
+ }
15
+ export declare function hasCompanyId<T>(entity: T): entity is T & ICompanyEnabled;
16
+ export declare function validateCompanyOwnership<T>(entity: T, user: ILoggedUserInfo | null | undefined, isCompanyFeatureEnabled: boolean, entityName: string): void;
@@ -0,0 +1,4 @@
1
+ import { CookieOptions, Request } from 'express';
2
+ export declare function isBrowserRequest(req: Request): boolean;
3
+ export declare function buildCookieOptions(req: Request): Pick<CookieOptions, 'secure' | 'sameSite' | 'domain'>;
4
+ export declare function parseDurationToMs(duration: string, defaultMs?: number): number;
@@ -0,0 +1,2 @@
1
+ export declare function generateSlug(text: string, maxLength?: number): string;
2
+ export declare function generateUniqueSlug(text: string, findMatchingSlugs: (baseSlug: string) => Promise<string[]>, maxLength?: number): Promise<string>;
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- Object.defineProperty(exports, "SetCreatedByOnBody", {
6
- enumerable: true,
7
- get: function() {
8
- return SetCreatedByOnBody;
9
- }
10
- });
11
- const _common = require("@nestjs/common");
12
- function _ts_decorate(decorators, target, key, desc) {
13
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
14
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
15
- 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;
16
- return c > 3 && r && Object.defineProperty(target, key, r), r;
17
- }
18
- let SetCreatedByOnBody = class SetCreatedByOnBody {
19
- intercept(context, next) {
20
- const request = context.switchToHttp().getRequest();
21
- const user = request?.user;
22
- if (user) {
23
- if (Array.isArray(request.body)) {
24
- request.body = request.body.map((item)=>({
25
- ...item,
26
- createdById: user.id
27
- }));
28
- } else if (typeof request.body === 'object' && request.body !== null) {
29
- request.body = {
30
- ...request.body,
31
- createdById: user.id
32
- };
33
- }
34
- }
35
- return next.handle();
36
- }
37
- };
38
- SetCreatedByOnBody = _ts_decorate([
39
- (0, _common.Injectable)()
40
- ], SetCreatedByOnBody);
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- Object.defineProperty(exports, "SetDeletedByOnBody", {
6
- enumerable: true,
7
- get: function() {
8
- return SetDeletedByOnBody;
9
- }
10
- });
11
- const _common = require("@nestjs/common");
12
- function _ts_decorate(decorators, target, key, desc) {
13
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
14
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
15
- 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;
16
- return c > 3 && r && Object.defineProperty(target, key, r), r;
17
- }
18
- let SetDeletedByOnBody = class SetDeletedByOnBody {
19
- intercept(context, next) {
20
- const request = context.switchToHttp().getRequest();
21
- const user = request?.user;
22
- if (user) {
23
- if (Array.isArray(request.body)) {
24
- request.body = request.body.map((item)=>({
25
- ...item,
26
- deletedById: user.id
27
- }));
28
- } else if (typeof request.body === 'object' && request.body !== null) {
29
- request.body = {
30
- ...request.body,
31
- deletedById: user.id
32
- };
33
- }
34
- }
35
- return next.handle();
36
- }
37
- };
38
- SetDeletedByOnBody = _ts_decorate([
39
- (0, _common.Injectable)()
40
- ], SetDeletedByOnBody);