@flusys/nestjs-shared 1.0.0-rc → 1.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.
- package/README.md +493 -658
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/permissions.js +11 -1
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/response-payload.dto.js +0 -75
- package/cjs/guards/permission.guard.js +19 -18
- package/cjs/interceptors/index.js +0 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +20 -3
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/cjs/modules/utils/utils.service.js +22 -103
- package/cjs/utils/error-handler.util.js +12 -67
- package/cjs/utils/html-sanitizer.util.js +1 -11
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-service.class.d.ts +2 -0
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/permissions.d.ts +12 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/response-payload.dto.d.ts +0 -13
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/permissions.js +8 -1
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/response-payload.dto.js +0 -69
- package/fesm/guards/permission.guard.js +19 -18
- package/fesm/interceptors/index.js +0 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +3 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/fesm/modules/utils/utils.service.js +19 -89
- package/fesm/utils/error-handler.util.js +12 -68
- package/fesm/utils/html-sanitizer.util.js +1 -14
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/request.util.js +58 -0
- package/fesm/utils/string.util.js +71 -0
- package/guards/permission.guard.d.ts +2 -0
- package/interceptors/index.d.ts +0 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +3 -0
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/datasource.module.d.ts +1 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +2 -18
- package/package.json +2 -2
- package/utils/error-handler.util.d.ts +3 -18
- package/utils/html-sanitizer.util.d.ts +0 -1
- package/utils/index.d.ts +2 -0
- package/utils/request.util.d.ts +4 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -12
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -4
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -1
- 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
|
}
|
package/interceptors/index.d.ts
CHANGED
|
@@ -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>>;
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
|
3
|
+
"version": "1.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
|
|
108
|
+
"@flusys/nestjs-core": "1.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
|
|
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
|
|
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
|
}
|
package/utils/index.d.ts
CHANGED
|
@@ -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;
|
|
@@ -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 +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>>;
|