@flusys/nestjs-shared 1.0.0-rc → 2.0.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 (63) hide show
  1. package/README.md +493 -658
  2. package/cjs/classes/api-service.class.js +59 -92
  3. package/cjs/classes/winston-logger-adapter.class.js +23 -40
  4. package/cjs/constants/permissions.js +11 -1
  5. package/cjs/dtos/delete.dto.js +10 -0
  6. package/cjs/dtos/response-payload.dto.js +0 -75
  7. package/cjs/guards/permission.guard.js +19 -18
  8. package/cjs/interceptors/index.js +0 -3
  9. package/cjs/interceptors/set-user-field-on-body.interceptor.js +20 -3
  10. package/cjs/middlewares/logger.middleware.js +50 -89
  11. package/cjs/modules/datasource/datasource.module.js +11 -14
  12. package/cjs/modules/datasource/multi-tenant-datasource.service.js +0 -4
  13. package/cjs/modules/utils/utils.service.js +22 -103
  14. package/cjs/utils/error-handler.util.js +12 -67
  15. package/cjs/utils/html-sanitizer.util.js +1 -11
  16. package/cjs/utils/index.js +2 -0
  17. package/cjs/utils/request.util.js +70 -0
  18. package/cjs/utils/string.util.js +63 -0
  19. package/classes/api-service.class.d.ts +2 -0
  20. package/classes/winston-logger-adapter.class.d.ts +2 -0
  21. package/constants/permissions.d.ts +12 -0
  22. package/dtos/delete.dto.d.ts +1 -0
  23. package/dtos/response-payload.dto.d.ts +0 -13
  24. package/fesm/classes/api-service.class.js +59 -92
  25. package/fesm/classes/winston-logger-adapter.class.js +23 -40
  26. package/fesm/constants/permissions.js +8 -1
  27. package/fesm/dtos/delete.dto.js +12 -2
  28. package/fesm/dtos/response-payload.dto.js +0 -69
  29. package/fesm/guards/permission.guard.js +19 -18
  30. package/fesm/interceptors/index.js +0 -3
  31. package/fesm/interceptors/set-user-field-on-body.interceptor.js +3 -0
  32. package/fesm/middlewares/logger.middleware.js +50 -83
  33. package/fesm/modules/datasource/datasource.module.js +11 -14
  34. package/fesm/modules/datasource/multi-tenant-datasource.service.js +0 -4
  35. package/fesm/modules/utils/utils.service.js +19 -89
  36. package/fesm/utils/error-handler.util.js +12 -68
  37. package/fesm/utils/html-sanitizer.util.js +1 -14
  38. package/fesm/utils/index.js +2 -0
  39. package/fesm/utils/request.util.js +58 -0
  40. package/fesm/utils/string.util.js +71 -0
  41. package/guards/permission.guard.d.ts +2 -0
  42. package/interceptors/index.d.ts +0 -3
  43. package/interceptors/set-user-field-on-body.interceptor.d.ts +3 -0
  44. package/interfaces/logged-user-info.interface.d.ts +0 -2
  45. package/middlewares/logger.middleware.d.ts +2 -2
  46. package/modules/datasource/datasource.module.d.ts +1 -0
  47. package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
  48. package/modules/utils/utils.service.d.ts +2 -18
  49. package/package.json +2 -2
  50. package/utils/error-handler.util.d.ts +3 -18
  51. package/utils/html-sanitizer.util.d.ts +0 -1
  52. package/utils/index.d.ts +2 -0
  53. package/utils/request.util.d.ts +4 -0
  54. package/utils/string.util.d.ts +2 -0
  55. package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -12
  56. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -12
  57. package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -12
  58. package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -4
  59. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -4
  60. package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -4
  61. package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -1
  62. package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -1
  63. package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -1
