@koalarx/nest 4.0.6 → 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 (48) 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 +10 -7
  4. package/cli/utils/parse-new-args.js +0 -1
  5. package/cli/utils/patch-env.js +34 -0
  6. package/cli/utils/patch-main.js +3 -4
  7. package/cli/utils/remove-sample-parts.js +1 -2
  8. package/koala-nest/.env.example +12 -0
  9. package/koala-nest/src/application/common/pagination.request.ts +0 -5
  10. package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
  11. package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
  12. package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
  13. package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
  14. package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
  15. package/koala-nest/src/core/common/list-response.base.ts +10 -0
  16. package/koala-nest/src/core/env.ts +5 -0
  17. package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
  18. package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
  19. package/koala-nest/src/core/tools/mapping/auto-mapper.ts +25 -11
  20. package/koala-nest/src/core/tools/mapping/mapping-store.ts +13 -15
  21. package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
  22. package/koala-nest/src/core/utils/hash-password.ts +7 -2
  23. package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
  24. package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
  25. package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
  26. package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
  27. package/koala-nest/src/domain/entities/person/person.ts +0 -2
  28. package/koala-nest/src/host/bootstrap/apply-http-middleware.ts +22 -0
  29. package/koala-nest/src/host/main.ts +14 -14
  30. package/koala-nest/src/infra/common/redis-cache.service.ts +15 -4
  31. package/koala-nest/src/infra/repositories/person.repository.ts +3 -3
  32. package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
  33. package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
  34. package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
  35. package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
  36. package/koala-nest/src/test/core/env.spec.ts +19 -0
  37. package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
  38. package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
  39. package/koala-nest/src/test/core/mapping.spec.ts +15 -0
  40. package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
  41. package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
  42. package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
  43. package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +20 -60
  44. package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
  45. package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
  46. package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
  47. package/package.json +1 -1
  48. package/koala-nest/src/core/utils/icomparable.ts +0 -1
@@ -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
+ });
@@ -10,13 +10,11 @@ import { IPersonRepository } from '@/domain/repositories/iperson.repository';
10
10
  import { Test, TestingModule } from '@nestjs/testing';
11
11
 
12
12
  /**
13
- * Testes E2E do RepositoryBase - Lazy Loading
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
13
+ * Valida carregamento explícito de relacionamentos no repositório.
14
+ * - findById: relations { address, contacts }
15
+ * - findMany: sem relations (listagem leve; contacts normalizado como [])
18
16
  */
