@koalarx/nest 4.0.5 → 4.0.8

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 (59) hide show
  1. package/cli/commands/new/configure-test-runner.js +13 -2
  2. package/cli/commands/new/create-ddd-structure.js +2 -4
  3. package/cli/constants/cli-project-checklist.js +27 -3
  4. package/cli/constants/core-packages.js +4 -6
  5. package/cli/utils/install-module.js +15 -7
  6. package/cli/utils/install-workspace-config.js +2 -0
  7. package/cli/utils/parse-new-args.js +0 -1
  8. package/cli/utils/patch-app-test-module.js +29 -0
  9. package/cli/utils/patch-env.js +34 -0
  10. package/cli/utils/patch-main.js +3 -4
  11. package/cli/utils/remove-sample-parts.js +11 -5
  12. package/koala-nest/.env.example +12 -0
  13. package/koala-nest/.vscode/tasks.json +9 -0
  14. package/koala-nest/src/application/common/pagination.request.ts +0 -5
  15. package/koala-nest/src/application/common/request-validator.base.ts +36 -1
  16. package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
  17. package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
  18. package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
  19. package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
  20. package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
  21. package/koala-nest/src/core/common/list-response.base.ts +10 -0
  22. package/koala-nest/src/core/env.ts +5 -0
  23. package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
  24. package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
  25. package/koala-nest/src/core/tools/mapping/auto-mapper.ts +30 -12
  26. package/koala-nest/src/core/tools/mapping/mapping-store.ts +35 -29
  27. package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
  28. package/koala-nest/src/core/utils/hash-password.ts +7 -2
  29. package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
  30. package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
  31. package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
  32. package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
  33. package/koala-nest/src/domain/entities/person/person.ts +0 -2
  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/repositories/person.repository.ts +3 -3
  38. package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
  39. package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
  40. package/koala-nest/src/test/application/read-many-person.handler.spec.ts +42 -2
  41. package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
  42. package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
  43. package/koala-nest/src/test/core/env.spec.ts +19 -0
  44. package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
  45. package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
  46. package/koala-nest/src/test/core/mapping.spec.ts +76 -0
  47. package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
  48. package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
  49. package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
  50. package/koala-nest/src/test/host/controllers/app/app.e2e.spec.ts +24 -0
  51. package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +20 -60
  52. package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
  53. package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
  54. package/koala-nest/src/test/utils/create-e2e-database.ts +17 -1
  55. package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
  56. package/koala-nest/vitest.config.e2e.ts +13 -0
  57. package/koala-nest/vitest.config.ts +12 -0
  58. package/package.json +1 -1
  59. package/koala-nest/src/core/utils/icomparable.ts +0 -1
