@koalarx/nest 1.8.5 → 1.9.1
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/README.md +329 -0
- package/core/database/repository.base.d.ts +1 -1
- package/core/database/repository.base.js +3 -2
- package/core/index.d.ts +1 -0
- package/core/koala-app.d.ts +10 -1
- package/core/koala-app.js +56 -1
- package/core/security/strategies/api-key.strategy.d.ts +16 -0
- package/core/security/strategies/api-key.strategy.js +31 -0
- package/core/validators/file-validator.d.ts +27 -0
- package/core/validators/file-validator.js +94 -0
- package/decorators/upload.decorator.d.ts +1 -0
- package/decorators/upload.decorator.js +19 -0
- package/env/env.d.ts +1 -1
- package/package.json +22 -17
- package/services/logging/logging.service.js +2 -2
- package/tsconfig.lib.tsbuildinfo +1 -1
package/README.md
CHANGED
|
@@ -5,3 +5,332 @@
|
|
|
5
5
|
<h1 align="center">@koalarx/nest</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">Uma abstração <a href="https://nestjs.com" target="_blank">Nest.js</a> para APIs escaláveis.</p>
|
|
8
|
+
|
|
9
|
+
# Índice
|
|
10
|
+
1. [Introdução](#introdução)
|
|
11
|
+
2. [Estrutura do Projeto](#estrutura-do-projeto)
|
|
12
|
+
3. [Uso da CLI @koalarx/nest-cli](#uso-da-cli-koalarxnest-cli)
|
|
13
|
+
4. [Recursos Optionais](#recursos-opcionais)
|
|
14
|
+
|
|
15
|
+
4.1. [API Key Strategy](#api-key-strategy)
|
|
16
|
+
|
|
17
|
+
4.2. [Ngrok](#ngrok)
|
|
18
|
+
|
|
19
|
+
4.3. [ApiPropertyEnum](#apipropertyenum)
|
|
20
|
+
|
|
21
|
+
4.4. [Upload](#upload)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Introdução
|
|
26
|
+
|
|
27
|
+
Este projeto utiliza a CLI `@koalarx/nest-cli` para facilitar a criação de aplicações seguindo os princípios do Domain-Driven Design (DDD). A CLI automatiza a configuração inicial e a estruturação do projeto, permitindo que você comece rapidamente a desenvolver sua aplicação.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Estrutura do Projeto
|
|
32
|
+
|
|
33
|
+
A estrutura do projeto gerada pela CLI segue os princípios do DDD, separando as responsabilidades em camadas:
|
|
34
|
+
|
|
35
|
+
- **application**: Contém a lógica de mapeamento e casos de uso.
|
|
36
|
+
- **core**: Configurações e variáveis de ambiente.
|
|
37
|
+
- **domain**: Entidades, DTOs, repositórios e serviços do domínio.
|
|
38
|
+
- **host**: Controladores e ponto de entrada da aplicação.
|
|
39
|
+
- **infra**: Implementações de infraestrutura, como banco de dados e serviços externos.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Uso da CLI @koalarx/nest-cli
|
|
44
|
+
|
|
45
|
+
### Instalação da CLI
|
|
46
|
+
|
|
47
|
+
Certifique-se de instalar a CLI globalmente no seu ambiente:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g @koalarx/nest-cli
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Criação de um Novo Projeto
|
|
54
|
+
|
|
55
|
+
Para criar um novo projeto, execute o seguinte comando:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
koala-nest new my-project
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Este comando irá gerar um projeto com a estrutura recomendada e todas as dependências configuradas.
|
|
62
|
+
|
|
63
|
+
### Recursos Opcionais
|
|
64
|
+
|
|
65
|
+
#### API Key Strategy
|
|
66
|
+
|
|
67
|
+
Tendo em vista a falta de uma opção para o Nest 11 de estratégias de autenticação para APIKey, foi disponibilizada uma abstração para o mesmo no Koala Nest.
|
|
68
|
+
|
|
69
|
+
Abaixo está a estrutura de pastas recomendada para a implementação de segurança no diretório `host/security`:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
host
|
|
73
|
+
└── security
|
|
74
|
+
├── strategies
|
|
75
|
+
│ └── api-key.strategy.ts
|
|
76
|
+
├── guards
|
|
77
|
+
│ └── auth.guard.ts
|
|
78
|
+
└── security.module.ts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
##### Exemplo de implementação
|
|
82
|
+
|
|
83
|
+
###### api-key.strategy.ts
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
DoneFn,
|
|
87
|
+
ApiKeyStrategy as KoalaApiKeyStrategy,
|
|
88
|
+
} from '@koalarx/nest/core/security/strategies/api-key.strategy'
|
|
89
|
+
import { Injectable } from '@nestjs/common'
|
|
90
|
+
import { PassportStrategy } from '@nestjs/passport'
|
|
91
|
+
import { Request } from 'express'
|
|
92
|
+
|
|
93
|
+
@Injectable()
|
|
94
|
+
export class ApiKeyStrategy extends PassportStrategy(
|
|
95
|
+
KoalaApiKeyStrategy,
|
|
96
|
+
'apikey',
|
|
97
|
+
) {
|
|
98
|
+
constructor() {
|
|
99
|
+
super({ header: 'ApiKey' })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
validate(apikey: string, done: DoneFn, request: Request) {
|
|
103
|
+
// Valide a chave de API aqui
|
|
104
|
+
// Por exemplo, verifique se ela corresponde a um valor específico
|
|
105
|
+
if (apikey === 'valid-api-key') {
|
|
106
|
+
// Se for válida, chame done com o objeto do usuário
|
|
107
|
+
return done(null, { userId: 1, username: 'testuser' })
|
|
108
|
+
} else {
|
|
109
|
+
// Se for inválida, chame done com false
|
|
110
|
+
return done(null, false)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
###### auth.guard.ts
|
|
117
|
+
```ts
|
|
118
|
+
import { IS_PUBLIC_KEY } from '@koalarx/nest/decorators/is-public.decorator'
|
|
119
|
+
import { ExecutionContext, Injectable } from '@nestjs/common'
|
|
120
|
+
import { Reflector } from '@nestjs/core'
|
|
121
|
+
import { AuthGuard as NestAuthGuard } from '@nestjs/passport'
|
|
122
|
+
|
|
123
|
+
@Injectable()
|
|
124
|
+
export class AuthGuard extends NestAuthGuard(['apikey']) {
|
|
125
|
+
constructor(private readonly reflector: Reflector) {
|
|
126
|
+
super()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
130
|
+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
131
|
+
context.getHandler(),
|
|
132
|
+
context.getClass(),
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
const request = context.switchToHttp().getRequest()
|
|
136
|
+
|
|
137
|
+
if (isPublic) {
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const canActivate = super.canActivate(context)
|
|
142
|
+
|
|
143
|
+
if (typeof canActivate === 'boolean') {
|
|
144
|
+
return canActivate
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (canActivate as Promise<boolean>).then(async (activated) => {
|
|
148
|
+
if (!request.user) {
|
|
149
|
+
const user = {} // busque o usuário aqui
|
|
150
|
+
|
|
151
|
+
if (user) {
|
|
152
|
+
request.user = user
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return activated
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
###### security.module.ts
|
|
163
|
+
```ts
|
|
164
|
+
import { EnvService } from '@koalarx/nest/env/env.service'
|
|
165
|
+
import { Module } from '@nestjs/common'
|
|
166
|
+
import { PassportModule } from '@nestjs/passport'
|
|
167
|
+
import { ApiKeyStrategy } from './strategies/api-key.strategy'
|
|
168
|
+
|
|
169
|
+
@Module({
|
|
170
|
+
imports: [PassportModule],
|
|
171
|
+
providers: [EnvService, ApiKeyStrategy],
|
|
172
|
+
})
|
|
173
|
+
export class SecurityModule {}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Agora basta importar o módulo de segurança em seu `app.module.ts` e utilizar globalmente ou em um endpoint específico
|
|
177
|
+
|
|
178
|
+
###### app.module.ts
|
|
179
|
+
```ts
|
|
180
|
+
import { CreatePersonJob } from '@/application/person/create-person-job/create-person-job'
|
|
181
|
+
import { DeleteInactiveJob } from '@/application/person/delete-inative-job/delete-inactive-job'
|
|
182
|
+
import { InactivePersonHandler } from '@/application/person/events/inactive-person/inactive-person-handler'
|
|
183
|
+
import { env } from '@/core/env'
|
|
184
|
+
import { KoalaNestModule } from '@koalarx/nest/core/koala-nest.module'
|
|
185
|
+
import { Module } from '@nestjs/common'
|
|
186
|
+
import { PersonModule } from './controllers/person/person.module'
|
|
187
|
+
import { SecurityModule } from './security/security.module'
|
|
188
|
+
|
|
189
|
+
@Module({
|
|
190
|
+
imports: [
|
|
191
|
+
SecurityModule,
|
|
192
|
+
KoalaNestModule.register({
|
|
193
|
+
env,
|
|
194
|
+
controllers: [PersonModule],
|
|
195
|
+
cronJobs: [DeleteInactiveJob, CreatePersonJob],
|
|
196
|
+
eventJobs: [InactivePersonHandler],
|
|
197
|
+
}),
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
export class AppModule {}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Para configurar globalmente inclua o guard em seu arquivo `main.ts`
|
|
204
|
+
|
|
205
|
+
###### main.ts
|
|
206
|
+
```ts
|
|
207
|
+
import { CreatePersonJob } from '@/application/person/create-person-job/create-person-job'
|
|
208
|
+
import { DeleteInactiveJob } from '@/application/person/delete-inative-job/delete-inactive-job'
|
|
209
|
+
import { InactivePersonHandler } from '@/application/person/events/inactive-person/inactive-person-handler'
|
|
210
|
+
import { DbTransactionContext } from '@/infra/database/db-transaction-context'
|
|
211
|
+
import { KoalaApp } from '@koalarx/nest/core/koala-app'
|
|
212
|
+
import { NestFactory } from '@nestjs/core'
|
|
213
|
+
import { AppModule } from './app.module'
|
|
214
|
+
import { AuthGuard } from './security/guards/auth.guard'
|
|
215
|
+
|
|
216
|
+
async function bootstrap() {
|
|
217
|
+
return NestFactory.create(AppModule).then((app) =>
|
|
218
|
+
new KoalaApp(app)
|
|
219
|
+
.useDoc({
|
|
220
|
+
ui: 'scalar',
|
|
221
|
+
endpoint: '/doc',
|
|
222
|
+
title: 'API de Demonstração',
|
|
223
|
+
version: '1.0',
|
|
224
|
+
authorizations: [
|
|
225
|
+
{ name: 'ApiKey', config: { type: 'apiKey', name: 'ApiKey' } },
|
|
226
|
+
],
|
|
227
|
+
})
|
|
228
|
+
.addGlobalGuard(AuthGuard)
|
|
229
|
+
.addCronJob(CreatePersonJob)
|
|
230
|
+
.addCronJob(DeleteInactiveJob)
|
|
231
|
+
.addEventJob(InactivePersonHandler)
|
|
232
|
+
.setAppName('example')
|
|
233
|
+
.setInternalUserName('integration.bot')
|
|
234
|
+
.setDbTransactionContext(DbTransactionContext)
|
|
235
|
+
.enableCors()
|
|
236
|
+
.buildAndServe(),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
bootstrap()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Ngrok
|
|
243
|
+
|
|
244
|
+
[Ngrok](https://ngrok.com) é uma ferramenta que cria túneis seguros para expor servidores locais à internet. Ele é útil para testar webhooks, compartilhar aplicações em desenvolvimento ou acessar serviços locais remotamente.
|
|
245
|
+
|
|
246
|
+
##### Exemplo de implementação
|
|
247
|
+
|
|
248
|
+
Inclua seu token no arquivo `main.ts` no método `.useNgrok()` e inicie a aplicação. O servidor Ngrok será configurado automaticamente para expor sua aplicação local à internet.
|
|
249
|
+
|
|
250
|
+
Certifique-se de substituir `'erarwrqwrqwr...'` pelo seu token de autenticação do Ngrok. Após iniciar a aplicação, você poderá acessar o endereço gerado pelo Ngrok para testar webhooks ou compartilhar sua aplicação em desenvolvimento.
|
|
251
|
+
|
|
252
|
+
###### main.ts
|
|
253
|
+
```ts
|
|
254
|
+
...
|
|
255
|
+
import { KoalaApp } from '@koalarx/nest/core/koala-app'
|
|
256
|
+
import { NestFactory } from '@nestjs/core'
|
|
257
|
+
import { AppModule } from './app.module'
|
|
258
|
+
|
|
259
|
+
async function bootstrap() {
|
|
260
|
+
return NestFactory.create(AppModule).then((app) =>
|
|
261
|
+
new KoalaApp(app)
|
|
262
|
+
...
|
|
263
|
+
.useNgrok('erarwrqwrqwr...') // Inclua sua Chave do Ngrok aqui
|
|
264
|
+
...
|
|
265
|
+
.buildAndServe(),
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
bootstrap()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### ApiPropertyEnum
|
|
272
|
+
|
|
273
|
+
Um decorador para aprimorar o `@ApiProperty` do `@nestjs/swagger`, fornecendo suporte adicional para enumerações. Ele gera uma descrição para os valores do enum, incluindo suas representações numéricas e descrições, e aplica isso à propriedade na documentação.
|
|
274
|
+
|
|
275
|
+
#### Parâmetros
|
|
276
|
+
|
|
277
|
+
- **options** - Opções de configuração para o decorador.
|
|
278
|
+
- **options.enum** - A enumeração a ser documentada. Deve ser um objeto onde as chaves são os nomes dos enums e os valores são suas representações numéricas.
|
|
279
|
+
- **options.required** - (Opcional) Indica se a propriedade é obrigatória.
|
|
280
|
+
|
|
281
|
+
#### Exemplo de Uso
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { ApiPropertyEnum } from './decorators/api-property-enum.decorator';
|
|
285
|
+
|
|
286
|
+
enum Status {
|
|
287
|
+
Ativo = 1,
|
|
288
|
+
Inativo = 2,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
class ExemploDto {
|
|
292
|
+
@ApiPropertyEnum({ enum: Status, required: true })
|
|
293
|
+
status: Status;
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Na documentação, a propriedade `status` exibirá uma descrição com os valores do enum e suas representações numéricas correspondentes, por exemplo:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
Ativo: 1
|
|
301
|
+
Inativo: 2
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### ApiExcludeEndpointDiffDevelop
|
|
305
|
+
|
|
306
|
+
O decorator `ApiExcludeEndpointDiffDevelop` é utilizado para condicionar a exclusão de endpoints na documentação com base no ambiente de execução da aplicação. Ele utiliza a configuração de ambiente definida na classe `EnvConfig` para determinar se o endpoint será ou não excluído.
|
|
307
|
+
|
|
308
|
+
#### Como funciona
|
|
309
|
+
|
|
310
|
+
- Se o ambiente atual for de desenvolvimento (`isEnvDevelop` for `true`), o endpoint será incluído na documentação.
|
|
311
|
+
- Caso contrário, o endpoint será excluído da documentação.
|
|
312
|
+
|
|
313
|
+
### Upload
|
|
314
|
+
|
|
315
|
+
Um decorator personalizado para lidar com o upload de arquivos em um controlador NestJS.
|
|
316
|
+
|
|
317
|
+
@param {number} maxSizeInKb - O tamanho máximo permitido para os arquivos em kilobytes.
|
|
318
|
+
|
|
319
|
+
@param {RegExp} filetype - Um padrão de expressão regular para validar os tipos de arquivo permitidos.
|
|
320
|
+
|
|
321
|
+
@returns {MethodDecorator} - Um decorator que pode ser aplicado a métodos de controladores para processar uploads de arquivos.
|
|
322
|
+
|
|
323
|
+
Este decorator utiliza o `UploadedFiles` do NestJS para processar múltiplos arquivos enviados em uma requisição.
|
|
324
|
+
Ele valida os arquivos com base no tamanho máximo permitido e no tipo de arquivo especificado.
|
|
325
|
+
|
|
326
|
+
Exemplos de uso:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
@Post('upload')
|
|
330
|
+
@UploadDecorator(1024, /\.(jpg|jpeg|png)$/)
|
|
331
|
+
uploadFiles(@UploadedFiles() files: Express.Multer.File[]) {
|
|
332
|
+
console.log(files);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
No exemplo acima, o método `uploadFiles` aceita múltiplos arquivos com tamanho máximo de 1MB (1024 KB) e tipos de arquivo `.jpg`, `.jpeg` ou `.png`.
|
|
@@ -24,7 +24,7 @@ export declare abstract class RepositoryBase<TEntity extends EntityBase<TEntity>
|
|
|
24
24
|
protected findUnique<T>(where: T): Promise<any>;
|
|
25
25
|
protected findMany<T>(where: T, pagination?: PaginationDto): Promise<any>;
|
|
26
26
|
protected findManyAndCount<T>(where: T, pagination?: PaginationDto): Promise<ListResponse<TEntity>>;
|
|
27
|
-
protected saveChanges<TWhere = any>(entity: TEntity, updateWhere?: TWhere): Promise<TEntity
|
|
27
|
+
protected saveChanges<TWhere = any>(entity: TEntity, updateWhere?: TWhere): Promise<TEntity>;
|
|
28
28
|
protected remove<TWhere = any>(where: TWhere, externalServices?: Promise<any>): Promise<any>;
|
|
29
29
|
private listToRelationActionList;
|
|
30
30
|
private entityToPrisma;
|
|
@@ -83,9 +83,10 @@ class RepositoryBase {
|
|
|
83
83
|
.then((response) => this.createEntity(response));
|
|
84
84
|
}
|
|
85
85
|
else {
|
|
86
|
+
const where = updateWhere ?? { id: entity._id };
|
|
86
87
|
return this.withTransaction((client) => this.context(client)
|
|
87
88
|
.update({
|
|
88
|
-
where
|
|
89
|
+
where,
|
|
89
90
|
data: prismaEntity,
|
|
90
91
|
})
|
|
91
92
|
.then(() => {
|
|
@@ -94,7 +95,7 @@ class RepositoryBase {
|
|
|
94
95
|
...relationUpdates.map((relation) => client[relation.modelName].updateMany(relation.schema)),
|
|
95
96
|
...relationDeletes.map((relation) => client[relation.modelName].deleteMany(relation.schema)),
|
|
96
97
|
]);
|
|
97
|
-
})).then(() =>
|
|
98
|
+
})).then(() => this.findFirst(where));
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
async remove(where, externalServices) {
|
package/core/index.d.ts
CHANGED
package/core/koala-app.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INestApplication, Type } from '@nestjs/common';
|
|
1
|
+
import { CanActivate, INestApplication, Type } from '@nestjs/common';
|
|
2
2
|
import { BaseExceptionFilter } from '@nestjs/core';
|
|
3
3
|
import { SecuritySchemeObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
|
|
4
4
|
import { CronJobHandlerBase } from './backgroud-services/cron-service/cron-job.handler.base';
|
|
@@ -35,9 +35,14 @@ export declare class KoalaApp {
|
|
|
35
35
|
private _prismaValidationExceptionFilter;
|
|
36
36
|
private _domainExceptionFilter;
|
|
37
37
|
private _zodExceptionFilter;
|
|
38
|
+
private _guards;
|
|
38
39
|
private _cronJobs;
|
|
39
40
|
private _eventJobs;
|
|
41
|
+
private _apiReferenceEndpoint;
|
|
42
|
+
private _ngrokKey;
|
|
43
|
+
private _ngrokUrl;
|
|
40
44
|
constructor(app: INestApplication<any>);
|
|
45
|
+
addGlobalGuard(Guard: Type<CanActivate>): this;
|
|
41
46
|
addCronJob(job: CronJobClass): this;
|
|
42
47
|
addEventJob(eventJob: EventJobClass): this;
|
|
43
48
|
addCustomGlobalExceptionFilter(filter: BaseExceptionFilter): this;
|
|
@@ -45,10 +50,14 @@ export declare class KoalaApp {
|
|
|
45
50
|
addCustomDomainExceptionFilter(filter: BaseExceptionFilter): this;
|
|
46
51
|
addCustomZodExceptionFilter(filter: BaseExceptionFilter): this;
|
|
47
52
|
useDoc(config: ApiDocConfig): this;
|
|
53
|
+
useNgrok(key: string): this;
|
|
48
54
|
enableCors(): this;
|
|
49
55
|
setAppName(name: string): this;
|
|
50
56
|
setInternalUserName(name: string): this;
|
|
51
57
|
setDbTransactionContext(transactionContext: Type<PrismaTransactionalClient>): this;
|
|
52
58
|
build(): Promise<INestApplication<any>>;
|
|
59
|
+
serve(): Promise<void>;
|
|
60
|
+
buildAndServe(): Promise<void>;
|
|
61
|
+
private showListeningMessage;
|
|
53
62
|
}
|
|
54
63
|
export {};
|
package/core/koala-app.js
CHANGED
|
@@ -5,23 +5,30 @@ const common_1 = require("@nestjs/common");
|
|
|
5
5
|
const core_1 = require("@nestjs/core");
|
|
6
6
|
const swagger_1 = require("@nestjs/swagger");
|
|
7
7
|
const nestjs_api_reference_1 = require("@scalar/nestjs-api-reference");
|
|
8
|
+
const consola = require("consola");
|
|
8
9
|
const expressBasicAuth = require("express-basic-auth");
|
|
10
|
+
const ngrok = require("ngrok");
|
|
11
|
+
const env_service_1 = require("../env/env.service");
|
|
9
12
|
const domain_errors_filter_1 = require("../filters/domain-errors.filter");
|
|
10
13
|
const global_exception_filter_1 = require("../filters/global-exception.filter");
|
|
11
14
|
const prisma_validation_exception_filter_1 = require("../filters/prisma-validation-exception.filter");
|
|
12
15
|
const zod_errors_filter_1 = require("../filters/zod-errors.filter");
|
|
13
16
|
const ilogging_service_1 = require("../services/logging/ilogging.service");
|
|
14
17
|
const koala_global_vars_1 = require("./koala-global-vars");
|
|
15
|
-
const instanciate_class_with_dependencies_injection_1 = require("./utils/instanciate-class-with-dependencies-injection");
|
|
16
18
|
const env_config_1 = require("./utils/env.config");
|
|
19
|
+
const instanciate_class_with_dependencies_injection_1 = require("./utils/instanciate-class-with-dependencies-injection");
|
|
17
20
|
class KoalaApp {
|
|
18
21
|
app;
|
|
19
22
|
_globalExceptionFilter;
|
|
20
23
|
_prismaValidationExceptionFilter;
|
|
21
24
|
_domainExceptionFilter;
|
|
22
25
|
_zodExceptionFilter;
|
|
26
|
+
_guards = [];
|
|
23
27
|
_cronJobs = [];
|
|
24
28
|
_eventJobs = [];
|
|
29
|
+
_apiReferenceEndpoint;
|
|
30
|
+
_ngrokKey;
|
|
31
|
+
_ngrokUrl;
|
|
25
32
|
constructor(app) {
|
|
26
33
|
this.app = app;
|
|
27
34
|
const { httpAdapter } = this.app.get(core_1.HttpAdapterHost);
|
|
@@ -34,6 +41,11 @@ class KoalaApp {
|
|
|
34
41
|
this._domainExceptionFilter = new domain_errors_filter_1.DomainErrorsFilter(loggingService);
|
|
35
42
|
this._zodExceptionFilter = new zod_errors_filter_1.ZodErrorsFilter(loggingService);
|
|
36
43
|
}
|
|
44
|
+
addGlobalGuard(Guard) {
|
|
45
|
+
const reflector = this.app.get(core_1.Reflector);
|
|
46
|
+
this._guards.push(new Guard(reflector));
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
37
49
|
addCronJob(job) {
|
|
38
50
|
this._cronJobs.push(job);
|
|
39
51
|
return this;
|
|
@@ -102,6 +114,7 @@ class KoalaApp {
|
|
|
102
114
|
}
|
|
103
115
|
const document = swagger_1.SwaggerModule.createDocument(this.app, documentBuilder.build());
|
|
104
116
|
const swaggerEndpoint = config.endpoint;
|
|
117
|
+
this._apiReferenceEndpoint = swaggerEndpoint;
|
|
105
118
|
if (config.ui === 'scalar' && swaggerEndpoint === '/') {
|
|
106
119
|
throw new common_1.InternalServerErrorException("O endpoint de documentação não pode ser '/' para UI Scalar.");
|
|
107
120
|
}
|
|
@@ -155,6 +168,10 @@ class KoalaApp {
|
|
|
155
168
|
}
|
|
156
169
|
return this;
|
|
157
170
|
}
|
|
171
|
+
useNgrok(key) {
|
|
172
|
+
this._ngrokKey = key;
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
158
175
|
enableCors() {
|
|
159
176
|
this.app.enableCors({
|
|
160
177
|
credentials: true,
|
|
@@ -185,7 +202,45 @@ class KoalaApp {
|
|
|
185
202
|
for (const eventJob of eventJobs) {
|
|
186
203
|
eventJob.setupSubscriptions();
|
|
187
204
|
}
|
|
205
|
+
for (const guard of this._guards) {
|
|
206
|
+
this.app.useGlobalGuards(guard);
|
|
207
|
+
}
|
|
208
|
+
if (this._ngrokKey) {
|
|
209
|
+
const envService = this.app.get(env_service_1.EnvService);
|
|
210
|
+
const port = envService.get('PORT') ?? 3000;
|
|
211
|
+
await ngrok
|
|
212
|
+
.connect({
|
|
213
|
+
authtoken: this._ngrokKey,
|
|
214
|
+
addr: port,
|
|
215
|
+
})
|
|
216
|
+
.then((url) => {
|
|
217
|
+
this._ngrokUrl = url;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
188
220
|
return this.app;
|
|
189
221
|
}
|
|
222
|
+
async serve() {
|
|
223
|
+
const envService = this.app.get(env_service_1.EnvService);
|
|
224
|
+
const port = envService.get('PORT') ?? 3000;
|
|
225
|
+
this.app.listen(port).then(() => this.showListeningMessage(port));
|
|
226
|
+
}
|
|
227
|
+
async buildAndServe() {
|
|
228
|
+
await this.build();
|
|
229
|
+
await this.serve();
|
|
230
|
+
}
|
|
231
|
+
showListeningMessage(port) {
|
|
232
|
+
const envService = this.app.get(env_service_1.EnvService);
|
|
233
|
+
console.log('------------------------------');
|
|
234
|
+
if (this._apiReferenceEndpoint) {
|
|
235
|
+
consola.info('API Reference:', `http://localhost:${port}${this._apiReferenceEndpoint}`);
|
|
236
|
+
}
|
|
237
|
+
consola.info('Internal Host:', `http://localhost:${port}`);
|
|
238
|
+
if (this._ngrokUrl) {
|
|
239
|
+
consola.info('External Host:', this._ngrokUrl);
|
|
240
|
+
consola.info('External Inspect:', 'http://localhost:4040/inspect/http');
|
|
241
|
+
}
|
|
242
|
+
consola.box('Environment:', envService.get('NODE_ENV'));
|
|
243
|
+
console.log('------------------------------');
|
|
244
|
+
}
|
|
190
245
|
}
|
|
191
246
|
exports.KoalaApp = KoalaApp;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
import { Strategy } from 'passport-custom';
|
|
3
|
+
export type DoneFn = (err: Error | null, user?: any) => void;
|
|
4
|
+
interface ApiKeyStrategyOptions {
|
|
5
|
+
header: string;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
}
|
|
8
|
+
declare abstract class ApiKeyStrategyBase extends Strategy {
|
|
9
|
+
constructor({ header, prefix }: ApiKeyStrategyOptions);
|
|
10
|
+
abstract validate(apikey: string, done: DoneFn, request: Request): Promise<void> | void;
|
|
11
|
+
}
|
|
12
|
+
export declare class ApiKeyStrategy extends ApiKeyStrategyBase {
|
|
13
|
+
constructor(options: ApiKeyStrategyOptions);
|
|
14
|
+
validate(apikey: string, done: DoneFn, request: Request): void;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiKeyStrategy = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const passport_custom_1 = require("passport-custom");
|
|
6
|
+
class ApiKeyStrategyBase extends passport_custom_1.Strategy {
|
|
7
|
+
constructor({ header, prefix }) {
|
|
8
|
+
super(async (request, done) => {
|
|
9
|
+
try {
|
|
10
|
+
const apikey = request.headers[header.toLowerCase()];
|
|
11
|
+
const apiKeyEncoded = apikey?.replace(`${prefix || ''} `, '');
|
|
12
|
+
if (apiKeyEncoded) {
|
|
13
|
+
return await this.validate(apiKeyEncoded, done, request);
|
|
14
|
+
}
|
|
15
|
+
return done(new common_1.UnauthorizedException());
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return done(err, null);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class ApiKeyStrategy extends ApiKeyStrategyBase {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
super(options);
|
|
26
|
+
}
|
|
27
|
+
validate(apikey, done, request) {
|
|
28
|
+
throw new Error('Method not implemented.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.ApiKeyStrategy = ApiKeyStrategy;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FileValidator } from '@nestjs/common';
|
|
2
|
+
import { FileType } from '..';
|
|
3
|
+
type FileTypeInternal = FileType | FileType[] | Record<string, FileType[]>;
|
|
4
|
+
export declare class FileSizeValidator extends FileValidator {
|
|
5
|
+
private maxSizeBytes;
|
|
6
|
+
private multiple;
|
|
7
|
+
private errorFileName;
|
|
8
|
+
constructor(args: {
|
|
9
|
+
maxSizeBytes: number;
|
|
10
|
+
multiple: boolean;
|
|
11
|
+
});
|
|
12
|
+
isValid(file?: FileTypeInternal): Promise<boolean>;
|
|
13
|
+
buildErrorMessage(file: any): string;
|
|
14
|
+
}
|
|
15
|
+
export declare class FileTypeValidator extends FileValidator {
|
|
16
|
+
private multiple;
|
|
17
|
+
private errorFileName;
|
|
18
|
+
private filetype;
|
|
19
|
+
constructor(args: {
|
|
20
|
+
multiple: boolean;
|
|
21
|
+
filetype: RegExp | string;
|
|
22
|
+
});
|
|
23
|
+
isMimeTypeValid(file: FileType): boolean;
|
|
24
|
+
isValid(file?: FileTypeInternal): Promise<boolean>;
|
|
25
|
+
buildErrorMessage(file: any): string;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileTypeValidator = exports.FileSizeValidator = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const runFileValidation = async (args) => {
|
|
6
|
+
if (args.multiple) {
|
|
7
|
+
const fileFields = Object.keys(args.file);
|
|
8
|
+
for (const field of fileFields) {
|
|
9
|
+
const fieldFile = args.file[field];
|
|
10
|
+
if (Array.isArray(fieldFile)) {
|
|
11
|
+
for (const f of fieldFile) {
|
|
12
|
+
if (!args.validator(f)) {
|
|
13
|
+
return { errorFileName: f.originalname, isValid: false };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
if (!args.validator(fieldFile)) {
|
|
19
|
+
return { errorFileName: fieldFile.originalname, isValid: false };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { isValid: true };
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(args.file)) {
|
|
26
|
+
for (const f of args.file) {
|
|
27
|
+
if (!args.validator(f)) {
|
|
28
|
+
return { errorFileName: f.originalname, isValid: false };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { isValid: true };
|
|
32
|
+
}
|
|
33
|
+
if (args.validator(args.file)) {
|
|
34
|
+
return { errorFileName: args.file.originalname, isValid: false };
|
|
35
|
+
}
|
|
36
|
+
return { isValid: true };
|
|
37
|
+
};
|
|
38
|
+
class FileSizeValidator extends common_1.FileValidator {
|
|
39
|
+
maxSizeBytes;
|
|
40
|
+
multiple;
|
|
41
|
+
errorFileName;
|
|
42
|
+
constructor(args) {
|
|
43
|
+
super({});
|
|
44
|
+
this.maxSizeBytes = args.maxSizeBytes;
|
|
45
|
+
this.multiple = args.multiple;
|
|
46
|
+
}
|
|
47
|
+
async isValid(file) {
|
|
48
|
+
if (!file) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const result = await runFileValidation({
|
|
52
|
+
file,
|
|
53
|
+
multiple: this.multiple,
|
|
54
|
+
validator: (f) => f.size < this.maxSizeBytes,
|
|
55
|
+
});
|
|
56
|
+
this.errorFileName = result.errorFileName ?? '';
|
|
57
|
+
return result.isValid;
|
|
58
|
+
}
|
|
59
|
+
buildErrorMessage(file) {
|
|
60
|
+
return (`file ${this.errorFileName || ''} exceeded the size limit ` +
|
|
61
|
+
parseFloat((this.maxSizeBytes / 1024 / 1024).toFixed(2)) +
|
|
62
|
+
'MB');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.FileSizeValidator = FileSizeValidator;
|
|
66
|
+
class FileTypeValidator extends common_1.FileValidator {
|
|
67
|
+
multiple;
|
|
68
|
+
errorFileName;
|
|
69
|
+
filetype;
|
|
70
|
+
constructor(args) {
|
|
71
|
+
super({});
|
|
72
|
+
this.multiple = args.multiple;
|
|
73
|
+
this.filetype = args.filetype;
|
|
74
|
+
}
|
|
75
|
+
isMimeTypeValid(file) {
|
|
76
|
+
return file.mimetype.search(this.filetype) === 0;
|
|
77
|
+
}
|
|
78
|
+
async isValid(file) {
|
|
79
|
+
if (!file) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const result = await runFileValidation({
|
|
83
|
+
multiple: this.multiple,
|
|
84
|
+
file,
|
|
85
|
+
validator: (f) => this.isMimeTypeValid(f),
|
|
86
|
+
});
|
|
87
|
+
this.errorFileName = result.errorFileName ?? '';
|
|
88
|
+
return result.isValid;
|
|
89
|
+
}
|
|
90
|
+
buildErrorMessage(file) {
|
|
91
|
+
return `file ${this.errorFileName || ''} must be of type ${this.filetype}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.FileTypeValidator = FileTypeValidator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function UploadDecorator(maxSizeInKb: number, filetype: RegExp): ParameterDecorator;
|