19
- describe('RepositoryBase - Lazy Loading (E2E)', () => {
17
+ describe('PersonRepository - Relations (E2E)', () => {
20
18
  let moduleRef: TestingModule;
21
19
 
22
20
  beforeAll(async () => {
@@ -25,8 +23,8 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
25
23
  }).compile();
26
24
  });
27
25
 
28
- describe('findById - Carregamento de Relacionamentos', () => {
29
- it('should load person with all relationships using findById', async () => {
26
+ describe('findById - relations explícitas', () => {
27
+ it('should load person with address and contacts using findById', async () => {
30
28
  const createHandler = moduleRef.get(CreatePersonHandler);
31
29
  const created = await createHandler.handle({
32
30
  name: 'João Silva',
@@ -53,30 +51,10 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
53
51
  'joao@example.com',
54
52
  ]);
55
53
  });
56
-
57
- it('should have all relationships loaded and accessible', async () => {
58
- const createHandler = moduleRef.get(CreatePersonHandler);
59
- const created = await createHandler.handle({
60
- name: 'Test Person',
61
- contacts: [{ contact: 'test@example.com' }],
62
- address: {
63
- address: 'Test Address',
64
- },
65
- });
66
-
67
- const readHandler = moduleRef.get(ReadPersonHandler);
68
- const person = await readHandler.handle(created.id);
69
-
70
- expect(person.id).toBe(created.id);
71
- expect(person.address).toBeDefined();
72
- expect(person.address.address).toBeDefined();
73
- expect(person.contacts).toBeDefined();
74
- expect(person.contacts.length).toBeGreaterThan(0);
75
- });
76
54
  });
77
55
 
78
- describe('findMany - Carregamento Otimizado para Listas', () => {
79
- it('should load persons with relationships in findMany', async () => {
56
+ describe('findMany - listagem sem relations', () => {
57
+ it('should return persons without loading relations in findMany', async () => {
80
58
  const createHandler = moduleRef.get(CreatePersonHandler);
81
59
 
82
60
  for (let i = 0; i < 2; i++) {
@@ -90,10 +68,7 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
90
68
  }
91
69
 
92
70
  const repository = moduleRef.get(IPersonRepository);
93
- const query = Object.assign(new PersonQueryDto(), {
94
- limit: 100,
95
- page: 0,
96
- });
71
+ const query = PersonQueryDto.from({ limit: 100, page: 0 });
97
72
  const { items } = await repository.findMany(query);
98
73
 
99
74
  expect(items.length).toBeGreaterThan(0);
@@ -101,11 +76,11 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
101
76
  const person = items[0];
102
77
  expect(person.id).toBeDefined();
103
78
  expect(person.name).toBeDefined();
104
- expect(person.address).toBeDefined();
105
- expect(person.contacts).toBeDefined();
79
+ expect(person.address).toBeUndefined();
80
+ expect(person.contacts ?? []).toHaveLength(0);
106
81
  });
107
82
 
108
- it('should load multiple persons efficiently', async () => {
83
+ it('should list via handler without requiring relations on entities', async () => {
109
84
  const createHandler = moduleRef.get(CreatePersonHandler);
110
85
 
111
86
  const createdIds: number[] = [];
@@ -120,8 +95,6 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
120
95
  createdIds.push(result.id);
121
96
  }
122
97
 
123
- expect(createdIds.length).toBe(3);
124
-
125
98
  const readManyHandler = moduleRef.get(ReadManyPersonHandler);
126
99
  const result = await readManyHandler.handle(new ReadManyPersonRequest());
127
100
 
@@ -132,23 +105,15 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
132
105
  createdIds.forEach((id) => {
133
106
  expect(returnedIds).toContain(id);
134
107
  });
135
-
136
- result.items.forEach((person) => {
137
- expect(person.id).toBeDefined();
138
- expect(person.name).toBeDefined();
139
- });
140
108
  });
141
109
  });
142
110
 
143
- describe('Comparação entre findById e findMany', () => {
144
- it('should load relationships in both methods', async () => {
111
+ describe('findById vs findMany', () => {
112
+ it('findById carrega relations; findMany retorna entidade leve', async () => {
145
113
  const createHandler = moduleRef.get(CreatePersonHandler);
146
114
  const created = await createHandler.handle({
147
115
  name: 'Comparação Test',
148
- contacts: [
149
- { contact: 'first@example.com' },
150
- { contact: 'second@example.com' },
151
- ],
116
+ contacts: [{ contact: 'first@example.com' }],
152
117
  address: {
153
118
  address: 'Rua Comparação, 789',
154
119
  },
@@ -158,24 +123,19 @@ describe('RepositoryBase - Lazy Loading (E2E)', () => {
158
123
  const personFromFindById = await readHandler.handle(created.id);
159
124
 
160
125
  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);
126
+ const { items } = await repository.findMany(
127
+ PersonQueryDto.from({ limit: 100, page: 0 }),
128
+ );
166
129
  const personFromFindMany = items.find((p) => p.id === created.id);
167
130
 
168
131
  expect(personFromFindMany).toBeDefined();
169
132
  expect(personFromFindById.id).toBe(personFromFindMany!.id);
170
133
  expect(personFromFindById.name).toBe(personFromFindMany!.name);
171
-
172
134
  expect(personFromFindById.address).toBeDefined();
173
135
  expect(personFromFindById.contacts).toBeDefined();
174
136
  expect(personFromFindById.contacts.length).toBeGreaterThan(0);
175
-
176
- expect(personFromFindMany?.address).toBeDefined();
177
- expect(personFromFindMany?.contacts).toBeDefined();
178
- expect(personFromFindMany?.contacts.length).toBeGreaterThan(0);
137
+ expect(personFromFindMany?.address).toBeUndefined();
138
+ expect(personFromFindMany?.contacts ?? []).toHaveLength(0);
179
139
  });
180
140
  });
181
141
  });
@@ -35,6 +35,14 @@ class RedisStub {
35
35
  );
36
36
  }
37
37
 
38
+ scan(cursor: string, ...args: unknown[]) {
39
+ const pattern = String(args[1] ?? '');
40
+ const prefix = pattern.replace(/\*$/, '');
41
+ const keys = [...this.store.keys()].filter((key) => key.startsWith(prefix));
42
+
43
+ return Promise.resolve(['0', keys]);
44
+ }
45
+
38
46
  disconnect() {}
39
47
  }
40
48
 
@@ -1,17 +1,11 @@
1
+ import { applyHttpMiddleware } from '@/host/bootstrap/apply-http-middleware';
1
2
  import { ErrorsFilter } from '@/host/filters/errors.filter';
2
3
  import { ILoggingService } from '@/domain/common/ilogging.service';
3
4
  import { INestApplication } from '@nestjs/common';
4
5
  import { HttpAdapterHost } from '@nestjs/core';
5
- import cookieParser from 'cookie-parser';
6
6
 
7
7
  export function setupTestApp(app: INestApplication) {
8
- app.use(cookieParser());
9
-
10
- app.enableCors({
11
- credentials: true,
12
- origin: true,
13
- optionsSuccessStatus: 200,
14
- });
8
+ applyHttpMiddleware(app);
15
9
 
16
10
  const { httpAdapter } = app.get(HttpAdapterHost);
17
11
  const loggingService = app.get(ILoggingService);
@@ -0,0 +1,80 @@
1
+ import { PaginationDto } from '@/domain/dtos/pagination.dto';
2
+ import { ListResponseBase } from '@/core/common/list-response.base';
3
+ import { QUERY_FILTER_PARAMS } from '@/core/constants/query-params';
4
+ import { KlArray } from '@koalarx/utils/KlArray';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ export abstract class InMemoryBaseRepository<
8
+ TClass extends { id?: number | string | null },
9
+ > {
10
+ protected items: TClass[] = [];
11
+
12
+ constructor(private readonly typeId: 'number' | 'string' = 'number') {}
13
+
14
+ protected async findById(id: number | string) {
15
+ return this.items.find((item) => item.id === id) ?? null;
16
+ }
17
+
18
+ protected async findMany<T extends PaginationDto>(
19
+ query: T,
20
+ predicate?: (value: TClass, index: number, array: TClass[]) => unknown,
21
+ ) {
22
+ const page = query.page ?? QUERY_FILTER_PARAMS.page;
23
+ const limit = query.limit ?? QUERY_FILTER_PARAMS.limit;
24
+
25
+ return new KlArray(predicate ? this.items.filter(predicate) : this.items)
26
+ .orderBy(query.orderBy ?? '', query.direction)
27
+ .slice(page * limit, (page + 1) * limit);
28
+ }
29
+
30
+ protected async findManyAndCount<T extends PaginationDto>(
31
+ query: T,
32
+ predicate?: (value: TClass, index: number, array: TClass[]) => unknown,
33
+ ): Promise<ListResponseBase<TClass>> {
34
+ const filtered = predicate ? this.items.filter(predicate) : this.items;
35
+ const page = query.page ?? QUERY_FILTER_PARAMS.page;
36
+ const limit = query.limit ?? QUERY_FILTER_PARAMS.limit;
37
+ const items = new KlArray(filtered)
38
+ .orderBy(query.orderBy ?? '', query.direction)
39
+ .slice(page * limit, (page + 1) * limit);
40
+
41
+ return ListResponseBase.from({ items, count: filtered.length });
42
+ }
43
+
44
+ protected async saveChanges(
45
+ item: TClass,
46
+ updateWhere?: (item: TClass) => boolean,
47
+ ) {
48
+ if (!item.id) {
49
+ const id = this.typeId === 'number' ? this.getNewId() : randomUUID();
50
+ (item as { id: number | string }).id = id;
51
+ this.items.push(item);
52
+ return { id };
53
+ }
54
+
55
+ const predicate = updateWhere ?? ((stored) => stored.id === item.id);
56
+ const itemIndex = this.items.findIndex(predicate);
57
+
58
+ if (itemIndex > -1) {
59
+ this.items[itemIndex] = item;
60
+ } else {
61
+ this.items.push(item);
62
+ }
63
+ }
64
+
65
+ protected async remove(
66
+ predicate: (value: TClass, index: number, obj: TClass[]) => unknown,
67
+ ) {
68
+ const itemIndex = this.items.findIndex(predicate);
69
+ if (itemIndex > -1) this.items.splice(itemIndex, 1);
70
+ }
71
+
72
+ private getNewId() {
73
+ if (this.typeId === 'number') {
74
+ const lastId = new KlArray(this.items).orderBy('id', 'desc')[0]?.id;
75
+ return lastId ? (lastId as number) + 1 : 1;
76
+ }
77
+
78
+ return randomUUID();
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koalarx/nest",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
4
4
  "description": "CLI para criar APIs NestJS com arquitetura DDD — copia módulos prontos para o seu repositório.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- export type IComparableId = string | number;