@koalarx/nest 4.0.6 → 4.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.
- package/cli/constants/cli-project-checklist.js +5 -0
- package/cli/constants/core-packages.js +4 -6
- package/cli/utils/install-module.js +21 -21
- package/cli/utils/parse-new-args.js +0 -1
- package/cli/utils/patch-app-test-module.js +2 -1
- package/cli/utils/patch-env.js +36 -0
- package/cli/utils/patch-main.js +3 -4
- package/cli/utils/remove-sample-parts.js +1 -2
- package/koala-nest/.env.example +12 -0
- package/koala-nest/src/application/common/pagination.request.ts +0 -5
- package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
- package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
- package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
- package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
- package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
- package/koala-nest/src/core/common/list-response.base.ts +10 -0
- package/koala-nest/src/core/database/db-context.ts +3 -0
- package/koala-nest/src/core/database/entity.ts +9 -0
- package/koala-nest/src/core/env.ts +6 -0
- package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
- package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
- package/koala-nest/src/core/tools/mapping/auto-mapper.ts +25 -11
- package/koala-nest/src/core/tools/mapping/mapping-store.ts +13 -15
- package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
- package/koala-nest/src/core/utils/hash-password.ts +7 -2
- package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
- package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
- package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
- package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
- package/koala-nest/src/domain/entities/person/person-address.ts +2 -1
- package/koala-nest/src/domain/entities/person/person-contact.ts +2 -2
- package/koala-nest/src/domain/entities/person/person.ts +2 -4
- package/koala-nest/src/domain/entities/user/user.ts +1 -1
- package/koala-nest/src/host/bootstrap/apply-http-middleware.ts +22 -0
- package/koala-nest/src/host/main.ts +14 -14
- package/koala-nest/src/infra/common/redis-cache.service.ts +15 -4
- package/koala-nest/src/infra/database/data-source-factory.ts +4 -6
- package/koala-nest/src/infra/database/migrations/migration-datasource.ts +7 -0
- package/koala-nest/src/infra/repositories/person.repository.ts +3 -3
- package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
- package/koala-nest/src/test/app-auth-test.module.ts +2 -1
- package/koala-nest/src/test/app-test.module.ts +2 -1
- package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
- package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
- package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
- package/koala-nest/src/test/core/env.spec.ts +19 -0
- package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
- package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
- package/koala-nest/src/test/core/mapping.spec.ts +15 -0
- package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
- package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
- package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
- package/koala-nest/src/test/e2e-context.ts +4 -1
- package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +16 -20
- package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
- package/koala-nest/src/test/setup-e2e.ts +6 -43
- package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
- package/koala-nest/src/test/utils/create-e2e-database.ts +24 -16
- package/koala-nest/src/test/utils/e2e-database-client.ts +2 -2
- package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
- package/package.json +1 -1
- package/koala-nest/src/core/utils/icomparable.ts +0 -1
|
@@ -3,12 +3,36 @@ import { MappingStore } from './mapping-store';
|
|
|
3
3
|
|
|
4
4
|
interface AutoMapConfig<T> {
|
|
5
5
|
type?: () => Type<T>;
|
|
6
|
+
isArray?: boolean;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function AutoMap<T>(config?: AutoMapConfig<T>) {
|
|
9
|
-
return function (target:
|
|
10
|
-
const
|
|
10
|
+
return function (target: object, propertyKey: string) {
|
|
11
|
+
const designType = Reflect.getMetadata('design:type', target, propertyKey);
|
|
12
|
+
const isArray = config?.isArray ?? designType === Array;
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
if (config?.type) {
|
|
15
|
+
if (isArray) {
|
|
16
|
+
Reflect.defineMetadata(
|
|
17
|
+
'composition:type',
|
|
18
|
+
config.type,
|
|
19
|
+
target,
|
|
20
|
+
propertyKey,
|
|
21
|
+
);
|
|
22
|
+
} else if (!designType) {
|
|
23
|
+
Reflect.defineMetadata(
|
|
24
|
+
'design:type',
|
|
25
|
+
config.type(),
|
|
26
|
+
target,
|
|
27
|
+
propertyKey,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
MappingStore.setProp(
|
|
33
|
+
(target as { constructor: Type<unknown> }).constructor,
|
|
34
|
+
propertyKey,
|
|
35
|
+
config?.type,
|
|
36
|
+
);
|
|
13
37
|
};
|
|
14
38
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initializeUndefinedArrayProps } from '@/core/utils/initialize-undefined-array-props';
|
|
1
2
|
import { Type } from '@nestjs/common';
|
|
2
3
|
import { MappingStore } from './mapping-store';
|
|
3
4
|
|
|
@@ -25,7 +26,6 @@ export class AutoMapper {
|
|
|
25
26
|
|
|
26
27
|
static map<S, R>(data: any, source: Type<S>, target: Type<R>): R {
|
|
27
28
|
const mapping = MappingStore.getMapping(source, target);
|
|
28
|
-
|
|
29
29
|
const targetInstance = new target();
|
|
30
30
|
|
|
31
31
|
if (!mapping) {
|
|
@@ -70,9 +70,14 @@ export class AutoMapper {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const targetClass: Type<any> = this.toClass(targetPropType);
|
|
73
|
+
const sourceValue = data[sourceProp.name];
|
|
74
|
+
|
|
75
|
+
if (sourceValue === undefined) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
73
78
|
|
|
74
79
|
targetInstance[targetPropName] = this.map(
|
|
75
|
-
|
|
80
|
+
sourceValue,
|
|
76
81
|
sourcePropTypeClass,
|
|
77
82
|
targetClass,
|
|
78
83
|
);
|
|
@@ -80,17 +85,21 @@ export class AutoMapper {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
if (sourceProp.isArray) {
|
|
83
|
-
|
|
84
|
-
(item: unknown) => {
|
|
85
|
-
const targetItemType = targetProps.find(
|
|
86
|
-
(p) => p.name === targetPropName,
|
|
87
|
-
)!.type;
|
|
88
|
+
const sourceArray = data[sourceProp.name];
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
if (sourceArray === undefined) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
targetInstance[targetPropName] = sourceArray.map((item: unknown) => {
|
|
95
|
+
const targetItemType = targetProps.find(
|
|
96
|
+
(p) => p.name === targetPropName,
|
|
97
|
+
)!.type;
|
|
98
|
+
|
|
99
|
+
const targetClassType = this.toClass(targetItemType);
|
|
100
|
+
|
|
101
|
+
return this.map(item, sourcePropTypeClass, targetClassType);
|
|
102
|
+
});
|
|
94
103
|
continue;
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -101,6 +110,11 @@ export class AutoMapper {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
initializeUndefinedArrayProps(
|
|
114
|
+
targetInstance as Record<string, unknown>,
|
|
115
|
+
target,
|
|
116
|
+
);
|
|
117
|
+
|
|
104
118
|
return targetInstance;
|
|
105
119
|
}
|
|
106
120
|
}
|
|
@@ -30,8 +30,15 @@ export class MappingStore {
|
|
|
30
30
|
entity.prototype,
|
|
31
31
|
propName,
|
|
32
32
|
);
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const explicitComposition = Reflect.getMetadata(
|
|
34
|
+
'composition:type',
|
|
35
|
+
entity.prototype,
|
|
36
|
+
propName,
|
|
37
|
+
);
|
|
38
|
+
const isArray = propType === Array || explicitComposition != null;
|
|
39
|
+
const type = isArray
|
|
40
|
+
? (compositionType ?? explicitComposition ?? propType)
|
|
41
|
+
: propType;
|
|
35
42
|
|
|
36
43
|
const props = this._entities.get(entity) || [];
|
|
37
44
|
|
|
@@ -64,6 +71,10 @@ export class MappingStore {
|
|
|
64
71
|
return props;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
static getAllProps(entity: Type<any>) {
|
|
75
|
+
return this.getProps(entity);
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
static getPropType(entity: Type<any>, propName: string) {
|
|
68
79
|
let current: Type<any> | null = entity;
|
|
69
80
|
|
|
@@ -113,17 +124,4 @@ export class MappingStore {
|
|
|
113
124
|
static getMapping(source: Type<any>, target: Type<any>) {
|
|
114
125
|
return this._mappings.get(source)?.get(target) ?? null;
|
|
115
126
|
}
|
|
116
|
-
|
|
117
|
-
/** Compatibilidade com chave legada usada em testes. */
|
|
118
|
-
static getMappingByName(mapName: string) {
|
|
119
|
-
for (const targetMappings of this._mappings.values()) {
|
|
120
|
-
for (const mapping of targetMappings.values()) {
|
|
121
|
-
if (`${mapping.source.name}To${mapping.target.name}` === mapName) {
|
|
122
|
-
return mapping;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
127
|
}
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { CronExpressionParser } from 'cron-parser';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Retorna uma chave única do tick cron atual, ou null se o instante não coincide.
|
|
5
|
+
*/
|
|
6
|
+
export function getCronExecutionKey(
|
|
7
|
+
cronExpression: string,
|
|
8
|
+
now: Date = new Date(),
|
|
9
|
+
): string | null {
|
|
10
|
+
try {
|
|
11
|
+
const interval = CronExpressionParser.parse(cronExpression, {
|
|
12
|
+
currentDate: now,
|
|
13
|
+
});
|
|
14
|
+
const prev = interval.prev();
|
|
15
|
+
|
|
16
|
+
const matches =
|
|
17
|
+
prev.getFullYear() === now.getFullYear() &&
|
|
18
|
+
prev.getMonth() === now.getMonth() &&
|
|
19
|
+
prev.getDate() === now.getDate() &&
|
|
20
|
+
prev.getHours() === now.getHours() &&
|
|
21
|
+
prev.getMinutes() === now.getMinutes() &&
|
|
22
|
+
prev.getSeconds() === now.getSeconds();
|
|
23
|
+
|
|
24
|
+
if (!matches) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return prev.toISOString();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
/**
|
|
4
35
|
* Verifica se a expressão cron corresponde ao momento atual.
|
|
5
36
|
*
|
|
@@ -45,21 +76,5 @@ export function cronExpressionToBoolean(
|
|
|
45
76
|
cronExpression: string,
|
|
46
77
|
now: Date = new Date(),
|
|
47
78
|
): boolean {
|
|
48
|
-
|
|
49
|
-
const interval = CronExpressionParser.parse(cronExpression, {
|
|
50
|
-
currentDate: now,
|
|
51
|
-
});
|
|
52
|
-
const prev = interval.prev();
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
prev.getFullYear() === now.getFullYear() &&
|
|
56
|
-
prev.getMonth() === now.getMonth() &&
|
|
57
|
-
prev.getDate() === now.getDate() &&
|
|
58
|
-
prev.getHours() === now.getHours() &&
|
|
59
|
-
prev.getMinutes() === now.getMinutes() &&
|
|
60
|
-
prev.getSeconds() === now.getSeconds()
|
|
61
|
-
);
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
79
|
+
return getCronExecutionKey(cronExpression, now) !== null;
|
|
65
80
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { hash } from 'bcrypt';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const DEFAULT_BCRYPT_ROUNDS = 10;
|
|
4
|
+
|
|
5
|
+
export function hashPassword(
|
|
6
|
+
password: string,
|
|
7
|
+
rounds = Number(process.env.BCRYPT_ROUNDS) || DEFAULT_BCRYPT_ROUNDS,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
return hash(password, rounds);
|
|
5
10
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Type } from '@nestjs/common';
|
|
2
|
+
import { MappingStore } from '@/core/tools/mapping/mapping-store';
|
|
3
|
+
|
|
4
|
+
export function initializeUndefinedArrayProps(
|
|
5
|
+
target: Record<string, unknown>,
|
|
6
|
+
entity: Type<unknown>,
|
|
7
|
+
onlyProps?: string[],
|
|
8
|
+
): void {
|
|
9
|
+
MappingStore.getAllProps(entity).forEach((prop) => {
|
|
10
|
+
if (onlyProps && !onlyProps.includes(prop.name)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (target[prop.name] !== undefined || !prop.isArray) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
target[prop.name] = [];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS aberto por padrão (`true` = reflete a origin da requisição).
|
|
3
|
+
* Defina `CORS_ORIGINS` apenas quando quiser restringir a origens específicas.
|
|
4
|
+
*/
|
|
5
|
+
export function resolveCorsOrigin(
|
|
6
|
+
origins: string | undefined,
|
|
7
|
+
): boolean | string | string[] {
|
|
8
|
+
const normalized = origins?.trim();
|
|
9
|
+
|
|
10
|
+
if (!normalized) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const list = normalized
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((origin) => origin.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
|
|
19
|
+
if (list.length === 1) {
|
|
20
|
+
return list[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return list;
|
|
24
|
+
}
|
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
import { QUERY_FILTER_PARAMS } from '@/core/constants/query-params';
|
|
2
2
|
import { AutoMap } from '@/core/tools/mapping';
|
|
3
3
|
import type { QueryDirectionType } from '@/core/types';
|
|
4
|
+
import type {
|
|
5
|
+
FindOptionsOrder,
|
|
6
|
+
ObjectLiteral,
|
|
7
|
+
SelectQueryBuilder,
|
|
8
|
+
} from 'typeorm';
|
|
9
|
+
|
|
10
|
+
type OrderDirection = 'ASC' | 'DESC';
|
|
11
|
+
|
|
12
|
+
function normalizeDirection(
|
|
13
|
+
direction?: QueryDirectionType | string,
|
|
14
|
+
): OrderDirection {
|
|
15
|
+
return String(direction ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function flattenOrderBy(
|
|
19
|
+
order: Record<string, unknown>,
|
|
20
|
+
prefix = '',
|
|
21
|
+
): Array<{ path: string; direction: OrderDirection }> {
|
|
22
|
+
const entries: Array<{ path: string; direction: OrderDirection }> = [];
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of Object.entries(order)) {
|
|
25
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
26
|
+
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
entries.push({ path, direction: normalizeDirection(value) });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (value && typeof value === 'object') {
|
|
33
|
+
entries.push(...flattenOrderBy(value as Record<string, unknown>, path));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return entries;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeFindOptionsOrder(
|
|
41
|
+
order: Record<string, unknown>,
|
|
42
|
+
): FindOptionsOrder<ObjectLiteral> {
|
|
43
|
+
const normalized: Record<string, unknown> = {};
|
|
44
|
+
|
|
45
|
+
for (const [key, value] of Object.entries(order)) {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
normalized[key] = normalizeDirection(value);
|
|
48
|
+
} else if (value && typeof value === 'object') {
|
|
49
|
+
normalized[key] = normalizeFindOptionsOrder(
|
|
50
|
+
value as Record<string, unknown>,
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
normalized[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return normalized as FindOptionsOrder<ObjectLiteral>;
|
|
58
|
+
}
|
|
4
59
|
|
|
5
60
|
export class PaginationDto {
|
|
6
61
|
@AutoMap()
|
|
@@ -19,17 +74,47 @@ export class PaginationDto {
|
|
|
19
74
|
return (this.limit ?? 0) * (this.page ?? QUERY_FILTER_PARAMS.page);
|
|
20
75
|
}
|
|
21
76
|
|
|
22
|
-
generateOrderBy() {
|
|
77
|
+
generateOrderBy(): Record<string, unknown> | undefined {
|
|
23
78
|
if (this.orderBy) {
|
|
24
79
|
const orderByField = this.orderBy.split('.');
|
|
25
80
|
return orderByField.reduceRight(
|
|
26
81
|
(acc, item, index) => ({
|
|
27
82
|
[item]: index === orderByField.length - 1 ? this.direction : acc,
|
|
28
83
|
}),
|
|
29
|
-
{},
|
|
84
|
+
{} as Record<string, unknown>,
|
|
30
85
|
);
|
|
31
86
|
}
|
|
32
87
|
|
|
33
88
|
return undefined;
|
|
34
89
|
}
|
|
90
|
+
|
|
91
|
+
toFindOptionsOrder(): FindOptionsOrder<ObjectLiteral> | undefined {
|
|
92
|
+
const order = this.generateOrderBy();
|
|
93
|
+
|
|
94
|
+
if (!order) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return normalizeFindOptionsOrder(order);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
applyQueryBuilderPagination<T extends ObjectLiteral>(
|
|
102
|
+
qb: SelectQueryBuilder<T>,
|
|
103
|
+
alias: string,
|
|
104
|
+
): SelectQueryBuilder<T> {
|
|
105
|
+
const order = this.generateOrderBy();
|
|
106
|
+
|
|
107
|
+
if (order) {
|
|
108
|
+
for (const { path, direction } of flattenOrderBy(order)) {
|
|
109
|
+
const sortPath = path.includes('.') ? path : `${alias}.${path}`;
|
|
110
|
+
qb.addOrderBy(sortPath, direction);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return qb.skip(this.skip()).take(this.limit);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static from<T extends PaginationDto>(this: new () => T, props: Partial<T>): T {
|
|
118
|
+
return Object.assign(new this(), props);
|
|
119
|
+
}
|
|
35
120
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EntityBase } from '@/core/base/entity.base';
|
|
2
|
+
import { Entity } from '@/core/database/entity';
|
|
2
3
|
import { AutoMap } from '@/core/tools/mapping';
|
|
3
|
-
import { Column,
|
|
4
|
+
import { Column, PrimaryGeneratedColumn } from 'typeorm';
|
|
4
5
|
|
|
5
6
|
@Entity('person_address')
|
|
6
7
|
export class PersonAddress extends EntityBase<PersonAddress> {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { EntityBase } from '@/core/base/entity.base';
|
|
2
|
+
import { Entity } from '@/core/database/entity';
|
|
1
3
|
import { AutoMap } from '@/core/tools/mapping';
|
|
2
4
|
import {
|
|
3
5
|
Column,
|
|
4
|
-
Entity,
|
|
5
6
|
ManyToOne,
|
|
6
7
|
PrimaryGeneratedColumn,
|
|
7
8
|
type Relation,
|
|
8
9
|
} from 'typeorm';
|
|
9
10
|
import type { Person } from './person';
|
|
10
|
-
import { EntityBase } from '@/core/base/entity.base';
|
|
11
11
|
|
|
12
12
|
@Entity('person_contact')
|
|
13
13
|
export class PersonContact extends EntityBase<PersonContact> {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { EntityBase } from '@/core/base/entity.base';
|
|
2
|
+
import { Entity } from '@/core/database/entity';
|
|
1
3
|
import { AutoMap } from '@/core/tools/mapping';
|
|
2
4
|
import {
|
|
3
5
|
Column,
|
|
4
|
-
Entity,
|
|
5
6
|
JoinColumn,
|
|
6
7
|
OneToMany,
|
|
7
8
|
OneToOne,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
} from 'typeorm';
|
|
10
11
|
import { PersonAddress } from './person-address';
|
|
11
12
|
import { PersonContact } from './person-contact';
|
|
12
|
-
import { EntityBase } from '@/core/base/entity.base';
|
|
13
13
|
|
|
14
14
|
@Entity('person')
|
|
15
15
|
export class Person extends EntityBase<Person> {
|
|
@@ -27,7 +27,6 @@ export class Person extends EntityBase<Person> {
|
|
|
27
27
|
|
|
28
28
|
@OneToOne(() => PersonAddress, {
|
|
29
29
|
cascade: true,
|
|
30
|
-
eager: true,
|
|
31
30
|
onDelete: 'CASCADE',
|
|
32
31
|
})
|
|
33
32
|
@JoinColumn()
|
|
@@ -36,7 +35,6 @@ export class Person extends EntityBase<Person> {
|
|
|
36
35
|
|
|
37
36
|
@OneToMany(() => PersonContact, (contact) => contact.person, {
|
|
38
37
|
cascade: true,
|
|
39
|
-
eager: true,
|
|
40
38
|
onDelete: 'CASCADE',
|
|
41
39
|
})
|
|
42
40
|
@AutoMap({ type: () => PersonContact })
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AuthProfile } from '@/core/auth/auth-profile.enum';
|
|
2
2
|
import { EntityBase } from '@/core/base/entity.base';
|
|
3
|
+
import { Entity } from '@/core/database/entity';
|
|
3
4
|
import { AutoMap } from '@/core/tools/mapping';
|
|
4
5
|
import { UserStatus } from '@/domain/entities/user/enums/user-status.enum';
|
|
5
6
|
import {
|
|
6
7
|
Column,
|
|
7
8
|
CreateDateColumn,
|
|
8
|
-
Entity,
|
|
9
9
|
PrimaryGeneratedColumn,
|
|
10
10
|
UpdateDateColumn,
|
|
11
11
|
} from 'typeorm';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRateLimitMiddleware } from '@/core/http/rate-limit.middleware';
|
|
2
|
+
import { resolveCorsOrigin } from '@/core/utils/resolve-cors-origins';
|
|
3
|
+
import { EnvService } from '@/infra/common/env.service';
|
|
4
|
+
import type { INestApplication } from '@nestjs/common';
|
|
5
|
+
import cookieParser from 'cookie-parser';
|
|
6
|
+
|
|
7
|
+
export function applyHttpMiddleware(app: INestApplication) {
|
|
8
|
+
const env = app.get(EnvService);
|
|
9
|
+
|
|
10
|
+
app.use(cookieParser());
|
|
11
|
+
app.use(
|
|
12
|
+
createRateLimitMiddleware({
|
|
13
|
+
windowMs: env.get('RATE_LIMIT_WINDOW_MS'),
|
|
14
|
+
maxRequests: env.get('RATE_LIMIT_MAX'),
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
app.enableCors({
|
|
18
|
+
credentials: true,
|
|
19
|
+
origin: resolveCorsOrigin(env.get('CORS_ORIGINS')),
|
|
20
|
+
optionsSuccessStatus: 200,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
|
|
3
|
+
import { applyHttpMiddleware } from '@/host/bootstrap/apply-http-middleware';
|
|
4
|
+
import { resolveApiHost } from '@/core/utils/resolve-api-host';
|
|
3
5
|
import { NestFactory } from '@nestjs/core';
|
|
4
|
-
import cookieParser from 'cookie-parser';
|
|
5
6
|
import { AppModule } from './app.module';
|
|
6
7
|
import { defineDocumentation } from './open-api/define-documentation';
|
|
7
8
|
import { ErrorsFilter } from './filters/errors.filter';
|
|
@@ -13,13 +14,7 @@ import { ILoggingService } from '@/domain/common/ilogging.service';
|
|
|
13
14
|
async function bootstrap() {
|
|
14
15
|
const app = await NestFactory.create(AppModule);
|
|
15
16
|
|
|
16
|
-
app
|
|
17
|
-
|
|
18
|
-
app.enableCors({
|
|
19
|
-
credentials: true,
|
|
20
|
-
origin: true,
|
|
21
|
-
optionsSuccessStatus: 200,
|
|
22
|
-
});
|
|
17
|
+
applyHttpMiddleware(app);
|
|
23
18
|
|
|
24
19
|
await defineDocumentation(app);
|
|
25
20
|
|
|
@@ -32,12 +27,17 @@ async function bootstrap() {
|
|
|
32
27
|
await app.resolve(ProfilesGuard),
|
|
33
28
|
);
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
const port = Number(process.env.PORT) || 3000;
|
|
31
|
+
const bindHost = process.env.HOST ?? '0.0.0.0';
|
|
32
|
+
const publicHost = resolveApiHost(process.env.API_HOST, port);
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
34
|
+
await app.listen(port, bindHost);
|
|
35
|
+
|
|
36
|
+
console.log(`Server is running on ${publicHost}`);
|
|
37
|
+
console.log(`Documentation is available at ${publicHost}/doc`);
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
bootstrap()
|
|
40
|
+
bootstrap().catch((error) => {
|
|
41
|
+
console.error(error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
@@ -59,11 +59,22 @@ export class RedisCacheService implements ICacheService {
|
|
|
59
59
|
|
|
60
60
|
async invalidateByPrefix(prefix: string): Promise<void> {
|
|
61
61
|
const pattern = `${this.buildKey(prefix)}*`;
|
|
62
|
-
|
|
62
|
+
let cursor = '0';
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
await this.client.
|
|
66
|
-
|
|
64
|
+
do {
|
|
65
|
+
const [nextCursor, keys] = await this.client.scan(
|
|
66
|
+
cursor,
|
|
67
|
+
'MATCH',
|
|
68
|
+
pattern,
|
|
69
|
+
'COUNT',
|
|
70
|
+
100,
|
|
71
|
+
);
|
|
72
|
+
cursor = nextCursor;
|
|
73
|
+
|
|
74
|
+
if (keys.length > 0) {
|
|
75
|
+
await this.client.del(...keys);
|
|
76
|
+
}
|
|
77
|
+
} while (cursor !== '0');
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
onModuleDestroy() {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { PersonAddress } from '@/domain/entities/person/person-address';
|
|
3
|
-
import { PersonContact } from '@/domain/entities/person/person-contact';
|
|
4
|
-
import { User } from '@/domain/entities/user/user';
|
|
5
|
-
import { DataSource } from 'typeorm';
|
|
1
|
+
import { DbContext } from '@/core/database/db-context';
|
|
6
2
|
import { EnvService } from '@/infra/common/env.service';
|
|
3
|
+
import { DataSource } from 'typeorm';
|
|
7
4
|
|
|
8
5
|
export const DATA_SOURCE_PROVIDER_TOKEN = 'DATA_SOURCE';
|
|
9
6
|
|
|
@@ -11,7 +8,8 @@ export async function dataSourceFactory(env: EnvService) {
|
|
|
11
8
|
const dataSource = new DataSource({
|
|
12
9
|
type: 'postgres',
|
|
13
10
|
url: env.get('DATABASE_URL'),
|
|
14
|
-
|
|
11
|
+
schema: env.get('DATABASE_SCHEMA'),
|
|
12
|
+
entities: Array.from(DbContext.entities.values()),
|
|
15
13
|
invalidWhereValuesBehavior: {
|
|
16
14
|
undefined: 'ignore',
|
|
17
15
|
},
|
|
@@ -3,10 +3,17 @@ import path from 'node:path';
|
|
|
3
3
|
import { DataSource } from 'typeorm';
|
|
4
4
|
|
|
5
5
|
const root = process.cwd();
|
|
6
|
+
const schema = process.env.DATABASE_SCHEMA;
|
|
6
7
|
|
|
7
8
|
export default new DataSource({
|
|
8
9
|
type: 'postgres',
|
|
9
10
|
url: process.env.DATABASE_URL,
|
|
11
|
+
...(schema
|
|
12
|
+
? {
|
|
13
|
+
schema,
|
|
14
|
+
extra: { options: `-c search_path=${schema},public` },
|
|
15
|
+
}
|
|
16
|
+
: {}),
|
|
10
17
|
entities: [path.join(root, 'src/domain/entities/**/*.{js,ts}')],
|
|
11
18
|
migrations: [path.join(root, 'src/infra/database/migrations/[0-9]*.{js,ts}')],
|
|
12
19
|
migrationsTableName: 'migrations',
|
|
@@ -30,18 +30,18 @@ export class PersonRepository
|
|
|
30
30
|
return this.repository
|
|
31
31
|
.findAndCount({
|
|
32
32
|
where,
|
|
33
|
-
order: query.
|
|
33
|
+
order: query.toFindOptionsOrder(),
|
|
34
34
|
skip: query.skip(),
|
|
35
35
|
take: query.limit,
|
|
36
36
|
})
|
|
37
37
|
.then(([items, count]) => ({
|
|
38
|
-
items,
|
|
38
|
+
items: this.normalizeEntities(items),
|
|
39
39
|
count,
|
|
40
40
|
}));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
findById(id: number): Promise<Person | null> {
|
|
44
|
-
return this.
|
|
44
|
+
return this.findOneNormalized({
|
|
45
45
|
where: { id },
|
|
46
46
|
relations: { address: true, contacts: true },
|
|
47
47
|
});
|