@@ -0,0 +1,58 @@
1
+ import { CLIENT_TYPE_HEADER } from '../constants';
2
+ /** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
3
+ s: 1000,
4
+ m: 60 * 1000,
5
+ h: 60 * 60 * 1000,
6
+ d: 24 * 60 * 60 * 1000,
7
+ w: 7 * 24 * 60 * 60 * 1000
8
+ };
9
+ /** Get normalized client type from request header */ function getClientType(req) {
10
+ const clientType = req.headers[CLIENT_TYPE_HEADER];
11
+ return clientType ? clientType.toLowerCase() : null;
12
+ }
13
+ /**
14
+ * Detect if request is from a browser client.
15
+ * Uses x-client-type header first, then falls back to user-agent detection.
16
+ */ export function isBrowserRequest(req) {
17
+ const clientType = getClientType(req);
18
+ if (clientType) {
19
+ return clientType === 'browser' || clientType === 'web';
20
+ }
21
+ const accept = req.headers['accept'] || '';
22
+ if (accept.includes('text/html')) return true;
23
+ const userAgent = req.headers['user-agent'] || '';
24
+ const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
25
+ return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
26
+ }
27
+ /**
28
+ * Build secure cookie options based on request context.
29
+ * Handles HTTPS detection for TLS-terminating proxies.
30
+ */ export function buildCookieOptions(req) {
31
+ const hostname = req.hostname || '';
32
+ const origin = req.headers.origin || '';
33
+ const isProduction = process.env.NODE_ENV === 'production';
34
+ const forwardedProto = req.headers['x-forwarded-proto'];
35
+ const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
36
+ let domain;
37
+ const domainParts = hostname.split('.');
38
+ if (domainParts.length > 2 && !hostname.includes('localhost')) {
39
+ domain = '.' + domainParts.slice(-2).join('.');
40
+ }
41
+ return {
42
+ secure: isHttps,
43
+ sameSite: isHttps ? 'strict' : 'lax',
44
+ ...domain && {
45
+ domain
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Parse duration string to milliseconds.
51
+ * @example '7d' → 604800000, '24h' → 86400000, '30m' → 1800000
52
+ */ export function parseDurationToMs(duration, defaultMs = TIME_UNIT_MS.w) {
53
+ const match = duration.match(/^(\d+)(s|m|h|d|w)$/);
54
+ if (!match) return defaultMs;
55
+ const value = parseInt(match[1], 10);
56
+ const unit = match[2];
57
+ return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
58
+ }
@@ -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
  }
@@ -3,7 +3,4 @@ export * from './idempotency.interceptor';
3
3
  export * from './query-performance.interceptor';
4
4
  export * from './response-meta.interceptor';
5
5
  export * from './set-user-field-on-body.interceptor';
6
- export * from './set-create-by-on-body.interceptor';
7
- export * from './set-delete-by-on-body.interceptor';
8
- export * from './set-update-by-on-body.interceptor';
9
6
  export * from './slug.interceptor';
@@ -1,2 +1,5 @@
1
1
  import { NestInterceptor, Type } from '@nestjs/common';
2
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>>;
@@ -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
  }
@@ -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,27 +1,11 @@
1
1
  import { HybridCache } from '../../classes/hybrid-cache.class';
2
- export declare const DEFAULT_PHONE_REGEX: RegExp;
3
- export declare const DEFAULT_PHONE_COUNTRY_CODE = "+88";
4
- export interface PhoneValidationConfig {
5
- regex: RegExp;
6
- countryCode: string;
7
- }
8
2
  export declare class UtilsService {
9
3
  private readonly logger;
10
- private phoneConfig;
11
- setPhoneValidationConfig(config: PhoneValidationConfig): void;
12
4
  getCacheKey(entityName: string, params: any, entityId?: string, tenantId?: string): string;
13
5
  trackCacheKey(cacheKey: string, entityName: string, cacheManager: HybridCache, entityId?: string, tenantId?: string): Promise<void>;
14
6
  clearCache(entityName: string, cacheManager: HybridCache, entityId?: string, tenantId?: string): Promise<void>;
15
- checkPhoneOrEmail(value: string): {
16
- value: string | null;
17
- type: 'phone' | 'email' | null;
18
- };
19
7
  transformToSlug(value: string, salt?: boolean): string;
20
8
  getRandomInt(min: number, max: number): number;
21
- generateRandomId(length: number): string;
22
- getRandomOtpCode(): number;
23
- extractColumnNameFromError(detail: string): {
24
- columnName: string;
25
- value: string;
26
- };
9
+ private buildTenantPrefix;
10
+ private buildTrackingKey;
27
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-shared",
3
- "version": "1.0.0-rc",
3
+ "version": "2.0.0",
4
4
  "description": "Common shared utilities for Flusys NestJS applications",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -105,6 +105,6 @@
105
105
  "winston-daily-rotate-file": "^5.0.0"
106
106
  },
107
107
  "dependencies": {
108
- "@flusys/nestjs-core": "1.0.0-rc"
108
+ "@flusys/nestjs-core": "2.0.0"
109
109
  }
110
110
  }
