@flusys/nestjs-shared 1.0.0-beta → 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 +501 -720
- package/cjs/classes/api-controller.class.js +9 -24
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/index.js +1 -0
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/index.js +14 -0
- package/cjs/constants/permissions.js +184 -0
- package/cjs/decorators/api-response.decorator.js +1 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/sanitize-html.decorator.js +36 -0
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +0 -116
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +51 -105
- package/cjs/interceptors/index.js +1 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
- package/cjs/interceptors/slug.interceptor.js +30 -9
- package/cjs/interfaces/datasource.interface.js +4 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/cjs/modules/utils/utils.service.js +40 -203
- package/cjs/utils/error-handler.util.js +35 -12
- package/cjs/utils/html-sanitizer.util.js +64 -0
- package/cjs/utils/index.js +4 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +7 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +179 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -20
- package/fesm/classes/api-controller.class.js +9 -24
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/index.js +2 -0
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/permissions.js +128 -0
- package/fesm/decorators/api-response.decorator.js +1 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/sanitize-html.decorator.js +45 -0
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +0 -107
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +51 -105
- package/fesm/interceptors/index.js +1 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
- package/fesm/interceptors/slug.interceptor.js +31 -10
- package/fesm/interfaces/datasource.interface.js +20 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/fesm/modules/utils/utils.service.js +41 -204
- package/fesm/utils/error-handler.util.js +36 -13
- package/fesm/utils/html-sanitizer.util.js +69 -0
- package/fesm/utils/index.js +4 -0
- package/fesm/utils/query-helpers.util.js +78 -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 +1 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
- package/interceptors/slug.interceptor.d.ts +2 -1
- package/interfaces/api.interface.d.ts +2 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +2 -1
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- 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 +4 -14
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +14 -19
- package/utils/html-sanitizer.util.d.ts +2 -0
- package/utils/index.d.ts +4 -0
- package/utils/query-helpers.util.d.ts +16 -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 -40
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
- package/fesm/interfaces/base-query.interface.js +0 -3
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
- 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
|
+
'<': '<',
|
|
11
|
+
'>': '>',
|
|
12
|
+
'"': '"',
|
|
13
|
+
"'": ''',
|
|
14
|
+
'/': '/',
|
|
15
|
+
'`': '`',
|
|
16
|
+
'=': '='
|
|
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: '<script>alert("xss")</script>'
|
|
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: '<script>xss</script>', 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
|
+
}
|
package/fesm/utils/index.js
CHANGED
|
@@ -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,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
|
@@ -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-
|
|
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
|
|
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 '
|
|
2
|
-
import { ILoggedUserInfo } from '
|
|
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>>;
|
|
@@ -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
|
}
|
package/interfaces/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './api.interface';
|
|
2
|
-
export * from './
|
|
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';
|
|
@@ -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
|
}
|
|
@@ -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 '
|
|
1
|
+
import { HybridCache } from '../../classes/hybrid-cache.class';
|
|
2
2
|
export declare class UtilsService {
|
|
3
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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.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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
|
-
"@keyv/redis": "^
|
|
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": "^
|
|
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.0.0
|
|
108
|
+
"@flusys/nestjs-core": "1.0.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
|
|
5
|
-
static createErrorContext
|
|
6
|
-
|
|
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
|
}
|
package/utils/index.d.ts
CHANGED
|
@@ -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;
|
|
@@ -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);
|