@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.
Files changed (62) hide show
  1. package/cli/constants/cli-project-checklist.js +5 -0
  2. package/cli/constants/core-packages.js +4 -6
  3. package/cli/utils/install-module.js +21 -21
  4. package/cli/utils/parse-new-args.js +0 -1
  5. package/cli/utils/patch-app-test-module.js +2 -1
  6. package/cli/utils/patch-env.js +36 -0
  7. package/cli/utils/patch-main.js +3 -4
  8. package/cli/utils/remove-sample-parts.js +1 -2
  9. package/koala-nest/.env.example +12 -0
  10. package/koala-nest/src/application/common/pagination.request.ts +0 -5
  11. package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
  12. package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
  13. package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
  14. package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
  15. package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
  16. package/koala-nest/src/core/common/list-response.base.ts +10 -0
  17. package/koala-nest/src/core/database/db-context.ts +3 -0
  18. package/koala-nest/src/core/database/entity.ts +9 -0
  19. package/koala-nest/src/core/env.ts +6 -0
  20. package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
  21. package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
  22. package/koala-nest/src/core/tools/mapping/auto-mapper.ts +25 -11
  23. package/koala-nest/src/core/tools/mapping/mapping-store.ts +13 -15
  24. package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
  25. package/koala-nest/src/core/utils/hash-password.ts +7 -2
  26. package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
  27. package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
  28. package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
  29. package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
  30. package/koala-nest/src/domain/entities/person/person-address.ts +2 -1
  31. package/koala-nest/src/domain/entities/person/person-contact.ts +2 -2
  32. package/koala-nest/src/domain/entities/person/person.ts +2 -4
  33. package/koala-nest/src/domain/entities/user/user.ts +1 -1
  34. package/koala-nest/src/host/bootstrap/apply-http-middleware.ts +22 -0
  35. package/koala-nest/src/host/main.ts +14 -14
  36. package/koala-nest/src/infra/common/redis-cache.service.ts +15 -4
  37. package/koala-nest/src/infra/database/data-source-factory.ts +4 -6
  38. package/koala-nest/src/infra/database/migrations/migration-datasource.ts +7 -0
  39. package/koala-nest/src/infra/repositories/person.repository.ts +3 -3
  40. package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
  41. package/koala-nest/src/test/app-auth-test.module.ts +2 -1
  42. package/koala-nest/src/test/app-test.module.ts +2 -1
  43. package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
  44. package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
  45. package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
  46. package/koala-nest/src/test/core/env.spec.ts +19 -0
  47. package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
  48. package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
  49. package/koala-nest/src/test/core/mapping.spec.ts +15 -0
  50. package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
  51. package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
  52. package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
  53. package/koala-nest/src/test/e2e-context.ts +4 -1
  54. package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +16 -20
  55. package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
  56. package/koala-nest/src/test/setup-e2e.ts +6 -43
  57. package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
  58. package/koala-nest/src/test/utils/create-e2e-database.ts +24 -16
  59. package/koala-nest/src/test/utils/e2e-database-client.ts +2 -2
  60. package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
  61. package/package.json +1 -1
  62. package/koala-nest/src/core/utils/icomparable.ts +0 -1
@@ -1,4 +1,27 @@
1
- import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
1
+ import { initializeUndefinedArrayProps } from '@/core/utils/initialize-undefined-array-props';
2
+ import type { Type } from '@nestjs/common';
3
+ import {
4
+ DataSource,
5
+ EntityTarget,
6
+ FindManyOptions,
7
+ FindOneOptions,
8
+ ObjectLiteral,
9
+ Repository,
10
+ } from 'typeorm';
11
+
12
+ function getLoadedRelationKeys(
13
+ relations?: FindOneOptions<ObjectLiteral>['relations'],
14
+ ): string[] {
15
+ if (!relations) {
16
+ return [];
17
+ }
18
+
19
+ if (Array.isArray(relations)) {
20
+ return relations.map(String);
21
+ }
22
+
23
+ return Object.keys(relations);
24
+ }
2
25
 