@@ -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.use(cookieParser());
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
- await app.listen(process.env.PORT || 3000);
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
- console.log(`Server is running on port ${process.env.PORT || 3000}`);
38
- console.log(
39
- `Documentation is available at http://localhost:${process.env.PORT || 3000}/doc`,
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
- const keys = await this.client.keys(pattern);
62
+ let cursor = '0';
63
63
 
64
- if (keys.length > 0) {
65
- await this.client.del(...keys);
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() {
@@ -30,18 +30,18 @@ export class PersonRepository
30
30
  return this.repository
31
31
  .findAndCount({
32
32
  where,
33
- order: query.generateOrderBy(),
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.repository.findOne({
44
+ return this.findOneNormalized({
45
45
  where: { id },
46
46
  relations: { address: true, contacts: true },
47
47
  });
@@ -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
  }
@@ -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
  });
@@ -21,18 +21,58 @@ describe('ReadManyPersonHandler', () => {
21
21
  contacts: [],
22
22
  });
23
23
 
24
+ let capturedPage: number | undefined;
24
25
  const repository = {
25
- findMany: async () => ({ items: [person], count: 1 }),
26
+ findMany: async (query: { page?: number; skip: () => number }) => {
27
+ capturedPage = query.page;
28
+ return { items: [person], count: 1 };
29
+ },
26
30
  } as unknown as IPersonRepository;
27
31
 
28
32
  const handler = new ReadManyPersonHandler(repository, new CacheStub());
29
- const result = await handler.handle(new ReadManyPersonRequest());
33
+ const result = await handler.handle({ page: '2', limit: '5' } as never);
30
34
 
35
+ expect(capturedPage).toBe(1);
31
36
  expect(result.count).toBe(1);
32
37
  expect(result.items).toHaveLength(1);
33
38
  expect(result.items[0].name).toBe('Jane');
34
39
  });
35
40
 
41
+ it('preserva paginação ao receber instância de ReadManyPersonRequest', async () => {
42
+ let capturedPage: number | undefined;
43
+ const repository = {
44
+ findMany: async (query: { page?: number }) => {
45
+ capturedPage = query.page;
46
+ return { items: [], count: 0 };
47
+ },
48
+ } as unknown as IPersonRepository;
49
+
50
+ const handler = new ReadManyPersonHandler(repository, new CacheStub());
51
+ const request = Object.assign(new ReadManyPersonRequest(), {
52
+ page: '2',
53
+ limit: '5',
54
+ });
55
+
56
+ await handler.handle(request);
57
+
58
+ expect(capturedPage).toBe(1);
59
+ });
60
+
61
+ it('aplica defaults de paginação quando a request não informa page', async () => {
62
+ let capturedPage: number | undefined;
63
+ const repository = {
64
+ findMany: async (query: { page?: number }) => {
65
+ capturedPage = query.page;
66
+ return { items: [], count: 0 };
67
+ },
68
+ } as unknown as IPersonRepository;
69
+
70
+ const handler = new ReadManyPersonHandler(repository, new CacheStub());
71
+ await handler.handle(new ReadManyPersonRequest());
72
+
73
+ expect(capturedPage).toBe(0);
74
+ });
75
+
36
76
  it('reutiliza cache para a mesma consulta de listagem', async () => {
37
77
  const person = Person.from({
38
78
  id: 1,
@@ -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()
@@ -137,6 +152,67 @@ describe('AutoMapper', () => {
137
152
  expect(target.fullName).toBe('John Doe');
138
153
  });
139
154
 
155
+ it('should map inherited properties from parent classes', () => {
156
+ class BaseRequest {
157
+ @AutoMap()
158
+ page?: number;
159
+
160
+ @AutoMap()
161
+ limit?: number;
162
+ }
163
+
164
+ class ChildRequest extends BaseRequest {
165
+ @AutoMap()
166
+ name?: string;
167
+ }
168
+
169
+ class BaseDto {
170
+ @AutoMap()
171
+ page?: number = 0;
172
+
173
+ @AutoMap()
174
+ limit?: number = 10;
175
+ }
176
+
177
+ class ChildDto extends BaseDto {
178
+ @AutoMap()
179
+ name?: string;
180
+ }
181
+
182
+ createMap(ChildRequest, ChildDto);
183
+
184
+ const source = Object.assign(new ChildRequest(), {
185
+ page: 2,
186
+ limit: 25,
187
+ name: 'John',
188
+ });
189
+
190
+ const target = AutoMapper.map(source, ChildRequest, ChildDto);
191
+
192
+ expect(target.page).toBe(2);
193
+ expect(target.limit).toBe(25);
194
+ expect(target.name).toBe('John');
195
+ });
196
+
197
+ it('should resolve inherited property types via getProps and getPropType', () => {
198
+ class BaseRequest {
199
+ @AutoMap()
200
+ page?: number;
201
+ }
202
+
203
+ class ChildRequest extends BaseRequest {
204
+ @AutoMap()
205
+ name?: string;
206
+ }
207
+
208
+ const props = MappingStore.getProps(ChildRequest).map((p) => p.name);
209
+
210
+ expect(props).toContain('page');
211
+ expect(props).toContain('name');
212
+ expect(MappingStore.getPropType(ChildRequest, 'page')).toBe(Number);
213
+ expect(MappingStore.getPropType(ChildRequest, 'name')).toBe(String);
214
+ });
215
+
140
216
  it('should map nested objects using the target property name', () => {
141
217
  class SourceChild {
142
218
  @AutoMap()