@biorate/nestjs-tools 1.127.0 → 1.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/src/app/application/index.ts +2 -0
- package/src/app/application/ports/client.driven.port.ts +5 -0
- package/src/app/application/ports/crud.driven.port.ts +60 -0
- package/src/app/application/ports/index.ts +3 -0
- package/src/app/application/ports/metrics.driven.port.ts +3 -0
- package/src/app/application/service/client/get-locale.use-case.ts +13 -0
- package/src/app/application/service/client/index.ts +2 -0
- package/src/app/application/service/client/set-locale.use-case.ts +13 -0
- package/src/app/application/service/index.ts +2 -0
- package/src/app/application/service/metrics/get-metrics.use-case.ts +13 -0
- package/src/app/application/service/metrics/index.ts +1 -0
- package/src/app/domain/.keep +0 -0
- package/src/app/index.ts +2 -0
- package/src/app/infrastructure/adapters/client.repository.adapter.ts +34 -0
- package/src/app/infrastructure/adapters/index.ts +2 -0
- package/src/app/infrastructure/adapters/metrics.repository.adapter.ts +13 -0
- package/src/app/infrastructure/controllers/client.controller.ts +28 -0
- package/src/app/infrastructure/controllers/dto/index.ts +1 -0
- package/src/app/infrastructure/controllers/dto/locales.ts +15 -0
- package/src/app/infrastructure/controllers/index.ts +3 -0
- package/src/app/infrastructure/controllers/metrics.controller.ts +16 -0
- package/src/app/infrastructure/controllers/probe.controller.ts +18 -0
- package/src/app/infrastructure/index.ts +3 -0
- package/src/decorators/auth.ts +9 -0
- package/src/decorators/index.ts +2 -0
- package/src/decorators/roles.ts +12 -0
- package/src/enums/index.ts +1 -0
- package/src/enums/user.ts +1 -0
- package/src/errors.ts +13 -0
- package/src/filters/all-exception-filter.ts +91 -0
- package/src/filters/index.ts +1 -0
- package/src/http/controllers/index.ts +3 -0
- package/src/http/controllers/locales.ts +79 -0
- package/src/http/controllers/metrics.ts +21 -0
- package/src/http/controllers/probe.ts +26 -0
- package/src/http/dto/index.ts +1 -0
- package/src/http/dto/locales.ts +15 -0
- package/src/http/index.ts +2 -0
- package/src/index.ts +9 -0
- package/src/interceptors/index.ts +1 -0
- package/src/interceptors/routes-interceptor.ts +39 -0
- package/src/middlewares/index.ts +3 -0
- package/src/middlewares/proxy-middleware.ts +69 -0
- package/src/middlewares/request-count-middleware.ts +41 -0
- package/src/middlewares/response-time-middleware.ts +68 -0
- package/src/providers/auth.ts +36 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/roles.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.128.0](https://github.com/biorate/core/compare/v1.127.0...v1.128.0) (2025-05-22)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **nestjs-tools:** CRUD interfaces added ([9d9b5dc](https://github.com/biorate/core/commit/9d9b5dcdbdff20beb68799fc4fdbee61eec57d60))
|
|
11
|
+
|
|
6
12
|
# [1.127.0](https://github.com/biorate/core/compare/v1.126.0...v1.127.0) (2025-05-22)
|
|
7
13
|
|
|
8
14
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biorate/nestjs-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.128.0",
|
|
4
4
|
"description": "NestJS tools",
|
|
5
5
|
"main": "dist",
|
|
6
6
|
"scripts": {
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"http-proxy-middleware": "2.0.6",
|
|
43
43
|
"on-headers": "1.0.2"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "c4713de966cf9f55cac9adef4bfc3521c5368f02"
|
|
46
46
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type CrudCreateDrivenPort<E> = {
|
|
2
|
+
create(data: E): E;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type CrudFindOneDrivenPort<E, I> = {
|
|
6
|
+
findOne(id: I): E;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CrudFindManyDrivenPort<
|
|
10
|
+
E,
|
|
11
|
+
F = { offset: number; limit: number },
|
|
12
|
+
R = { total: number; offset: number; limit: number; items: E[] },
|
|
13
|
+
> = {
|
|
14
|
+
findMany(filter?: F): R;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CrudUpdateOneDrivenPort<E, I, D = E> = {
|
|
18
|
+
updateOne(id: I, data: D): E;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type CrudUpdateManyDrivenPort<E, C, D = E[]> = {
|
|
22
|
+
updateMany(criteria: C, data: D): E[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type CrudInsertOneDrivenPort<E> = {
|
|
26
|
+
insertOne(data: E): E;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CrudInsertManyDrivenPort<E, R = E[]> = {
|
|
30
|
+
insertOne(items: E[]): R;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CrudDeleteOneDrivenPort<I, R = boolean> = {
|
|
34
|
+
deleteOne(id: I): R;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type CrudDeleteManyDrivenPort<CD, R = number> = {
|
|
38
|
+
deleteMany(criteria?: CD): R;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type CrudDrivenPort<
|
|
42
|
+
Id,
|
|
43
|
+
Entity,
|
|
44
|
+
FindManyFilter,
|
|
45
|
+
InsertManyResult,
|
|
46
|
+
UpdateManyCriteria,
|
|
47
|
+
DeleteCriteria,
|
|
48
|
+
FindManyresult = Entity[],
|
|
49
|
+
UpdateData = Entity,
|
|
50
|
+
DeleteOneResult = boolean,
|
|
51
|
+
DeleteManyResult = number,
|
|
52
|
+
> = CrudCreateDrivenPort<Entity> &
|
|
53
|
+
CrudFindOneDrivenPort<Entity, Id> &
|
|
54
|
+
CrudFindManyDrivenPort<Entity, FindManyFilter, FindManyresult> &
|
|
55
|
+
CrudUpdateOneDrivenPort<Entity, Id, UpdateData> &
|
|
56
|
+
CrudUpdateManyDrivenPort<Entity, UpdateManyCriteria, UpdateData[]> &
|
|
57
|
+
CrudInsertOneDrivenPort<Entity> &
|
|
58
|
+
CrudInsertManyDrivenPort<Entity, InsertManyResult> &
|
|
59
|
+
CrudDeleteOneDrivenPort<Id, DeleteOneResult> &
|
|
60
|
+
CrudDeleteManyDrivenPort<DeleteCriteria, DeleteManyResult>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Types } from '@biorate/inversion';
|
|
3
|
+
import { ClientDrivenPort } from '../../ports';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class GetLocaleUseCase {
|
|
7
|
+
@Inject(Types.ClientDrivenPort)
|
|
8
|
+
protected readonly clientProvider: ClientDrivenPort;
|
|
9
|
+
|
|
10
|
+
public async execute(lang: string, namespace: string) {
|
|
11
|
+
return this.clientProvider.getLang(lang, namespace);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Types } from '@biorate/inversion';
|
|
3
|
+
import { ClientDrivenPort } from '../../ports';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class SetLocaleUseCase {
|
|
7
|
+
@Inject(Types.ClientDrivenPort)
|
|
8
|
+
protected readonly clientProvider: ClientDrivenPort;
|
|
9
|
+
|
|
10
|
+
public async execute(data: Record<string, string>, lang: string, namespace: string) {
|
|
11
|
+
return this.clientProvider.setLang(data, lang, namespace);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Types } from '@biorate/inversion';
|
|
3
|
+
import { MetricsDrivenPort } from '../../ports';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class GetMetricsUseCase {
|
|
7
|
+
@Inject(Types.MetricsDrivenPort)
|
|
8
|
+
protected readonly metricsProvider: MetricsDrivenPort;
|
|
9
|
+
|
|
10
|
+
public async execute() {
|
|
11
|
+
return this.metricsProvider.get();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './get-metrics.use-case';
|
|
File without changes
|
package/src/app/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import { path } from '@biorate/tools';
|
|
4
|
+
import { ClientDrivenPort } from '../../application';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class ClientRepositoryAdapter implements ClientDrivenPort {
|
|
8
|
+
public getLang(lang: string, namespace: string) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(
|
|
11
|
+
readFileSync(
|
|
12
|
+
path.create(process.cwd(), 'locales', namespace, `${lang}.json`),
|
|
13
|
+
'utf-8',
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public setLang(data: Record<string, string>, lang: string, namespace: string) {
|
|
22
|
+
try {
|
|
23
|
+
const directory = path.create(process.cwd(), 'locales', namespace);
|
|
24
|
+
mkdirSync(directory, { recursive: true });
|
|
25
|
+
writeFileSync(
|
|
26
|
+
path.create(directory, `${lang}.json`),
|
|
27
|
+
JSON.stringify(data, null, ' '),
|
|
28
|
+
'utf-8',
|
|
29
|
+
);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { inject, Types } from '@biorate/inversion';
|
|
3
|
+
import { IPrometheus } from '@biorate/prometheus';
|
|
4
|
+
import { MetricsDrivenPort } from '../../application';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class MetricsRepositoryAdapter implements MetricsDrivenPort {
|
|
8
|
+
@inject(Types.Prometheus) protected prometheus: IPrometheus;
|
|
9
|
+
|
|
10
|
+
public get() {
|
|
11
|
+
return this.prometheus.registry.metrics();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Controller, Get, Post, Param, Body, Header } from '@nestjs/common';
|
|
2
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { GetLocaleUseCase, SetLocaleUseCase } from '../../application';
|
|
4
|
+
import { GetLocalesDTO, PostLocalesDTO } from './dto';
|
|
5
|
+
|
|
6
|
+
@ApiTags('Client')
|
|
7
|
+
@Controller('client')
|
|
8
|
+
export class ClientController {
|
|
9
|
+
public constructor(
|
|
10
|
+
protected readonly getLoc: GetLocaleUseCase,
|
|
11
|
+
protected readonly setLoc: SetLocaleUseCase,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
@Get('locale/:lang/:namespace')
|
|
15
|
+
@ApiOperation({ summary: 'Get locale' })
|
|
16
|
+
protected getLocale(@Param() param: GetLocalesDTO) {
|
|
17
|
+
return this.getLoc.execute(param.lang, param.namespace);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Post('locale/:lang/:namespace')
|
|
21
|
+
@ApiOperation({ summary: 'Put locale' })
|
|
22
|
+
protected async postLocale(
|
|
23
|
+
@Param() param: PostLocalesDTO,
|
|
24
|
+
@Body() body: Record<string, string>,
|
|
25
|
+
) {
|
|
26
|
+
return this.setLoc.execute(body, param.lang, param.namespace);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './locales';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsString } from 'class-validator';
|
|
3
|
+
import { ILocalesDTO } from '../../../../../interfaces';
|
|
4
|
+
|
|
5
|
+
export class GetLocalesDTO implements ILocalesDTO {
|
|
6
|
+
@IsString()
|
|
7
|
+
@ApiProperty({ type: String, required: true })
|
|
8
|
+
lang: string;
|
|
9
|
+
|
|
10
|
+
@IsString()
|
|
11
|
+
@ApiProperty({ type: String, required: true })
|
|
12
|
+
namespace: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class PostLocalesDTO extends GetLocalesDTO {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller, Get, Header } from '@nestjs/common';
|
|
2
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { GetMetricsUseCase } from '../../application';
|
|
4
|
+
|
|
5
|
+
@ApiTags('Metrics')
|
|
6
|
+
@Controller('metrics')
|
|
7
|
+
export class MetricsController {
|
|
8
|
+
public constructor(protected readonly getMetrics: GetMetricsUseCase) {}
|
|
9
|
+
|
|
10
|
+
@Get()
|
|
11
|
+
@Header('content-type', 'text/plain')
|
|
12
|
+
@ApiOperation({ summary: 'Prometheus metrics hook' })
|
|
13
|
+
protected async metrics() {
|
|
14
|
+
return this.getMetrics.execute();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
|
|
4
|
+
@ApiTags('Probe')
|
|
5
|
+
@Controller('probe')
|
|
6
|
+
export class ProbeController {
|
|
7
|
+
@Get('readiness')
|
|
8
|
+
@ApiOperation({ summary: 'Readiness probe' })
|
|
9
|
+
protected readiness() {
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Get('healthz')
|
|
14
|
+
@ApiOperation({ summary: 'Healthz probe' })
|
|
15
|
+
protected healthz() {
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ApiBasicAuth } from '@nestjs/swagger';
|
|
3
|
+
import { AuthGuardProvider } from '../providers';
|
|
4
|
+
|
|
5
|
+
export const AuthBasic =
|
|
6
|
+
() => (target: any, propertyKey?: any, descriptor?: TypedPropertyDescriptor<any>) => {
|
|
7
|
+
ApiBasicAuth()(target, propertyKey!, descriptor!);
|
|
8
|
+
UseGuards(AuthGuardProvider)(target, propertyKey!, descriptor!);
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './user';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UserRoles } from '../../interfaces';
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseError } from '@biorate/errors';
|
|
2
|
+
|
|
3
|
+
export class UnsupportedProtocolError extends BaseError {
|
|
4
|
+
public constructor(name: string) {
|
|
5
|
+
super(`Unsupported protocol: [%s]`, [name]);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AxiosRequestError extends BaseError {
|
|
10
|
+
public constructor(code: number, error: string, message: string) {
|
|
11
|
+
super(`Axios request error (%s) [%s]: %s`, [code, error, message]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AxiosError } from '@biorate/axios';
|
|
2
|
+
import { BaseError } from '@biorate/errors';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
import {
|
|
5
|
+
ExceptionFilter,
|
|
6
|
+
Catch,
|
|
7
|
+
ArgumentsHost,
|
|
8
|
+
HttpException,
|
|
9
|
+
HttpStatus,
|
|
10
|
+
BadRequestException,
|
|
11
|
+
} from '@nestjs/common';
|
|
12
|
+
import { AxiosRequestError, UnsupportedProtocolError } from '../errors';
|
|
13
|
+
|
|
14
|
+
@Catch()
|
|
15
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
16
|
+
public catch(exception: Error, host: ArgumentsHost) {
|
|
17
|
+
const type = host.getType();
|
|
18
|
+
const method = <`_${typeof type}`>`_${type}`;
|
|
19
|
+
if (!this[method]) throw new UnsupportedProtocolError(host.getType());
|
|
20
|
+
this[method](exception, host);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private _http(exception: Error, host: ArgumentsHost) {
|
|
24
|
+
const ctx = host.switchToHttp();
|
|
25
|
+
const response = ctx.getResponse();
|
|
26
|
+
const request = ctx.getRequest();
|
|
27
|
+
const meta = (exception as BaseError)?.meta;
|
|
28
|
+
const status =
|
|
29
|
+
exception instanceof HttpException
|
|
30
|
+
? exception.getStatus()
|
|
31
|
+
: meta?.status ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
32
|
+
this.log(exception);
|
|
33
|
+
response.status(status).json({
|
|
34
|
+
status,
|
|
35
|
+
code: this.code(exception),
|
|
36
|
+
hint: this.hint(exception),
|
|
37
|
+
path: request.url,
|
|
38
|
+
meta: meta?.meta,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _ws(exception: Error, host: ArgumentsHost) {
|
|
43
|
+
const ctx = host.switchToWs();
|
|
44
|
+
const socket = ctx.getClient<WebSocket>();
|
|
45
|
+
const meta = (exception as BaseError)?.meta;
|
|
46
|
+
const status = meta?.status ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
47
|
+
this.log(exception);
|
|
48
|
+
socket.send(
|
|
49
|
+
//TODO: add serializers factory
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
event: 'error',
|
|
52
|
+
data: {
|
|
53
|
+
status,
|
|
54
|
+
code: this.code(exception),
|
|
55
|
+
hint: this.hint(exception),
|
|
56
|
+
meta: meta?.meta,
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _rpc(exception: Error, host: ArgumentsHost) {
|
|
63
|
+
throw new UnsupportedProtocolError('rpc');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private code(exception: Error) {
|
|
67
|
+
return (<BaseError>exception).code ?? exception.constructor.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private hint(exception: Error) {
|
|
71
|
+
return (
|
|
72
|
+
(
|
|
73
|
+
(<BadRequestException>exception)?.getResponse?.() as {
|
|
74
|
+
message?: [string];
|
|
75
|
+
}
|
|
76
|
+
)?.message ?? (<BaseError>exception).message
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected log(exception: Error) {
|
|
81
|
+
if (exception instanceof AxiosError)
|
|
82
|
+
console.error(
|
|
83
|
+
new AxiosRequestError(
|
|
84
|
+
exception?.response?.data?.statusCode,
|
|
85
|
+
exception?.response?.data?.error,
|
|
86
|
+
exception?.response?.data?.message,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
else console.error(exception.stack);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './all-exception-filter';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Controller, Get, Post, Param, Body, Header } from '@nestjs/common';
|
|
2
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { path } from '@biorate/tools';
|
|
4
|
+
import { promises as fs, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { merge } from 'lodash';
|
|
6
|
+
import { GetLocalesDTO, PostLocalesDTO } from '../dto';
|
|
7
|
+
|
|
8
|
+
@ApiTags('Locales')
|
|
9
|
+
@Controller('locales')
|
|
10
|
+
export class LocalesController {
|
|
11
|
+
protected static async getFile(namespace: string, lang: string) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(
|
|
14
|
+
await fs.readFile(
|
|
15
|
+
path.create(process.cwd(), 'locales', namespace, `${lang}.json`),
|
|
16
|
+
'utf-8',
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected static getFileSync(namespace: string, lang: string) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(
|
|
27
|
+
readFileSync(
|
|
28
|
+
path.create(process.cwd(), 'locales', namespace, `${lang}.json`),
|
|
29
|
+
'utf-8',
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected static putFile(
|
|
38
|
+
namespace: string,
|
|
39
|
+
lang: string,
|
|
40
|
+
data: Record<string, string>,
|
|
41
|
+
) {
|
|
42
|
+
try {
|
|
43
|
+
const directory = path.create(process.cwd(), 'locales', namespace);
|
|
44
|
+
mkdirSync(directory, { recursive: true });
|
|
45
|
+
writeFileSync(
|
|
46
|
+
path.create(directory, `${lang}.json`),
|
|
47
|
+
JSON.stringify(data, null, ' '),
|
|
48
|
+
'utf-8',
|
|
49
|
+
);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn(e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Get(':lang/:namespace')
|
|
56
|
+
@Header(
|
|
57
|
+
'Deprecation',
|
|
58
|
+
'Use domain controller instead [@biorate/nestjs-tools >= v1.121.0]',
|
|
59
|
+
)
|
|
60
|
+
@ApiOperation({ summary: 'Get locales', deprecated: true })
|
|
61
|
+
protected get(@Param() param: GetLocalesDTO) {
|
|
62
|
+
return LocalesController.getFile(param.namespace, param.lang);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Post(':lang/:namespace')
|
|
66
|
+
@Header(
|
|
67
|
+
'Deprecation',
|
|
68
|
+
'Use domain controller instead [@biorate/nestjs-tools >= v1.121.0]',
|
|
69
|
+
)
|
|
70
|
+
@ApiOperation({ summary: 'Put locales', deprecated: true })
|
|
71
|
+
protected async post(
|
|
72
|
+
@Param() param: PostLocalesDTO,
|
|
73
|
+
@Body() body: Record<string, string>,
|
|
74
|
+
) {
|
|
75
|
+
const data = LocalesController.getFileSync(param.namespace, param.lang);
|
|
76
|
+
const result = merge(data, body);
|
|
77
|
+
LocalesController.putFile(param.namespace, param.lang, result);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { inject, Types } from '@biorate/inversion';
|
|
2
|
+
import { Controller, Get, Header } from '@nestjs/common';
|
|
3
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
4
|
+
import { IPrometheus } from '@biorate/prometheus';
|
|
5
|
+
|
|
6
|
+
@ApiTags('Metrics')
|
|
7
|
+
@Controller('metrics')
|
|
8
|
+
export class MetricsController {
|
|
9
|
+
@inject(Types.Prometheus) protected prometheus: IPrometheus;
|
|
10
|
+
|
|
11
|
+
@Get()
|
|
12
|
+
@Header('content-type', 'text/plain')
|
|
13
|
+
@Header(
|
|
14
|
+
'Deprecation',
|
|
15
|
+
'Use domain controller instead [@biorate/nestjs-tools >= v1.121.0]',
|
|
16
|
+
)
|
|
17
|
+
@ApiOperation({ summary: 'Prometheus metrics hook', deprecated: true })
|
|
18
|
+
protected async metrics() {
|
|
19
|
+
return this.prometheus.registry.metrics();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Controller, Get, Header } from '@nestjs/common';
|
|
2
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
|
|
4
|
+
@ApiTags('Probe')
|
|
5
|
+
@Controller('probe')
|
|
6
|
+
export class ProbeController {
|
|
7
|
+
@Get('readiness')
|
|
8
|
+
@Header(
|
|
9
|
+
'Deprecation',
|
|
10
|
+
'Use domain controller instead [@biorate/nestjs-tools >= v1.121.0]',
|
|
11
|
+
)
|
|
12
|
+
@ApiOperation({ summary: 'Readiness probe', deprecated: true })
|
|
13
|
+
protected readiness() {
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Get('healthz')
|
|
18
|
+
@Header(
|
|
19
|
+
'Deprecation',
|
|
20
|
+
'Use domain controller instead [@biorate/nestjs-tools >= v1.121.0]',
|
|
21
|
+
)
|
|
22
|
+
@ApiOperation({ summary: 'Healthz probe', deprecated: true })
|
|
23
|
+
protected healthz() {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './locales';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsString } from 'class-validator';
|
|
3
|
+
import { ILocalesDTO } from '../../../interfaces';
|
|
4
|
+
|
|
5
|
+
export class GetLocalesDTO implements ILocalesDTO {
|
|
6
|
+
@IsString()
|
|
7
|
+
@ApiProperty({ type: String, required: true })
|
|
8
|
+
lang: string;
|
|
9
|
+
|
|
10
|
+
@IsString()
|
|
11
|
+
@ApiProperty({ type: String, required: true })
|
|
12
|
+
namespace: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class PostLocalesDTO extends GetLocalesDTO {}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './routes-interceptor';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { join, normalize } from 'path/posix';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { PATH_METADATA } from '@nestjs/common/constants';
|
|
4
|
+
import { Request } from 'express';
|
|
5
|
+
import {
|
|
6
|
+
Injectable,
|
|
7
|
+
ExecutionContext,
|
|
8
|
+
CallHandler,
|
|
9
|
+
NestInterceptor,
|
|
10
|
+
} from '@nestjs/common';
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class RoutesInterceptor implements NestInterceptor {
|
|
14
|
+
public static readonly map = new WeakMap<Request, { path: string }>();
|
|
15
|
+
|
|
16
|
+
private readonly reflector = new Reflector();
|
|
17
|
+
|
|
18
|
+
public intercept(context: ExecutionContext, next: CallHandler) {
|
|
19
|
+
const namespaces = this.reflector.get<string | string[]>(
|
|
20
|
+
PATH_METADATA,
|
|
21
|
+
context.getClass(),
|
|
22
|
+
);
|
|
23
|
+
const routes = this.reflector.get<string | string[]>(
|
|
24
|
+
PATH_METADATA,
|
|
25
|
+
context.getHandler(),
|
|
26
|
+
);
|
|
27
|
+
RoutesInterceptor.map.set(context.switchToHttp().getRequest(), {
|
|
28
|
+
path: normalize(
|
|
29
|
+
join(
|
|
30
|
+
'/',
|
|
31
|
+
Array.isArray(namespaces) ? namespaces[0] : namespaces,
|
|
32
|
+
Array.isArray(routes) ? routes[0] : routes,
|
|
33
|
+
'/',
|
|
34
|
+
).slice(0, -1),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
return next.handle();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import type { ServerOptions } from 'http-proxy';
|
|
3
|
+
import { pick, omit } from 'lodash';
|
|
4
|
+
import { Request, Response } from 'express';
|
|
5
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
6
|
+
import { Options, RequestHandler } from 'http-proxy-middleware/dist/types';
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
8
|
+
import { counter, histogram, Counter, Histogram } from '@biorate/prometheus';
|
|
9
|
+
import { time } from '@biorate/tools';
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ProxyPrometheusMiddleware {
|
|
13
|
+
@counter({
|
|
14
|
+
name: 'http_proxy_server_requests_seconds_count',
|
|
15
|
+
help: 'Http proxy server requests count',
|
|
16
|
+
labelNames: ['method', 'uri', 'status'],
|
|
17
|
+
})
|
|
18
|
+
private counter: Counter;
|
|
19
|
+
|
|
20
|
+
@histogram({
|
|
21
|
+
name: 'http_proxy_server_requests_seconds',
|
|
22
|
+
help: 'Http proxy server requests seconds bucket',
|
|
23
|
+
labelNames: ['method', 'uri', 'status'],
|
|
24
|
+
buckets: [0.005, 0.01, 0.02, 0.05, 0.1, 0.3, 0.5, 1, 2, 3, 5, 10],
|
|
25
|
+
})
|
|
26
|
+
private histogram: Histogram;
|
|
27
|
+
|
|
28
|
+
private handler: RequestHandler;
|
|
29
|
+
|
|
30
|
+
private time = new WeakMap<Request, { time: () => number }>();
|
|
31
|
+
|
|
32
|
+
public static create(options?: Options) {
|
|
33
|
+
return new this(options).handler;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private constructor(options: Options = {}) {
|
|
37
|
+
const { onProxyReq, onProxyRes } = pick(options, 'onProxyReq', 'onProxyRes');
|
|
38
|
+
this.handler = createProxyMiddleware({
|
|
39
|
+
...omit(options, 'onProxyReq', 'onProxyRes'),
|
|
40
|
+
onProxyReq: (
|
|
41
|
+
proxyReq: http.ClientRequest,
|
|
42
|
+
req: Request,
|
|
43
|
+
res: Response,
|
|
44
|
+
options: ServerOptions,
|
|
45
|
+
) => {
|
|
46
|
+
this.time.set(req, { time: time.diff() });
|
|
47
|
+
return onProxyReq?.(proxyReq, req, res, options);
|
|
48
|
+
},
|
|
49
|
+
onProxyRes: (proxyRes: http.IncomingMessage, req: Request, res: Response) => {
|
|
50
|
+
const diff = this.time.get(req);
|
|
51
|
+
this.counter
|
|
52
|
+
.labels({
|
|
53
|
+
method: req.method,
|
|
54
|
+
uri: req.baseUrl,
|
|
55
|
+
status: res.statusCode,
|
|
56
|
+
})
|
|
57
|
+
.inc();
|
|
58
|
+
this.histogram
|
|
59
|
+
.labels({
|
|
60
|
+
method: req.method,
|
|
61
|
+
uri: req.baseUrl,
|
|
62
|
+
status: res.statusCode,
|
|
63
|
+
})
|
|
64
|
+
.observe(time.msTo(diff!.time(), 's'));
|
|
65
|
+
return onProxyRes?.(proxyRes, req, res);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import * as onHeaders from 'on-headers';
|
|
3
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
4
|
+
import { counter, Counter } from '@biorate/prometheus';
|
|
5
|
+
import { inject, Types } from '@biorate/inversion';
|
|
6
|
+
import { IConfig } from '@biorate/config';
|
|
7
|
+
import { Request, Response, NextFunction } from 'express';
|
|
8
|
+
import { RoutesInterceptor } from '../interceptors';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class RequestCountMiddleware implements NestMiddleware {
|
|
12
|
+
@inject(Types.Config) private config: IConfig;
|
|
13
|
+
|
|
14
|
+
@counter({
|
|
15
|
+
name: 'http_server_requests_seconds_count',
|
|
16
|
+
help: 'Http server requests count',
|
|
17
|
+
labelNames: ['method', 'uri', 'status'],
|
|
18
|
+
})
|
|
19
|
+
private counter: Counter;
|
|
20
|
+
|
|
21
|
+
public use(req: Request, res: Response, next: NextFunction) {
|
|
22
|
+
onHeaders(res, () => {
|
|
23
|
+
const route = RoutesInterceptor.map.get(req);
|
|
24
|
+
this.counter
|
|
25
|
+
.labels({
|
|
26
|
+
method: req.method,
|
|
27
|
+
uri:
|
|
28
|
+
route?.path ??
|
|
29
|
+
(this.config.get<boolean>(
|
|
30
|
+
'app.middleware.RequestCountMiddleware.log-base-url',
|
|
31
|
+
false,
|
|
32
|
+
)
|
|
33
|
+
? req.baseUrl || req.originalUrl
|
|
34
|
+
: '/'),
|
|
35
|
+
status: res.statusCode,
|
|
36
|
+
})
|
|
37
|
+
.inc();
|
|
38
|
+
});
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import * as onHeaders from 'on-headers';
|
|
3
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
4
|
+
import { inject, Types } from '@biorate/inversion';
|
|
5
|
+
import { IConfig } from '@biorate/config';
|
|
6
|
+
import { histogram, Histogram } from '@biorate/prometheus';
|
|
7
|
+
import { Request, Response, NextFunction } from 'express';
|
|
8
|
+
import { RoutesInterceptor } from '../interceptors';
|
|
9
|
+
import { time } from '@biorate/tools';
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ResponseTimeMiddleware implements NestMiddleware {
|
|
13
|
+
@inject(Types.Config) private config: IConfig;
|
|
14
|
+
|
|
15
|
+
private readonly suffix: boolean;
|
|
16
|
+
|
|
17
|
+
private readonly header: string;
|
|
18
|
+
|
|
19
|
+
private readonly digits: number;
|
|
20
|
+
|
|
21
|
+
@histogram({
|
|
22
|
+
name: 'http_server_requests_seconds',
|
|
23
|
+
help: 'Http server requests seconds bucket',
|
|
24
|
+
labelNames: ['method', 'uri', 'status'],
|
|
25
|
+
buckets: [0.005, 0.01, 0.02, 0.05, 0.1, 0.3, 0.5, 1, 2, 3, 5, 10],
|
|
26
|
+
})
|
|
27
|
+
private histogram: Histogram;
|
|
28
|
+
|
|
29
|
+
public constructor() {
|
|
30
|
+
this.suffix = this.config.get<boolean>(
|
|
31
|
+
'app.middleware.ResponseTimeMiddleware.suffix',
|
|
32
|
+
true,
|
|
33
|
+
);
|
|
34
|
+
this.header = this.config.get<string>(
|
|
35
|
+
'app.middleware.ResponseTimeMiddleware.header',
|
|
36
|
+
'x-response-time',
|
|
37
|
+
);
|
|
38
|
+
this.digits = this.config.get<number>(
|
|
39
|
+
'app.middleware.ResponseTimeMiddleware.digits',
|
|
40
|
+
3,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public use(req: Request, res: Response, next: NextFunction) {
|
|
45
|
+
const diff = time.diff();
|
|
46
|
+
const msTo = time.msTo;
|
|
47
|
+
onHeaders(res, () => {
|
|
48
|
+
const time = diff();
|
|
49
|
+
const route = RoutesInterceptor.map.get(req);
|
|
50
|
+
this.histogram
|
|
51
|
+
.labels({
|
|
52
|
+
method: req.method,
|
|
53
|
+
uri:
|
|
54
|
+
route?.path ??
|
|
55
|
+
(this.config.get<boolean>(
|
|
56
|
+
'app.middleware.ResponseTimeMiddleware.log-base-url',
|
|
57
|
+
false,
|
|
58
|
+
)
|
|
59
|
+
? req.baseUrl || req.originalUrl
|
|
60
|
+
: '/'),
|
|
61
|
+
status: res.statusCode,
|
|
62
|
+
})
|
|
63
|
+
.observe(msTo(time, 's'));
|
|
64
|
+
res.setHeader(this.header, time.toFixed(this.digits) + (this.suffix ? 'ms' : ''));
|
|
65
|
+
});
|
|
66
|
+
next();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { inject, Types } from '@biorate/inversion';
|
|
2
|
+
import { IConfig } from '@biorate/config';
|
|
3
|
+
import {
|
|
4
|
+
Injectable,
|
|
5
|
+
CanActivate,
|
|
6
|
+
ExecutionContext,
|
|
7
|
+
UnauthorizedException,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class AuthGuardProvider implements CanActivate {
|
|
12
|
+
@inject(Types.Config) private config: IConfig;
|
|
13
|
+
|
|
14
|
+
public canActivate(context: ExecutionContext): boolean {
|
|
15
|
+
const request = context.switchToHttp().getRequest();
|
|
16
|
+
const config = this.config.get<{ [key: string]: string } | null>(
|
|
17
|
+
'app.auth.basic',
|
|
18
|
+
null,
|
|
19
|
+
);
|
|
20
|
+
if (!config) return true;
|
|
21
|
+
let username: string, password: string;
|
|
22
|
+
try {
|
|
23
|
+
[username, password] = Buffer.from(
|
|
24
|
+
request.headers.authorization.replace('Basic ', ''),
|
|
25
|
+
'base64',
|
|
26
|
+
)
|
|
27
|
+
.toString('utf-8')
|
|
28
|
+
.split(':');
|
|
29
|
+
} catch {
|
|
30
|
+
throw new UnauthorizedException();
|
|
31
|
+
}
|
|
32
|
+
if (!username || !password) throw new UnauthorizedException();
|
|
33
|
+
if (config[username] === password) return true;
|
|
34
|
+
throw new UnauthorizedException();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { ROLES_KEY } from '../decorators';
|
|
4
|
+
import { UserRoles } from '../enums';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class RolesGuardProvider implements CanActivate {
|
|
8
|
+
constructor(private reflector: Reflector) {}
|
|
9
|
+
/**
|
|
10
|
+
* @description - This method for override
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const request = context.switchToHttp().getRequest();
|
|
14
|
+
* const { login, sessionId } = request.cookies;
|
|
15
|
+
* if (!login || !sessionId) throw new AuthorizationNotAuthorizedError();
|
|
16
|
+
* // get user from db, or somewhere else
|
|
17
|
+
* if (!user) throw new AuthorizationNotAuthorizedError();
|
|
18
|
+
* return user.role;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
protected async getUser(context: ExecutionContext) {
|
|
22
|
+
return { role: UserRoles.Basic | UserRoles.Admin };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async canActivate(context: ExecutionContext) {
|
|
26
|
+
const roles = this.reflector.getAllAndOverride<number>(ROLES_KEY, [
|
|
27
|
+
context.getHandler(),
|
|
28
|
+
context.getClass(),
|
|
29
|
+
]);
|
|
30
|
+
if (roles === undefined) return true;
|
|
31
|
+
const user = await this.getUser(context);
|
|
32
|
+
return (user.role & roles) === roles;
|
|
33
|
+
}
|
|
34
|
+
}
|