3
26
  export class RepositoryBase<T extends ObjectLiteral> {
4
27
  protected readonly repository: Repository<T>;
@@ -10,6 +33,52 @@ export class RepositoryBase<T extends ObjectLiteral> {
10
33
  this.repository = this.dataSource.getRepository<T>(entity);
11
34
  }
12
35
 
36
+ protected normalizeEntity<E extends ObjectLiteral>(
37
+ entity: E,
38
+ loadedRelationKeys: string[] = [],
39
+ ): E {
40
+ if (loadedRelationKeys.length > 0) {
41
+ initializeUndefinedArrayProps(
42
+ entity as Record<string, unknown>,
43
+ this.getEntityType(),
44
+ loadedRelationKeys,
45
+ );
46
+ }
47
+
48
+ return entity;
49
+ }
50
+
51
+ protected normalizeEntities<E extends ObjectLiteral>(
52
+ entities: E[],
53
+ loadedRelationKeys: string[] = [],
54
+ ): E[] {
55
+ return entities.map((entity) =>
56
+ this.normalizeEntity(entity, loadedRelationKeys),
57
+ );
58
+ }
59
+
60
+ protected getEntityType(): Type<unknown> {
61
+ return this.entity as Type<unknown>;
62
+ }
63
+
64
+ protected findOneNormalized(options: FindOneOptions<T>) {
65
+ const loadedRelationKeys = getLoadedRelationKeys(options.relations);
66
+
67
+ return this.repository
68
+ .findOne(options)
69
+ .then((entity) =>
70
+ entity ? this.normalizeEntity(entity, loadedRelationKeys) : null,
71
+ );
72
+ }
73
+
74
+ protected findNormalized(options: FindManyOptions<T>) {
75
+ const loadedRelationKeys = getLoadedRelationKeys(options.relations);
76
+
77
+ return this.repository
78
+ .find(options)
79
+ .then((entities) => this.normalizeEntities(entities, loadedRelationKeys));
80
+ }
81
+
13
82
  save(entity: T) {
14
83
  return this.repository.save(entity);
15
84
  }
@@ -3,7 +3,7 @@ import { AuthModule } from '@/host/controllers/auth/auth.module';
3
3
  import { Module } from '@nestjs/common';
4
4
  import { ConfigModule } from '@nestjs/config';
5
5
  import { PersonModule } from '@/host/controllers/person/person.module';
6
- import { e2eDatabaseUrl } from '@/test/e2e-context';
6
+ import { e2eDatabaseUrl, e2eSchemaName } from '@/test/e2e-context';
7
7
  import { getJwtTestKeys } from '@/test/utils/jwt-test-keys';
8
8
 
9
9
  const jwtKeys = getJwtTestKeys();
@@ -18,6 +18,7 @@ const jwtKeys = getJwtTestKeys();
18
18
  PORT: 3000,
19
19
  NODE_ENV: 'test',
20
20
  DATABASE_URL: e2eDatabaseUrl,
21
+ DATABASE_SCHEMA: e2eSchemaName,
21
22
  JWT_PRIVATE_KEY: jwtKeys.privateKey,
22
23
  JWT_PUBLIC_KEY: jwtKeys.publicKey,
23
24
  JWT_ACCESS_TOKEN_EXPIRES_IN: '15m',
@@ -2,7 +2,7 @@ import { envSchema } from '@/core/env';
2
2
  import { Module } from '@nestjs/common';
3
3
  import { ConfigModule } from '@nestjs/config';
4
4
  import { PersonModule } from '@/host/controllers/person/person.module';
5
- import { e2eDatabaseUrl } from '@/test/e2e-context';
5
+ import { e2eDatabaseUrl, e2eSchemaName } from '@/test/e2e-context';
6
6
 
7
7
  @Module({
8
8
  imports: [
@@ -14,6 +14,7 @@ import { e2eDatabaseUrl } from '@/test/e2e-context';
14
14
  PORT: 3000,
15
15
  NODE_ENV: 'test',
16
16
  DATABASE_URL: e2eDatabaseUrl,
17
+ DATABASE_SCHEMA: e2eSchemaName,
17
18
  }),
18
19
  }),