@@ -9,26 +9,11 @@ export interface IErrorContext {
9
9
  sectionId?: string;
10
10
  data?: Record<string, unknown>;
11
11
  }
12
- export interface ISanitizedError {
13
- message: string;
14
- code?: string;
15
- statusCode?: number;
16
- stack?: string;
17
- }
18
12
  export declare class ErrorHandler {
19
- static getErrorMessage(error: unknown, sanitizeForClient?: boolean): string;
20
- private static containsSensitiveData;
21
- static getErrorStack(error: unknown, forClient?: boolean): string | undefined;
22
- static createClientError(error: unknown, statusCode?: number, code?: string): ISanitizedError;
13
+ static getErrorMessage(error: unknown): string;
23
14
  private static sanitizeContextForLogging;
24
- static createErrorContext(error: unknown, context?: IErrorContext): {
25
- error: {
26
- message: string;
27
- stack?: string;
28
- name?: string;
29
- };
30
- context?: Record<string, unknown>;
31
- };
15
+ private static createErrorContext;
32
16
  static logError(logger: Logger, error: unknown, operation: string, context?: Omit<IErrorContext, 'operation'>): void;
33
17
  static rethrowError(error: unknown): never;
18
+ static logAndRethrow(logger: Logger, error: unknown, operation: string, context?: Omit<IErrorContext, 'operation'>): never;
34
19
  }
@@ -1,3 +1,2 @@
1
1
  export declare function escapeHtml(str: string): string;
2
2
  export declare function escapeHtmlVariables(variables: Record<string, unknown>): Record<string, string>;
3
- export declare function containsHtmlContent(str: string): boolean;
package/utils/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './error-handler.util';
2
2
  export * from './html-sanitizer.util';
3
3
  export * from './query-helpers.util';
4
+ export * from './request.util';
5
+ export * from './string.util';
@@ -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,12 +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 _setuserfieldonbodyinterceptor = require("./set-user-field-on-body.interceptor");
12
- const SetCreatedByOnBody = (0, _setuserfieldonbodyinterceptor.createSetUserFieldInterceptor)('createdById');
@@ -1,12 +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 _setuserfieldonbodyinterceptor = require("./set-user-field-on-body.interceptor");
12
- const SetDeletedByOnBody = (0, _setuserfieldonbodyinterceptor.createSetUserFieldInterceptor)('deletedById');
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- Object.defineProperty(exports, "SetUpdateByOnBody", {
6
- enumerable: true,
7
- get: function() {
8
- return SetUpdateByOnBody;
9
- }
10
- });
11
- const _setuserfieldonbodyinterceptor = require("./set-user-field-on-body.interceptor");
12
- const SetUpdateByOnBody = (0, _setuserfieldonbodyinterceptor.createSetUserFieldInterceptor)('updatedById');
@@ -1,4 +0,0 @@
1
- import { createSetUserFieldInterceptor } from './set-user-field-on-body.interceptor';
2
- /**
3
- * Interceptor that sets createdById field on request body from authenticated user
4
- */ export const SetCreatedByOnBody = createSetUserFieldInterceptor('createdById');
@@ -1,4 +0,0 @@
1
- import { createSetUserFieldInterceptor } from './set-user-field-on-body.interceptor';
2
- /**
3
- * Interceptor that sets deletedById field on request body from authenticated user
4
- */ export const SetDeletedByOnBody = createSetUserFieldInterceptor('deletedById');
@@ -1,4 +0,0 @@
1
- import { createSetUserFieldInterceptor } from './set-user-field-on-body.interceptor';
2
- /**
3
- * Interceptor that sets updatedById field on request body from authenticated user
4
- */ export const SetUpdateByOnBody = createSetUserFieldInterceptor('updatedById');
@@ -1 +0,0 @@
1
- export declare const SetCreatedByOnBody: import("@nestjs/common").Type<import("@nestjs/common").NestInterceptor<any, any>>;
@@ -1 +0,0 @@
1
- export declare const SetDeletedByOnBody: import("@nestjs/common").Type<import("@nestjs/common").NestInterceptor<any, any>>;
@@ -1 +0,0 @@
1
- export declare const SetUpdateByOnBody: import("@nestjs/common").Type<import("@nestjs/common").NestInterceptor<any, any>>;