@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.
- package/cli/commands/new/configure-test-runner.js +13 -2
- package/cli/commands/new/create-ddd-structure.js +2 -4
- package/cli/constants/cli-project-checklist.js +27 -3
- package/cli/constants/core-packages.js +4 -6
- package/cli/utils/install-module.js +15 -7
- package/cli/utils/install-workspace-config.js +2 -0
- package/cli/utils/parse-new-args.js +0 -1
- package/cli/utils/patch-app-test-module.js +29 -0
- package/cli/utils/patch-env.js +34 -0
- package/cli/utils/patch-main.js +3 -4
- package/cli/utils/remove-sample-parts.js +11 -5
- package/koala-nest/.env.example +12 -0
- package/koala-nest/.vscode/tasks.json +9 -0
- package/koala-nest/src/application/common/pagination.request.ts +0 -5
- package/koala-nest/src/application/common/request-validator.base.ts +36 -1
- 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/env.ts +5 -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 +30 -12
- package/koala-nest/src/core/tools/mapping/mapping-store.ts +35 -29
- 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.ts +0 -2
- 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/repositories/person.repository.ts +3 -3
- package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
- package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
- package/koala-nest/src/test/application/read-many-person.handler.spec.ts +42 -2
- 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 +76 -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/host/controllers/app/app.e2e.spec.ts +24 -0
- package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +20 -60
- package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
- package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
- package/koala-nest/src/test/utils/create-e2e-database.ts +17 -1
- package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
- package/koala-nest/vitest.config.e2e.ts +13 -0
- package/koala-nest/vitest.config.ts +12 -0
- package/package.json +1 -1
- package/koala-nest/src/core/utils/icomparable.ts +0 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// <reference types="bun-types/test-globals" />
|
|
2
|
+
|
|
3
|
+
import { createE2ETestApp } from '@/test/create-e2e-test-app';
|
|
4
|
+
import type { INestApplication } from '@nestjs/common';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Estrutura mínima de E2E — use como ponto de partida no template Padrão.
|
|
8
|
+
* No exemplo CRUD, veja `person.controller.e2e.spec.ts` e `auth.controller.e2e.spec.ts`.
|
|
9
|
+
*/
|
|
10
|
+
describe('App (E2E)', () => {
|
|
11
|
+
let app: INestApplication;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
app = await createE2ETestApp();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
await app.close();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should bootstrap the application', () => {
|
|
22
|
+
expect(app.getHttpServer()).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -10,13 +10,11 @@ import { IPersonRepository } from '@/domain/repositories/iperson.repository';
|
|
|
10
10
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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('
|
|
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 -
|
|
29
|
-
it('should load person with
|
|
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 -
|
|
79
|
-
it('should
|
|
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 =
|
|
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).
|
|
105
|
-
expect(person.contacts).
|
|
79
|
+
expect(person.address).toBeUndefined();
|
|
80
|
+
expect(person.contacts ?? []).toHaveLength(0);
|
|
106
81
|
});
|
|
107
82
|
|
|
108
|
-
it('should
|
|
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('
|
|
144
|
-
it('
|
|
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
|
|
162
|
-
limit: 100,
|
|
163
|
-
|
|
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?.
|
|
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
|
|
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);
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { Type } from '@nestjs/common';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { setE2EDatabaseUrl } from '../e2e-context';
|
|
5
6
|
import { E2EDatabaseClient } from './e2e-database-client';
|
|
6
7
|
|
|
8
|
+
function resolveMigrationRunner(): string {
|
|
9
|
+
const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
|
|
10
|
+
packageManager?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const packageManager = packageJson.packageManager ?? 'bun';
|
|
14
|
+
|
|
15
|
+
if (packageManager === 'bun') {
|
|
16
|
+
return 'bun';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return 'node --import ts-node/register/transpile-only';
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
function generateUniqueDatabaseURL() {
|
|
8
23
|
const schemaId = randomUUID();
|
|
9
24
|
|
|
@@ -33,8 +48,9 @@ export async function createE2EDatabase<T extends E2EDatabaseClient>(
|
|
|
33
48
|
await client.createDatabase(schemaId);
|
|
34
49
|
|
|
35
50
|
const env = { ...process.env, DATABASE_URL: url, NODE_ENV: 'test' };
|
|
51
|
+
const migrationRunner = resolveMigrationRunner();
|
|
36
52
|
execSync(
|
|
37
|
-
|
|
53
|
+
`${migrationRunner} ./node_modules/typeorm/cli.js migration:run -d ./src/infra/database/migrations/migration-datasource.ts`,
|
|
38
54
|
{
|
|
39
55
|
cwd: process.cwd(),
|
|
40
56
|
env,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [tsconfigPaths()],
|
|
6
|
+
test: {
|
|
7
|
+
include: ['src/test/**/*.e2e.spec.ts'],
|
|
8
|
+
setupFiles: ['src/test/setup-e2e.ts'],
|
|
9
|
+
environment: 'node',
|
|
10
|
+
testTimeout: 60_000,
|
|
11
|
+
hookTimeout: 60_000,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [tsconfigPaths()],
|
|
6
|
+
test: {
|
|
7
|
+
include: ['src/test/**/*.spec.ts'],
|
|
8
|
+
exclude: ['src/test/**/*.e2e.spec.ts'],
|
|
9
|
+
setupFiles: ['src/test/setup.ts'],
|
|
10
|
+
environment: 'node',
|
|
11
|
+
},
|
|
12
|
+
});
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type IComparableId = string | number;
|