19
20
  PersonModule,
@@ -1,19 +1,32 @@
1
+ import { describe, expect, it } from 'bun:test';
1
2
  import { DeleteInactiveJob } from '@/application/person/jobs/cron/delete-inactive.job';
2
3
  import { DeletePersonHandler } from '@/application/person/delete/delete-person.handler';
3
- import { ReadManyPersonHandler } from '@/application/person/read-many/read-many-person.handler';
4
+ import { Person } from '@/domain/entities/person/person';
5
+ import type { IPersonRepository } from '@/domain/repositories/iperson.repository';
4
6
  import { FakeLoggingService } from '@/test/services/fake-logging.service';
5
7
  import { FakeRedLockService } from '@/test/services/fake-red-lock.service';
6
8
 
7
9
  describe('DeleteInactiveJob', () => {
8
- it('remove pessoas inativas retornadas pela listagem', async () => {
10
+ it('remove todas as páginas de pessoas inativas', async () => {
9
11
  const deletedIds: number[] = [];
12
+ const inactivePeople = Array.from({ length: 35 }, (_, index) => {
13
+ const person = new Person();
14
+ person.id = index + 1;
15
+ person.name = `Inactive ${index + 1}`;
16
+ person.active = false;
17
+ return person;
18
+ });
10
19
 
11
- const readManyPerson = {
12
- handle: async () => ({
13
- items: [{ id: 1, name: 'Inactive', active: false }],
14
- count: 1,
15
- }),
16
- } as unknown as ReadManyPersonHandler;
20
+ const repository = {
21
+ findMany: async (query: { page?: number; limit?: number }) => {
22
+ const currentPage = query.page ?? 0;
23
+ const limit = query.limit ?? 30;
24
+ const start = currentPage * limit;
25
+ const items = inactivePeople.slice(start, start + limit);
26
+
27
+ return { items, count: inactivePeople.length };
28
+ },
29
+ } as unknown as IPersonRepository;
17
30
 
18
31
  const deletePerson = {
19
32
  handle: async (id: number) => {
@@ -24,12 +37,12 @@ describe('DeleteInactiveJob', () => {
24
37
  const job = new DeleteInactiveJob(
25
38
  new FakeRedLockService(),
26
39
  new FakeLoggingService(),
27
- readManyPerson,
40
+ repository,
28
41
  deletePerson,
29
42
  );
30
43
 
31
44
  await job['run']();
32
45
 
33
- expect(deletedIds).toEqual([1]);
46
+ expect(deletedIds).toHaveLength(35);
34
47
  });
35
48
  });
@@ -58,6 +58,48 @@ describe('UpdatePersonHandler', () => {
58
58
  expect(cache.invalidateByPrefixCalls).toEqual([PERSON_LIST_CACHE_PREFIX]);
59
59
  });
60
60
 
61
+ it('remove contatos ausentes do payload (órfãos via TypeORM)', async () => {
62
+ const cache = new CacheStub();
63
+ const keptContact = PersonContact.from({
64
+ id: 10,
65
+ contact: 'keep@example.com',
66
+ person: undefined as unknown as Person,
67
+ });
68
+ const removedContact = PersonContact.from({
69
+ id: 11,
70
+ contact: 'remove@example.com',
71
+ person: undefined as unknown as Person,
72
+ });
73
+
74
+ const person = Person.from({
75
+ id: 1,
76
+ name: 'Jane',
77
+ active: true,
78
+ address: PersonAddress.from({ id: 5, address: 'Street' }),
79
+ contacts: [keptContact, removedContact],
80
+ });
81
+ keptContact.person = person;
82
+ removedContact.person = person;
83
+
84
+ const repository = {
85
+ findById: async () => person,
86
+ save: async (entity: Person) => entity,
87
+ } as unknown as IPersonRepository;
88
+
89
+ const handler = new UpdatePersonHandler(repository, cache);
90
+
91
+ await handler.handle({
92
+ id: 1,
93
+ name: 'Jane',
94
+ address: { id: 5, address: 'Street' },
95
+ contacts: [{ id: 10, contact: 'keep@example.com' }],
96
+ });
97
+
98
+ expect(person.contacts).toHaveLength(1);
99
+ expect(person.contacts[0].id).toBe(10);
100
+ expect(person.contacts[0].contact).toBe('keep@example.com');
101
+ });
102
+
61
103
  it('lança NotFoundException quando a pessoa não existe', async () => {
62
104
  const repository = {
63
105
  findById: async () => null,
@@ -1,8 +1,27 @@
1
1
  /// <reference types="bun-types/test-globals" />
2
2
 
3
- import { cronExpressionToBoolean } from '@/core/utils/cron-expression-to-boolean';
3
+ import {
4
+ cronExpressionToBoolean,
5
+ getCronExecutionKey,
6
+ } from '@/core/utils/cron-expression-to-boolean';
4
7
  import { describe, expect, it } from 'bun:test';
5
8
 
9
+ describe('getCronExecutionKey', () => {
10
+ it('retorna ISO do tick quando o instante coincide com a expressão', () => {
11
+ const at = new Date(2024, 5, 12, 12, 0, 0, 1);
12
+
13
+ expect(getCronExecutionKey('0 0 12 * * *', at)).toBe(
14
+ new Date(2024, 5, 12, 12, 0, 0).toISOString(),
15
+ );
16
+ });
17
+
18
+ it('retorna null quando o instante não coincide', () => {
19
+ const at = new Date(2024, 5, 12, 12, 0, 1);
20
+
21
+ expect(getCronExecutionKey('0 0 12 * * *', at)).toBeNull();
22
+ });
23
+ });
24
+
6
25
  describe('cronExpressionToBoolean', () => {
7
26
  it('retorna true quando o instante atual coincide com o tick da expressão', () => {
8
27
  const at = new Date(2024, 5, 12, 12, 0, 0, 1);
@@ -3,6 +3,15 @@ import { parseOAuth2ProviderEnv } from '@/core/auth/parse-oauth2-provider-env';
3
3
  import { envSchema, validateEnvConfig } from '@/core/env';
4
4
 
5
5
  describe('envSchema', () => {
6
+ it('define HOST com default 0.0.0.0 para bind em container', () => {
7
+ const env = envSchema.parse({
8
+ NODE_ENV: 'test',
9
+ DATABASE_URL: 'postgres://localhost/test',
10
+ });
11
+
12
+ expect(env.HOST).toBe('0.0.0.0');
13
+ });
14
+
6
15
  it('interpreta CRON_JOBS_ENABLED=false como desabilitado', () => {
7
16
  const env = envSchema.parse({
8
17
  NODE_ENV: 'test',
@@ -12,6 +21,16 @@ describe('envSchema', () => {
12
21
 
13
22
  expect(env.CRON_JOBS_ENABLED).toBe(false);
14
23
  });
24
+
25
+ it('define rate limit desabilitado por padrão', () => {
26
+ const env = envSchema.parse({
27
+ NODE_ENV: 'test',
28
+ DATABASE_URL: 'postgres://localhost/test',
29
+ });
30
+
31
+ expect(env.RATE_LIMIT_MAX).toBe(0);
32
+ expect(env.RATE_LIMIT_WINDOW_MS).toBe(60_000);
33
+ });
15
34
  });
16
35
 
17
36
  describe('parseOAuth2ProviderEnv', () => {
@@ -0,0 +1,62 @@
1
+ /// <reference types="bun-types/test-globals" />
2
+
3
+ import { createRateLimitMiddleware } from '@/core/http/rate-limit.middleware';
4
+ import { describe, expect, it } from 'bun:test';
5
+
6
+ function createMockResponse() {
7
+ const response = {
8
+ statusCode: 200,
9
+ body: undefined as unknown,
10
+ status(code: number) {
11
+ this.statusCode = code;
12
+ return this;
13
+ },
14
+ json(payload: unknown) {
15
+ this.body = payload;
16
+ return this;
17
+ },
18
+ };
19
+
20
+ return response;
21
+ }
22
+
23
+ describe('createRateLimitMiddleware', () => {
24
+ it('permite requisições quando rate limit está desabilitado', () => {
25
+ const middleware = createRateLimitMiddleware({
26
+ windowMs: 60_000,
27
+ maxRequests: 0,
28
+ });
29
+ let called = false;
30
+ const next = () => {
31
+ called = true;
32
+ };
33
+
34
+ middleware(
35
+ { ip: '127.0.0.1' } as never,
36
+ createMockResponse() as never,
37
+ next,
38
+ );
39
+
40
+ expect(called).toBe(true);
41
+ });
42
+
43
+ it('bloqueia requisições após atingir maxRequests', () => {
44
+ const middleware = createRateLimitMiddleware({
45
+ windowMs: 60_000,
46
+ maxRequests: 2,
47
+ });
48
+ const req = { ip: '127.0.0.1' } as never;
49
+ const next = () => {};
50
+
51
+ middleware(req, createMockResponse() as never, next);
52
+ middleware(req, createMockResponse() as never, next);
53
+ const blockedResponse = createMockResponse();
54
+
55
+ middleware(req, blockedResponse as never, next);
56
+
57
+ expect(blockedResponse.statusCode).toBe(429);
58
+ expect(blockedResponse.body).toMatchObject({
59
+ statusCode: 429,
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,63 @@
1
+ /// <reference types="bun-types/test-globals" />
2
+
3
+ import { initializeUndefinedArrayProps } from '@/core/utils/initialize-undefined-array-props';
4
+ import { AutoMap } from '@/core/tools/mapping';
5
+ import { MappingStore } from '@/core/tools/mapping/mapping-store';
6
+ import { describe, expect, it } from 'bun:test';
7
+
8
+ class ChildItem {
9
+ @AutoMap()
10
+ name!: string;
11
+ }
12
+
13
+ class EntityWithArrays {
14
+ @AutoMap()
15
+ title!: string;
16
+
17
+ @AutoMap({ type: () => ChildItem })
18
+ children!: ChildItem[];
19
+ }
20
+
21
+ describe('initializeUndefinedArrayProps', () => {
22
+ it('inicializa props de array mapeadas como [] quando undefined', () => {
23
+ const target = { title: 'Test' } as Record<string, unknown>;
24
+
25
+ initializeUndefinedArrayProps(target, EntityWithArrays);
26
+
27
+ expect(target.children).toEqual([]);
28
+ });
29
+
30
+ it('respeita onlyProps quando informado', () => {
31
+ const target = { title: 'Test' } as Record<string, unknown>;
32
+
33
+ initializeUndefinedArrayProps(target, EntityWithArrays, ['title']);
34
+
35
+ expect(target.children).toBeUndefined();
36
+ });
37
+
38
+ it('não sobrescreve array já definido', () => {
39
+ const existing: ChildItem[] = [];
40
+ const target = { title: 'Test', children: existing } as Record<
41
+ string,
42
+ unknown
43
+ >;
44
+
45
+ initializeUndefinedArrayProps(target, EntityWithArrays);
46
+
47
+ expect(target.children).toBe(existing);
48
+ });
49
+
50
+ it('detecta arrays via metadata composition:type', () => {
51
+ class DtoWithExplicitArray {
52
+ @AutoMap({ type: () => ChildItem, isArray: true })
53
+ items!: ChildItem[];
54
+ }
55
+
56
+ MappingStore.getAllProps(DtoWithExplicitArray);
57
+
58
+ const target = {} as Record<string, unknown>;
59
+ initializeUndefinedArrayProps(target, DtoWithExplicitArray);
60
+
61
+ expect(target.items).toEqual([]);
62
+ });
63
+ });
@@ -104,6 +104,21 @@ describe('AutoMapper', () => {
104
104
  expect(person.contacts[0].contact).toBe(personRequest.contacts[0].contact);
105
105
  });
106
106
 
107
+ it('inicializa arrays undefined no destino após o map', () => {
108
+ createMap(PersonRequest, Person);
109
+
110
+ const personRequest = PersonRequest.from({
111
+ name: 'John Doe',
112
+ address: {
113
+ address: '123 Main St',
114
+ },
115
+ });
116
+
117
+ const person = AutoMapper.map(personRequest, PersonRequest, Person);
118
+
119
+ expect(person.contacts).toEqual([]);
120
+ });
121
+
107
122
  it('should apply forMember custom mappings', () => {
108
123
  class SourceRequest {
109
124
  @AutoMap()
@@ -0,0 +1,74 @@
1
+ /// <reference types="bun-types/test-globals" />
2
+
3
+ import { PaginationDto } from '@/domain/dtos/pagination.dto';
4
+ import { PersonQueryDto } from '@/domain/dtos/person-query.dto';
5
+ import { describe, expect, it } from 'bun:test';
6
+
7
+ describe('PaginationDto TypeORM', () => {
8
+ it('toFindOptionsOrder normaliza direction para ASC/DESC', () => {
9
+ const query = PaginationDto.from({ orderBy: 'name', direction: 'desc' });
10
+
11
+ expect(query.toFindOptionsOrder()).toEqual({ name: 'DESC' });
12
+ });
13
+
14
+ it('toFindOptionsOrder suporta ordenação aninhada', () => {
15
+ const query = PaginationDto.from({
16
+ orderBy: 'address.city',
17
+ direction: 'asc',
18
+ });
19
+
20
+ expect(query.toFindOptionsOrder()).toEqual({
21
+ address: { city: 'ASC' },
22
+ });
23
+ });
24
+
25
+ it('applyQueryBuilderPagination aplica order, skip e take', () => {
26
+ const qb = {
27
+ expressionMap: {
28
+ orderBys: {} as Record<string, string>,
29
+ skip: 0,
30
+ take: 0,
31
+ },
32
+ addOrderBy(path: string, direction: string) {
33
+ this.expressionMap.orderBys[path] = direction;
34
+ return this;
35
+ },
36
+ skip(value: number) {
37
+ this.expressionMap.skip = value;
38
+ return this;
39
+ },
40
+ take(value: number) {
41
+ this.expressionMap.take = value;
42
+ return this;
43
+ },
44
+ };
45
+ const query = PaginationDto.from({
46
+ page: 1,
47
+ limit: 10,
48
+ orderBy: 'name',
49
+ direction: 'desc',
50
+ });
51
+
52
+ const result = query.applyQueryBuilderPagination(qb as never, 'entity');
53
+
54
+ expect(result.expressionMap.orderBys).toEqual({ 'entity.name': 'DESC' });
55
+ expect(result.expressionMap.skip).toBe(10);
56
+ expect(result.expressionMap.take).toBe(10);
57
+ });
58
+ });
59
+
60
+ describe('PersonQueryDto default sort', () => {
61
+ it('usa id asc quando orderBy não é informado', () => {
62
+ const query = PersonQueryDto.from({ name: 'Jane' });
63
+
64
+ expect(query.generateOrderBy()).toEqual({ id: 'asc' });
65
+ expect(query.toFindOptionsOrder()).toEqual({ id: 'ASC' });
66
+ });
67
+
68
+ it('delega para PaginationDto quando orderBy é informado', () => {
69
+ const query = PersonQueryDto.from({ orderBy: 'name', direction: 'desc' });
70
+
71
+ expect(query.generateOrderBy()).toEqual({ name: 'desc' });
72
+ expect(query.toFindOptionsOrder()).toEqual({ name: 'DESC' });
73
+ });
74
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { resolveCorsOrigin } from '@/core/utils/resolve-cors-origins';
3
+
4
+ describe('resolveCorsOrigin', () => {
5
+ it('permite qualquer origin quando CORS_ORIGINS não está definido', () => {
6
+ expect(resolveCorsOrigin(undefined)).toBe(true);
7
+ expect(resolveCorsOrigin('')).toBe(true);
8
+ expect(resolveCorsOrigin(' ')).toBe(true);
9
+ });
10
+
11
+ it('aceita lista de origens separadas por vírgula', () => {
12
+ expect(
13
+ resolveCorsOrigin('http://localhost:4200, https://app.example.com'),
14
+ ).toEqual(['http://localhost:4200', 'https://app.example.com']);
15
+ });
16
+
17
+ it('retorna string única quando há uma origem', () => {
18
+ expect(resolveCorsOrigin('https://app.example.com')).toBe(
19
+ 'https://app.example.com',
20
+ );
21
+ });
22
+ });
@@ -0,0 +1,39 @@
1
+ /// <reference types="bun-types/test-globals" />
2
+
3
+ import { ListResponseBase } from '@/core/common/list-response.base';
4
+ import { PaginationDto } from '@/domain/dtos/pagination.dto';
5
+ import { describe, expect, it } from 'bun:test';
6
+
7
+ class QueryPersonDto extends PaginationDto {
8
+ name?: string;
9
+ }
10
+
11
+ class PersonListItem {
12
+ id!: number;
13
+ name!: string;
14
+ }
15
+
16
+ class PersonListResponse extends ListResponseBase<PersonListItem> {}
17
+
18
+ describe('PaginationDto.from', () => {
19
+ it('cria instância com props parciais herdando defaults', () => {
20
+ const query = QueryPersonDto.from({ name: 'Jane', limit: 50 });
21
+
22
+ expect(query).toBeInstanceOf(QueryPersonDto);
23
+ expect(query.name).toBe('Jane');
24
+ expect(query.limit).toBe(50);
25
+ expect(query.page).toBe(0);
26
+ });
27
+ });
28
+
29
+ describe('ListResponseBase.from', () => {
30
+ it('cria resposta paginada via ObjectClass.from', () => {
31
+ const response = PersonListResponse.from({
32
+ items: [{ id: 1, name: 'Jane' }],
33
+ count: 1,
34
+ });
35
+
36
+ expect(response.items).toHaveLength(1);
37
+ expect(response.count).toBe(1);
38
+ });
39
+ });
@@ -1,7 +1,10 @@
1
1
  export let e2eDatabaseUrl: string | undefined;
2
+ export let e2eSchemaName: string | undefined;
2
3
 
3
- export function setE2EDatabaseUrl(url: string) {
4
+ export function setE2EDatabaseContext(url: string, schemaName: string) {
4
5
  e2eDatabaseUrl = url;
6
+ e2eSchemaName = schemaName;
5
7
  process.env.DATABASE_URL = url;
8
+ process.env.DATABASE_SCHEMA = schemaName;
6
9
  process.env.NODE_ENV = 'test';
7
10
  }
@@ -12,9 +12,9 @@ import { Test, TestingModule } from '@nestjs/testing';
12
12
  /**
13
13
  * Testes E2E do RepositoryBase - Lazy Loading
14
14
  *
15
- * Valida o carregamento efetivo de relacionamentos em diferentes cenários:
16
- * - findById: carrega recursivamente todos os relacionamentos
17
- * - findMany: carrega com otimização para listas
15
+ * Valida o carregamento de relacionamentos por cenário:
16
+ * - findById: carrega address e contacts (detalhe da entidade)
17
+ * - findMany: retorna apenas dados escalares (performance em listagens)
18
18
  */
19
19
  describe('RepositoryBase - Lazy Loading (E2E)', () => {
20
20
  let moduleRef: TestingModule;
@@ -75,8 +75,8 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
75
75
  });
76
76
  });
77
77
 
78
- describe('findMany - Carregamento Otimizado para Listas', () => {
79
- it('should load persons with relationships in findMany', async () => {
78
+ describe('findMany - Listagem sem Relacionamentos', () => {
79
+ it('should return persons without loading relationships in findMany', async () => {
80
80
  const createHandler = moduleRef.get(CreatePersonHandler);
81
81
 
82
82
  for (let i = 0; i < 2; i++) {
@@ -90,10 +90,7 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
90
90
  }
91
91
 
92
92
  const repository = moduleRef.get(IPersonRepository);
93
- const query = Object.assign(new PersonQueryDto(), {
94
- limit: 100,
95
- page: 0,
96
- });
93
+ const query = PersonQueryDto.from({ limit: 100, page: 0 });
97
94
  const { items } = await repository.findMany(query);
98
95
 
99
96
  expect(items.length).toBeGreaterThan(0);
@@ -101,8 +98,9 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
101
98
  const person = items[0];
102
99
  expect(person.id).toBeDefined();
103
100
  expect(person.name).toBeDefined();
104
- expect(person.address).toBeDefined();
105
- expect(person.contacts).toBeDefined();
101
+ expect(person.active).toBeDefined();
102
+ expect(person.address).toBeUndefined();
103
+ expect(person.contacts).toBeUndefined();
106
104
  });
107
105
 
108
106
  it('should load multiple persons efficiently', async () => {
@@ -136,12 +134,13 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
136
134
  result.items.forEach((person) => {
137
135
  expect(person.id).toBeDefined();
138
136
  expect(person.name).toBeDefined();
137
+ expect(person.active).toBeDefined();
139
138
  });
140
139
  });
141
140
  });
142
141
 
143
142
  describe('Comparação entre findById e findMany', () => {
144
- it('should load relationships in both methods', async () => {
143
+ it('should load relationships only in findById', async () => {
145
144
  const createHandler = moduleRef.get(CreatePersonHandler);
146
145
  const created = await createHandler.handle({
147
146
  name: 'Comparação Test',
@@ -158,11 +157,9 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
158
157
  const personFromFindById = await readHandler.handle(created.id);
159
158
 
160
159
  const repository = moduleRef.get(IPersonRepository);
161
- const query = Object.assign(new PersonQueryDto(), {
162
- limit: 100,
163
- page: 0,
164
- });
165
- const { items } = await repository.findMany(query);
160
+ const { items } = await repository.findMany(
161
+ PersonQueryDto.from({ limit: 100, page: 0 }),
162
+ );
166
163
  const personFromFindMany = items.find((p) => p.id === created.id);
167
164
 
168
165
  expect(personFromFindMany).toBeDefined();
@@ -173,9 +170,8 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
173
170
  expect(personFromFindById.contacts).toBeDefined();
174
171
  expect(personFromFindById.contacts.length).toBeGreaterThan(0);
175
172
 
176
- expect(personFromFindMany?.address).toBeDefined();
177
- expect(personFromFindMany?.contacts).toBeDefined();
178
- expect(personFromFindMany?.contacts.length).toBeGreaterThan(0);
173
+ expect(personFromFindMany?.address).toBeUndefined();
174
+ expect(personFromFindMany?.contacts).toBeUndefined();
179
175
  });
180
176
  });
181
177
  });