feat(api): tanti moduli

This commit is contained in:
Francesco Spilla 2025-03-19 18:29:59 +01:00
parent aa07f3d4f9
commit 7970ccfc0e
98 changed files with 783 additions and 63 deletions
apps/ebitemp-api/src
app
app.controller.tsapp.module.ts
modules/database/connections/ebitemp-api/entities
features/anagrafica
main.ts
modules
auth
cls
config
database
enumify
file-transactions
health
keyv
logger
request-logging
schedule
validation
package.jsonpnpm-lock.yaml
tools/typeorm/src/generators/typeorm-model-generator

View File

@ -1,6 +1,6 @@
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
import { AppService } from './app.service';
import { Public } from './modules/auth/strategies/jwt/jwt-auth.guard';
import { Public } from '../modules/auth/strategies/jwt/jwt-auth.guard';
@Public()
@Controller({ version: VERSION_NEUTRAL })

View File

@ -2,27 +2,34 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppAuthModule } from './modules/auth/auth.module';
import { AppConfigModule } from './modules/config/config.module';
import { AppDatabaseModule } from './modules/database/database.module';
import { EnumifyModule } from './modules/enumify/enumify.module';
import { AppFileTransactionsModule } from './modules/file-transactions/file-transactions.module';
import { AppHealthModule } from './modules/health/health.module';
import { AppLoggerModule } from './modules/logger/logger.module';
import { AppScheduleModule } from './modules/schedule/schedule.module';
import { AppValidationModule } from './modules/validation/validation.module';
import { AppAuthModule } from '../modules/auth/auth.module';
import { AppClsModule } from '../modules/cls/cls.module';
import { AppConfigModule } from '../modules/config/config.module';
import { AppDatabaseModule } from '../modules/database/database.module';
import { EnumifyModule } from '../modules/enumify/enumify.module';
import { AppFileTransactionsModule } from '../modules/file-transactions/file-transactions.module';
import { AppHealthModule } from '../modules/health/health.module';
import { AppLoggerModule } from '../modules/logger/logger.module';
import { AppRequestLoggingModule } from '../modules/request-logging/request-logging.module';
import { AppScheduleModule } from '../modules/schedule/schedule.module';
import { AppValidationModule } from '../modules/validation/validation.module';
import { AnagraficaModule } from '../features/anagrafica/anagrafica.module';
@Module({
imports: [
AppConfigModule,
AppLoggerModule,
AppDatabaseModule,
AppClsModule,
AppHealthModule,
AppFileTransactionsModule,
AppScheduleModule,
AppValidationModule,
AppAuthModule,
AppRequestLoggingModule,
EnumifyModule,
AnagraficaModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -1,4 +0,0 @@
export * from './accounts.entity';
export * from './profili.entity';
export * from './ruoli.entity';
export * from './tipi_jobs.entity';

View File

@ -0,0 +1,20 @@
import { Test } from '@nestjs/testing';
import { AnagraficaController } from './anagrafica.controller';
import { AnagraficaService } from './anagrafica.service';
describe('AnagraficaController', () => {
let controller: AnagraficaController;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [AnagraficaService],
controllers: [AnagraficaController],
}).compile();
controller = module.get(AnagraficaController);
});
it('should be defined', () => {
expect(controller).toBeTruthy();
});
});

View File

@ -0,0 +1,28 @@
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiOkPaginatedResponse, ApiPaginationQuery, Paginate, PaginateQuery } from 'nestjs-paginate';
import { SocioEntity } from '../../modules/database/connections/oceano';
import { AnagraficaService } from './anagrafica.service';
import { RequestLoggerInterceptor } from '../../modules/request-logging/request-logger.interceptor';
@UseInterceptors(RequestLoggerInterceptor)
@ApiBearerAuth()
@ApiTags('anagrafica')
@Controller('anagrafiche')
export class AnagraficaController {
constructor(private anagraficaService: AnagraficaService) {}
@Get()
@ApiOperation({ summary: 'Find all Anagrafiche' })
@ApiPaginationQuery(AnagraficaService.findPaginateConfig())
@ApiOkPaginatedResponse(SocioEntity, AnagraficaService.findPaginateConfig())
async getAnagrafiche(@Paginate() query: PaginateQuery) {
return this.anagraficaService.getAnagrafiche(query);
}
@Get('id/:id')
@ApiOperation({ summary: 'Get one Anagrafica by id' })
async getOneAnagraficaById(@Param('id') id: string) {
return this.anagraficaService.getOneAnagraficaById(id);
}
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SocioEntity } from '../../modules/database/connections/oceano';
import { OCEANO_DATASOURCE } from '../../modules/database/connections/oceano/database.constants';
import { AnagraficaController } from './anagrafica.controller';
import { AnagraficaService } from './anagrafica.service';
@Module({
imports: [TypeOrmModule.forFeature([SocioEntity], OCEANO_DATASOURCE)],
controllers: [AnagraficaController],
providers: [AnagraficaService],
exports: [AnagraficaService],
})
export class AnagraficaModule {}

View File

@ -0,0 +1,18 @@
import { Test } from '@nestjs/testing';
import { AnagraficaService } from './anagrafica.service';
describe('AnagraficaService', () => {
let service: AnagraficaService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [AnagraficaService],
}).compile();
service = module.get(AnagraficaService);
});
it('should be defined', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,42 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { paginate, PaginateConfig, PaginateQuery } from 'nestjs-paginate';
import { Repository } from 'typeorm';
import { SocioEntity } from '../../modules/database/connections/oceano';
import { OCEANO_DATASOURCE } from '../../modules/database/connections/oceano/database.constants';
@Injectable()
export class AnagraficaService {
private readonly logger = new Logger(AnagraficaService.name);
static readonly findPaginateConfig = (): PaginateConfig<SocioEntity> => ({
relations: {},
sortableColumns: ['codiceSocio', 'idNucleo', 'idPersona'],
searchableColumns: [],
defaultSortBy: [['codiceSocio', 'ASC']],
defaultLimit: 50,
maxLimit: -1,
filterableColumns: {},
relativePath: true,
where: {
codiceGruppo: '6',
},
});
constructor(
@InjectRepository(SocioEntity, OCEANO_DATASOURCE) private readonly sociRepository: Repository<SocioEntity>
) {}
async getAnagrafiche(query: PaginateQuery) {
const config = AnagraficaService.findPaginateConfig();
query.limit ??= -1;
return await paginate(query, this.sociRepository, config);
}
async getOneAnagraficaById(idAnagrafica: string) {
return await this.sociRepository.findOne({
where: { ...AnagraficaService.findPaginateConfig().where, codiceSocio: idAnagrafica },
});
}
}

View File

@ -6,10 +6,10 @@ import { Logger as PinoLogger } from 'nestjs-pino';
import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
import { flow } from 'lodash';
import { AppModule } from './app/app.module';
import { patchPublicDecoratorSupport } from './app/modules/auth/strategies/jwt/jwt-auth.guard';
import { LocalConfig, localConfig } from './app/modules/config/local.config';
import { patchTypeOrm } from './app/modules/database/utils/typeorm-patch';
import { patchNestjsSwagger } from './app/modules/validation/utils/nestjs-swagger-patches';
import { patchPublicDecoratorSupport } from './modules/auth/strategies/jwt/jwt-auth.guard';
import { LocalConfig, localConfig } from './modules/config/local.config';
import { patchTypeOrm } from './modules/database/utils/typeorm-patch';
import { patchNestjsSwagger } from './modules/validation/utils/nestjs-swagger-patches';
async function bootstrap() {
await patchTypeOrm();

View File

@ -1,10 +1,11 @@
import { Body, Controller, HttpCode, HttpStatus, Post, UnauthorizedException, UseGuards } from '@nestjs/common';
import { Body, Controller, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';
import { AccountsEntity } from '../database/connections/ebitemp-api/entities';
import { LoginDto, LoginResDto } from './auth.dto';
import { AuthService } from './auth.service';
import { AuthenticatedUser } from './authenticated-user.decorator';
import { Public } from './strategies/jwt/jwt-auth.guard';
import { JwtRefreshGuard } from './strategies/jwt/jwt-refresh.guard';
@ApiBearerAuth()
@Controller('auth')
@ -17,21 +18,14 @@ export class AuthController {
async logIn(@Body() body: LoginDto): Promise<LoginResDto> {
const { username, password } = body;
const user = await this.authService.getAuthenticatedUser(username, password);
const { accessToken, refreshToken } = await this.authService.signJwts(user);
return {
accessToken: accessToken,
refreshToken: refreshToken,
};
return await this.authService.signJwts(user);
}
@HttpCode(HttpStatus.OK)
@UseGuards(JwtRefreshGuard)
@Public(true)
@Post('refresh')
async refresh(@AuthenticatedUser() user: AccountsEntity): Promise<LoginResDto> {
const { accessToken, refreshToken } = await this.authService.signJwts(user);
return {
accessToken: accessToken,
refreshToken: refreshToken,
};
return await this.authService.signJwts(user);
}
}

View File

@ -11,7 +11,9 @@ export class LoginDto extends createZodDto(loginSchema) {}
export const loginResSchema = z.object({
accessToken: z.string().jwt(),
accessTokenExp: z.string().datetime(),
refreshToken: z.string().jwt(),
refreshTokenExp: z.string().datetime()
})
export type LoginRes = z.infer<typeof loginResSchema>;
export class LoginResDto extends createZodDto(loginResSchema) {}

View File

@ -11,10 +11,11 @@ import { JwtAuthGuard } from './strategies/jwt/jwt-auth.guard';
import { JwtRefreshTokenAuthStrategy } from './strategies/jwt/jwt-refresh-token-auth.strategy';
import { JwtRefreshTokenModule } from './strategies/jwt/jwt-refresh-token.module';
import { UsersAuthModule } from './users/users.module';
import { EBITEMP_API_DATASOURCE } from '../database/connections/ebitemp-api/database.constants';
@Module({
imports: [
TypeOrmModule.forFeature([AccountsEntity]),
TypeOrmModule.forFeature([AccountsEntity], EBITEMP_API_DATASOURCE),
JwtAccessTokenModule,
JwtRefreshTokenModule,
PassportModule,

View File

@ -1,8 +1,10 @@
import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import dayjs from 'dayjs';
import { isEmpty } from 'lodash';
import { AccountsEntity } from '../database/connections/ebitemp-api/entities';
import { TokenPayload } from './constants/token-payload.interface';
import { ACCESS_TOKEN_JWT_SERVICE } from './strategies/jwt/jwt-access-token.module';
import { REFRESH_TOKEN_JWT_SERVICE } from './strategies/jwt/jwt-refresh-token.module';
import { UsersAuthService } from './users/users-auth.service';
@ -17,15 +19,31 @@ export class AuthService {
@Inject(REFRESH_TOKEN_JWT_SERVICE) private readonly refreshTokenJwtService: JwtService
) {}
public async signJwts(user: AccountsEntity) {
const payload = { username: user.username, sub: user.id };
public async signJwts(account: AccountsEntity) {
const { token: accessToken, exp: accessTokenExp } = await this.getAccessToken(account);
const { token: refreshToken, exp: refreshTokenExp } = await this.getRefreshToken(account);
const accessToken = await this.accessTokenJwtService.signAsync(payload);
return {
accessToken: accessToken,
accessTokenExp: dayjs.unix(accessTokenExp).toJSON(),
refreshToken: refreshToken,
refreshTokenExp: dayjs.unix(refreshTokenExp).toJSON(),
};
}
const refreshToken = await this.refreshTokenJwtService.signAsync(payload);
await this.usersAuthService.setCurrentRefreshTokenHash(user.id, refreshToken);
public async getAccessToken(account: AccountsEntity, options?: JwtSignOptions) {
const payload: TokenPayload = { username: account.username, sub: account.id };
const token = await this.accessTokenJwtService.signAsync(payload, options);
const exp = this.accessTokenJwtService.decode(token).exp;
return { token, exp };
}
return { accessToken, refreshToken };
public async getRefreshToken(account: AccountsEntity, options?: JwtSignOptions) {
const payload: TokenPayload = { username: account.username, sub: account.id };
const token = await this.refreshTokenJwtService.signAsync(payload, options);
await this.usersAuthService.setCurrentRefreshTokenHash(account.id, token);
const exp = this.refreshTokenJwtService.decode(token).exp;
return { token, exp };
}
public async getAuthenticatedUser(username: string, password: string): Promise<AccountsEntity> {

View File

@ -1,4 +1,5 @@
export interface TokenPayload {
username: string;
sub: number;
iat?: number;
exp?: number;

View File

@ -1,14 +1,20 @@
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { FastifyRequest } from 'fastify';
import { ClsService } from 'nestjs-cls';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AppClsStore } from '../../../cls/cls.interface';
import { AuthConfig, authConfig } from '../../auth.config';
import { TokenPayload } from '../../constants/token-payload.interface';
import { UsersAuthService } from '../../users/users-auth.service';
import { FastifyRequest } from 'fastify';
@Injectable()
export class JwtAccessTokenAuthStrategy extends PassportStrategy(Strategy, 'jwt-access-token') {
constructor(@Inject(authConfig.KEY) authConfig: AuthConfig, private readonly userAuthService: UsersAuthService) {
constructor(
@Inject(authConfig.KEY) authConfig: AuthConfig,
private readonly cls: ClsService<AppClsStore>,
private readonly userAuthService: UsersAuthService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: authConfig.accessToken.secret,
@ -19,6 +25,7 @@ export class JwtAccessTokenAuthStrategy extends PassportStrategy(Strategy, 'jwt-
async validate(request: FastifyRequest, payload: TokenPayload) {
const account = await this.userAuthService.getUserById(payload.sub);
if (!account) throw new UnauthorizedException('Access Token Guard');
this.cls.set('account', account);
return account;
}
}

View File

@ -4,8 +4,11 @@ import { AuthGuard } from '@nestjs/passport';
import { OpenAPIObject } from '@nestjs/swagger';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () =>
applyDecorators(SetMetadata(IS_PUBLIC_KEY, true), SetMetadata('swagger/apiSecurity', ['public']));
export const Public = (keepSecurity: boolean = false) => {
const decorators = [SetMetadata(IS_PUBLIC_KEY, true)];
if (!keepSecurity) decorators.push(SetMetadata('swagger/apiSecurity', ['public']));
return applyDecorators(...decorators);
};
export const patchPublicDecoratorSupport = (document: OpenAPIObject) => {
Object.values(document.paths).forEach((path: any) => {
@ -18,7 +21,7 @@ export const patchPublicDecoratorSupport = (document: OpenAPIObject) => {
return document;
};
export class JwtAuthGuard extends AuthGuard(['jwt-access-token', 'jwt-refresh-token']) {
export class JwtAuthGuard extends AuthGuard('jwt-access-token') {
constructor(private reflector: Reflector) {
super();
}

View File

@ -1,14 +1,20 @@
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { FastifyRequest } from 'fastify';
import { ClsService } from 'nestjs-cls';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AppClsStore } from '../../../cls/cls.interface';
import { authConfig, AuthConfig } from '../../auth.config';
import { TokenPayload } from '../../constants/token-payload.interface';
import { UsersAuthService } from '../../users/users-auth.service';
@Injectable()
export class JwtRefreshTokenAuthStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') {
constructor(@Inject(authConfig.KEY) authConfig: AuthConfig, private readonly usersAuthService: UsersAuthService) {
constructor(
@Inject(authConfig.KEY) authConfig: AuthConfig,
private readonly cls: ClsService<AppClsStore>,
private readonly usersAuthService: UsersAuthService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: authConfig.refreshToken.secret,
@ -17,9 +23,10 @@ export class JwtRefreshTokenAuthStrategy extends PassportStrategy(Strategy, 'jwt
}
async validate(request: FastifyRequest, payload: TokenPayload) {
const refreshToken = (request.headers?.authorization as string| undefined)?.replace('Bearer', '')?.trim() ?? '';
const refreshToken = (request.headers?.authorization as string | undefined)?.replace('Bearer', '')?.trim() ?? '';
const account = await this.usersAuthService.getUserByIdAndRefreshTokenPair(payload.sub, refreshToken);
if (!account) throw new UnauthorizedException('Refresh Token Guard');
this.cls.set('account', account);
return account;
}
}

View File

@ -0,0 +1,3 @@
import { AuthGuard } from '@nestjs/passport';
export class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}

View File

@ -1,13 +1,14 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import bcrypt from 'bcrypt';
import * as argon2 from 'argon2';
import { Repository } from 'typeorm';
import { AccountsEntity } from '../../database/connections/ebitemp-api/entities';
import { EBITEMP_API_DATASOURCE } from '../../database/connections/ebitemp-api/database.constants';
@Injectable()
export class UsersAuthService {
constructor(
@InjectRepository(AccountsEntity)
@InjectRepository(AccountsEntity, EBITEMP_API_DATASOURCE)
private readonly accountsRepository: Repository<AccountsEntity>
) {}
@ -29,12 +30,12 @@ export class UsersAuthService {
const accountById = await this.getUserById(accountId);
if (!accountById?.ultimoHashRefreshToken) return null;
const isRefreshTokenMatching = await bcrypt.compare(refreshToken, accountById.ultimoHashRefreshToken);
const isRefreshTokenMatching = await argon2.verify(accountById.ultimoHashRefreshToken, refreshToken);
return isRefreshTokenMatching ? accountById : null;
}
async setCurrentRefreshTokenHash(accountId: number, refreshToken: string | null) {
const hash = refreshToken ? await bcrypt.hash(refreshToken, 10) : null;
const hash = refreshToken ? await argon2.hash(refreshToken) : null;
await this.accountsRepository.update(accountId, {
ultimoHashRefreshToken: hash,
});

View File

@ -3,9 +3,10 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersAuthService } from './users-auth.service';
import { AccountsEntity } from '../../database/connections/ebitemp-api/entities';
import { EBITEMP_API_DATASOURCE } from '../../database/connections/ebitemp-api/database.constants';
@Module({
imports: [TypeOrmModule.forFeature([AccountsEntity])],
imports: [TypeOrmModule.forFeature([AccountsEntity], EBITEMP_API_DATASOURCE)],
controllers: [],
providers: [UsersAuthService],
exports: [UsersAuthService],

View File

@ -0,0 +1,6 @@
import { ClsStore } from "nestjs-cls";
import { AccountsEntity } from "../database/connections/ebitemp-api";
export interface AppClsStore extends ClsStore {
account: AccountsEntity|null;
}

View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { ClsModule } from 'nestjs-cls';
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
},
}),
],
providers: [],
exports: [ClsModule],
})
export class AppClsModule {}

View File

@ -0,0 +1 @@
export const EBITEMP_API_DATASOURCE = "EbitempApi";

View File

@ -6,17 +6,20 @@ import { typeormTransactionalDataSourceFactory } from '../../utils/typeorm-data-
import { typeormEntitiesFromImport } from '../../utils/typeorm-import-entities';
import { typeormModuleOptionsFactory } from '../../utils/typeorm-module-options-factory';
import { databaseConfig } from './database.config';
import { EBITEMP_API_DATASOURCE } from './database.constants';
@Module({
imports: [
ConfigModule.forFeature(databaseConfig),
TypeOrmModule.forRootAsync({
imports: databaseConfig.asProvider().imports,
dataSourceFactory: typeormTransactionalDataSourceFactory(),
name: EBITEMP_API_DATASOURCE,
dataSourceFactory: typeormTransactionalDataSourceFactory(EBITEMP_API_DATASOURCE),
useFactory: async (dbConfig: DatabaseConfig) => {
const config = await typeormModuleOptionsFactory(
dbConfig,
await typeormEntitiesFromImport(await import('./index'))
await typeormEntitiesFromImport(await import('./index')),
EBITEMP_API_DATASOURCE
);
return config;
},

View File

@ -0,0 +1,25 @@
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ApiClientInvocazioniEntity } from './api_client_invocazioni.entity';
@Index('pk_api_client', ['id'], { unique: true })
@Entity('api_client')
export class ApiClientEntity {
@PrimaryGeneratedColumn({ type: 'int', name: 'id' })
id!: number;
@Column('varchar', { name: 'nome', nullable: true, length: 255 })
nome!: string | null;
@Column('varchar', { name: 'descrizione', nullable: true, length: 255 })
descrizione!: string | null;
@Column('varchar', { name: 'base_url', length: 2000 })
baseUrl!: string;
@OneToMany(() => ApiClientInvocazioniEntity, (apiClientInvocazioniEntity) => apiClientInvocazioniEntity.client)
invocazioni!: ApiClientInvocazioniEntity[];
constructor(init?: Partial<ApiClientEntity>) {
Object.assign(this, init);
}
}

View File

@ -0,0 +1,41 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { ApiClientEntity } from './api_client.entity';
@Index('pk_api_client_invocazioni', ['id'], { unique: true })
@Entity('api_client_invocazioni')
export class ApiClientInvocazioniEntity {
@PrimaryGeneratedColumn({ type: 'int', name: 'id' })
id!: number;
@Column('int', { name: 'id_client' })
idClient!: number;
@Column('varchar', { name: 'percorso', length: 2000 })
percorso!: string;
@Column('varchar', { name: 'metodo', length: 7 })
metodo!: string;
@Column('datetime', { name: 'timestamp' })
timestamp!: Date;
@Column('varchar', { name: 'richiesta', nullable: true })
richiesta!: string | null;
@Column('varchar', { name: 'risposta' })
risposta!: string;
@Column('varchar', { name: 'errori', nullable: true })
errori!: string | null;
@Column('bit', { name: 'flag_errore' })
flagErrore!: boolean;
@ManyToOne(() => ApiClientEntity, (apiClientEntity) => apiClientEntity.invocazioni)
@JoinColumn([{ name: 'id_client', referencedColumnName: 'id' }])
client!: ApiClientEntity;
constructor(init?: Partial<ApiClientInvocazioniEntity>) {
Object.assign(this, init);
}
}

View File

@ -0,0 +1,25 @@
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ApiEndpointInvocazioniEntity } from './api_endpoint_invocazioni.entity';
@Index('pk_api_endpoint', ['id'], { unique: true })
@Entity('api_endpoint')
export class ApiEndpointEntity {
@PrimaryGeneratedColumn({ type: 'int', name: 'id' })
id!: number;
@Column('varchar', { name: 'percorso', length: 2000 })
percorso!: string;
@Column('varchar', { name: 'metodo', length: 7 })
metodo!: string;
@OneToMany(
() => ApiEndpointInvocazioniEntity,
(apiEndpointInvocazioniEntity) => apiEndpointInvocazioniEntity.endpoint
)
invocazioni!: ApiEndpointInvocazioniEntity[];
constructor(init?: Partial<ApiEndpointEntity>) {
Object.assign(this, init);
}
}

View File

@ -0,0 +1,40 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AccountsEntity } from './accounts.entity';
import { ApiEndpointEntity } from './api_endpoint.entity';
@Index('pk_api_endpoint_invocazioni', ['id'], { unique: true })
@Entity('api_endpoint_invocazioni')
export class ApiEndpointInvocazioniEntity {
@PrimaryGeneratedColumn({ type: 'int', name: 'id' })
id!: number;
@Column('int', { name: 'id_account' })
idAccount!: number;
@Column('int', { name: 'id_endpoint' })
idEndpoint!: number;
@Column('datetime', { name: 'timestamp' })
timestamp!: Date;
@Column('varchar', { name: 'richiesta' })
richiesta!: string;
@Column('varchar', { name: 'risposta' })
risposta!: string;
@Column('bit', { name: 'flag_errore' })
flagErrore!: boolean;
@ManyToOne(() => ApiEndpointEntity, (apiEndpointEntity) => apiEndpointEntity.invocazioni)
@JoinColumn([{ name: 'id_endpoint', referencedColumnName: 'id' }])
endpoint!: ApiEndpointEntity;
@ManyToOne(() => AccountsEntity)
@JoinColumn([{ name: 'id_account', referencedColumnName: 'id' }])
account!: AccountsEntity;
constructor(init?: Partial<ApiEndpointInvocazioniEntity>) {
Object.assign(this, init);
}
}

View File

@ -0,0 +1,8 @@
export * from './accounts.entity';
export * from './api_client.entity';
export * from './api_client_invocazioni.entity';
export * from './api_endpoint.entity';
export * from './api_endpoint_invocazioni.entity';
export * from './profili.entity';
export * from './ruoli.entity';
export * from './tipi_jobs.entity';

View File

@ -1,5 +1,5 @@
import { Column, Entity, Index } from 'typeorm';
import { Enumify } from '../../../../enumify/enumify';
import { Enum, Enumify } from '../../../../enumify/enumify';
export class InviaMailSeErrori {
@Column('bit', { name: 'flag', default: () => '(0)' })

View File

@ -0,0 +1 @@
export const OCEANO_DATASOURCE = "Oceano";

View File

@ -6,17 +6,20 @@ import { typeormTransactionalDataSourceFactory } from '../../utils/typeorm-data-
import { typeormEntitiesFromImport } from '../../utils/typeorm-import-entities';
import { typeormModuleOptionsFactory } from '../../utils/typeorm-module-options-factory';
import { databaseConfig } from './database.config';
import { OCEANO_DATASOURCE } from './database.constants';
@Module({
imports: [
ConfigModule.forFeature(databaseConfig),
TypeOrmModule.forRootAsync({
imports: databaseConfig.asProvider().imports,
dataSourceFactory: typeormTransactionalDataSourceFactory(),
name: OCEANO_DATASOURCE,
dataSourceFactory: typeormTransactionalDataSourceFactory(OCEANO_DATASOURCE),
useFactory: async (dbConfig: DatabaseConfig) => {
const config = await typeormModuleOptionsFactory(
dbConfig,
await typeormEntitiesFromImport(await import('./index'))
await typeormEntitiesFromImport(await import('./index')),
OCEANO_DATASOURCE
);
return config;
},

View File

@ -1,8 +1,8 @@
import { Column, Entity } from 'typeorm';
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('API_Luoghi_Estero')
export class ApiLuoghiEsteroEntity {
@Column('varchar', { name: 'codice', length: 5 })
@PrimaryColumn('varchar', { name: 'codice', length: 5 })
codice!: string;
@Column('nvarchar', { name: 'codice_stato', length: 5 })

View File

@ -1,8 +1,11 @@
import { Column, Entity, Index, OneToMany } from 'typeorm';
import { EnteEntity } from './ente.entity';
@Index('PK_gruppo', ['codiceGruppo'], { unique: true })
@Entity('gruppo')
export class GruppoEntity {
@Column('bigint', { primary: true, name: 'codiceGruppo' })
codiceGruppo!: string;

View File

@ -1,6 +1,7 @@
import { Global, Module } from '@nestjs/common';
import { APP_DATASOURCES } from './database.constants';
import { EbitempApiDatabaseModule } from './connections/ebitemp-api/database.module';
import { OceanoDatabaseModule } from './connections/oceano/database.module';
import { APP_DATASOURCES } from './database.constants';
import { dataSources } from './utils/typeorm-data-source-factory';
const dataSourcesProvider = {
@ -8,7 +9,7 @@ const dataSourcesProvider = {
useValue: dataSources,
};
const databaseModules = [EbitempApiDatabaseModule];
const databaseModules = [EbitempApiDatabaseModule, OceanoDatabaseModule];
@Global()
@Module({

View File

@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApiClientEntity, ApiClientInvocazioniEntity } from '../../database/connections/ebitemp-api';
import { EBITEMP_API_DATASOURCE } from '../../database/connections/ebitemp-api/database.constants';
import { KeyvModule } from '../../keyv/keyv.module';
import { ClientsService } from './clients.service';
@Global()
@Module({
imports: [
KeyvModule,
TypeOrmModule.forFeature([ApiClientEntity, ApiClientInvocazioniEntity], EBITEMP_API_DATASOURCE),
],
providers: [ClientsService],
exports: [ClientsService],
})
export class ClientsModule {}

View File

@ -0,0 +1,86 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isError } from '@stdlib/assert';
import AsyncLock from 'async-lock';
import { Keyv } from 'cacheable';
import { isObject } from 'lodash';
import { Repository } from 'typeorm';
import { runInTransaction } from 'typeorm-transactional';
import { ApiClientEntity, ApiClientInvocazioniEntity } from '../../database/connections/ebitemp-api';
import { EBITEMP_API_DATASOURCE } from '../../database/connections/ebitemp-api/database.constants';
import { KeyvService } from '../../keyv/keyv.service';
@Injectable()
export class ClientsService {
private readonly logger = new Logger(ClientsService.name);
private readonly lock = new AsyncLock();
private readonly keyv: Keyv;
constructor(
keyvService: KeyvService,
@InjectRepository(ApiClientEntity, EBITEMP_API_DATASOURCE)
private readonly clientRepository: Repository<ApiClientEntity>,
@InjectRepository(ApiClientInvocazioniEntity, EBITEMP_API_DATASOURCE)
private readonly clientInvocazioniRepository: Repository<ApiClientInvocazioniEntity>
) {
this.keyv = keyvService.create({
namespace: ClientsService.name,
});
}
async insertOne(
baseUrl: string,
path: string,
method: string,
requestBody: any,
responseBody: any,
hasFailed: boolean,
errors: string | null
) {
try {
return await runInTransaction(
async () => {
const client = await this.getClient(baseUrl);
if (!client) return;
const payload = JSON.stringify(requestBody);
const response = isObject(responseBody) ? JSON.stringify(responseBody) : responseBody;
const invocazione = this.clientInvocazioniRepository.create({
idClient: client.id,
percorso: path,
metodo: method,
timestamp: new Date(),
richiesta: payload,
risposta: response,
errori: errors,
flagErrore: hasFailed,
});
await this.clientInvocazioniRepository.insert(invocazione);
return invocazione;
},
{ connectionName: EBITEMP_API_DATASOURCE }
);
} catch (err) {
if (!isError(err)) throw err;
this.logger.error(`Unexpected error: ${err.message}`);
throw err;
}
}
private async getClient(baseUrl: string) {
const key = `client:baseUrl#${baseUrl}`;
return await this.lock.acquire(key, async () => {
let client = await this.keyv.get<ApiClientEntity>(key);
const isInCache = client != null;
if (!isInCache) {
client ??= (await this.clientRepository.findOne({ where: { baseUrl: baseUrl } })) ?? undefined;
if (client) this.keyv.set(key, client);
}
return client;
});
}
}

View File

@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApiEndpointEntity, ApiEndpointInvocazioniEntity } from '../../database/connections/ebitemp-api';
import { EBITEMP_API_DATASOURCE } from '../../database/connections/ebitemp-api/database.constants';
import { KeyvModule } from '../../keyv/keyv.module';
import { EndpointsService } from './endpoints.service';
@Global()
@Module({
imports: [
KeyvModule,
TypeOrmModule.forFeature([ApiEndpointEntity, ApiEndpointInvocazioniEntity], EBITEMP_API_DATASOURCE),
],
providers: [EndpointsService],
exports: [EndpointsService],
})
export class EndpointsModule {}

View File

@ -0,0 +1,84 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import AsyncLock from 'async-lock';
import { Keyv } from 'cacheable';
import { Repository } from 'typeorm';
import { runInTransaction } from 'typeorm-transactional';
import { AccountsEntity, ApiEndpointEntity, ApiEndpointInvocazioniEntity } from '../../database/connections/ebitemp-api';
import { EBITEMP_API_DATASOURCE } from '../../database/connections/ebitemp-api/database.constants';
import { KeyvService } from '../../keyv/keyv.service';
import { RequestLog } from '../request-logger.interceptor';
import { isError } from '@stdlib/assert';
@Injectable()
export class EndpointsService {
private readonly logger = new Logger(EndpointsService.name);
private readonly lock = new AsyncLock();
private readonly keyv: Keyv;
constructor(
keyvService: KeyvService,
@InjectRepository(ApiEndpointEntity, EBITEMP_API_DATASOURCE)
private readonly endpointRepository: Repository<ApiEndpointEntity>,
@InjectRepository(ApiEndpointInvocazioniEntity, EBITEMP_API_DATASOURCE)
private readonly endpointInvocazioniRepository: Repository<ApiEndpointInvocazioniEntity>
) {
this.keyv = keyvService.create({
namespace: EndpointsService.name,
});
}
async insertOne(
user: AccountsEntity,
route: string,
method: string,
requestLog: RequestLog,
responseLog: any,
hasFailed: boolean
) {
try {
return await runInTransaction(
async () => {
const endpoint = await this.getEndpoint(route, method);
if (!endpoint) return;
const payload = JSON.stringify(requestLog);
const response = JSON.stringify(responseLog);
const invocazione = this.endpointInvocazioniRepository.create({
idAccount: user.id,
idEndpoint: endpoint.id,
timestamp: new Date(),
richiesta: payload,
risposta: response,
flagErrore: hasFailed,
});
await this.endpointInvocazioniRepository.insert(invocazione);
return invocazione;
},
{ connectionName: EBITEMP_API_DATASOURCE }
);
} catch (err) {
if (!isError(err)) throw err;
this.logger.error(`Unexpected error: ${err.message}`);
throw err;
}
}
private async getEndpoint(route: string, method: string) {
const key = `endpoint:route,method#${route},${method}`;
return await this.lock.acquire(key, async () => {
let endpoint = await this.keyv.get<ApiEndpointEntity>(key);
const isInCache = endpoint != null;
if (!isInCache) {
endpoint ??=
(await this.endpointRepository.findOne({ where: { percorso: route, metodo: method } })) ?? undefined;
if (endpoint) this.keyv.set(key, endpoint);
}
return endpoint;
});
}
}

View File

@ -0,0 +1,67 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { omit } from 'lodash';
import { Observable } from 'rxjs';
import { catchError, concatMap } from 'rxjs/operators';
import { EndpointsService } from './endpoints/endpoints.service';
import { RequestWithUser } from '../auth/constants/request-with-user';
export type RequestLog = {
url: string;
idAccount: string;
params: object;
query: object;
body: object;
};
@Injectable()
export class RequestLoggerInterceptor implements NestInterceptor {
private readonly logger = new Logger(RequestLoggerInterceptor.name);
constructor(private readonly endpointsService: EndpointsService) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request: RequestWithUser = context.switchToHttp().getRequest();
const user = request.user;
const route = request.routeOptions.url ?? '/';
const method = request.method as string;
request.params = { ...(request.params as object ?? {}), route, method };
const requestLog = {
url: request.url,
idAccount: `${user.id}`,
params: omit((request.params as object), ['user', 'route', 'method']),
query: omit((request.query as object), ['__context']),
body: request.body as object,
};
return next.handle().pipe(
concatMap(async (res: any) => {
await this.endpointsService.insertOne(user, route, method, requestLog, res, false);
return res;
}),
catchError(async (err: any) => {
const getExceptionResponse = (exception: any): any => {
if (typeof exception.response === 'object') {
const { error, message, ...rest } = exception.response;
return {
error,
message,
...rest,
};
}
return {
error: exception.name,
message: exception.message,
};
};
const res = getExceptionResponse(err);
await this.endpointsService.insertOne(user, route, method, requestLog, res, true);
throw err;
})
);
}
}

View File

@ -0,0 +1,13 @@
import { Global, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ClientsModule } from './clients/clients.module';
import { EndpointsModule } from './endpoints/endpoints.module';
import { RequestLoggerInterceptor } from './request-logger.interceptor';
@Global()
@Module({
imports: [EndpointsModule, ClientsModule],
providers: [],
exports: [EndpointsModule, ClientsModule],
})
export class AppRequestLoggingModule {}

View File

@ -3,9 +3,10 @@ import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TipiJobsEntity } from '../database/connections/ebitemp-api/entities';
import { SchedulerService } from './schedule.service';
import { EBITEMP_API_DATASOURCE } from '../database/connections/ebitemp-api/database.constants';
@Module({
imports: [TypeOrmModule.forFeature([TipiJobsEntity]), ScheduleModule.forRoot()],
imports: [TypeOrmModule.forFeature([TipiJobsEntity], EBITEMP_API_DATASOURCE), ScheduleModule.forRoot()],
providers: [SchedulerService],
exports: [],
})

View File

@ -1,10 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { isError } from '@stdlib/assert';
import { CronJob, CronTime } from 'cron';
import { Repository } from 'typeorm';
import { EBITEMP_API_DATASOURCE } from '../database/connections/ebitemp-api/database.constants';
import { TipiJobsEntity } from '../database/connections/ebitemp-api/entities/tipi_jobs.entity';
import { isError } from '@stdlib/assert';
@Injectable()
export class SchedulerService {
@ -14,7 +15,8 @@ export class SchedulerService {
constructor(
private readonly schedulerRegistry: SchedulerRegistry,
@InjectRepository(TipiJobsEntity) private readonly tipiJobsRepository: Repository<TipiJobsEntity>
@InjectRepository(TipiJobsEntity, EBITEMP_API_DATASOURCE)
private readonly tipiJobsRepository: Repository<TipiJobsEntity>
) {}
async onApplicationBootstrap() {

View File

@ -41,6 +41,7 @@ export class ZodSerializerInterceptor implements NestInterceptor {
map((res: object | object[]) => {
if (!responseSchema) return res;
if (typeof res !== 'object' || res instanceof StreamableFile) return res;
if (!('safeParse' in responseSchema)) return res;
return Array.isArray(res)
? res.map(

View File

@ -30,6 +30,8 @@
"@nestjs/typeorm": "^11.0.0",
"@nx/devkit": "20.6.0",
"@stdlib/assert": "^0.3.3",
"argon2": "^0.41.1",
"async-lock": "^1.4.1",
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"cacheable": "^1.8.8",
@ -39,6 +41,8 @@
"fastify": "^5.2.1",
"flatted": "^3.3.2",
"lodash": "^4.17.21",
"nestjs-cls": "^5.4.1",
"nestjs-paginate": "^11.1.1",
"nestjs-pino": "^4.3.0",
"openapi3-ts": "^4.4.0",
"passport-jwt": "^4.0.1",
@ -73,6 +77,7 @@
"@swc/cli": "~0.3.12",
"@swc/core": "~1.10.15",
"@swc/helpers": "~0.5.11",
"@types/async-lock": "^1.4.2",
"@types/bcrypt": "^5.0.2",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.15",
@ -83,6 +88,7 @@
"execa": "5.1.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"jsonc-eslint-parser": "^2.1.0",
"mssql": "^11.0.1",
"nx": "20.6.0",
"prettier": "^2.6.2",

90
pnpm-lock.yaml generated
View File

@ -65,6 +65,12 @@ importers:
'@stdlib/assert':
specifier: ^0.3.3
version: 0.3.3
argon2:
specifier: ^0.41.1
version: 0.41.1
async-lock:
specifier: ^1.4.1
version: 1.4.1
axios:
specifier: ^1.7.9
version: 1.7.9
@ -92,6 +98,12 @@ importers:
lodash:
specifier: ^4.17.21
version: 4.17.21
nestjs-cls:
specifier: ^5.4.1
version: 5.4.1(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)
nestjs-paginate:
specifier: ^11.1.1
version: 11.1.1(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/swagger@11.0.3(@fastify/static@8.1.0)(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.8)(reflect-metadata@0.2.2))(express@5.0.1)(fastify@5.2.1)(typeorm@0.3.20(mssql@11.0.1)(ts-node@10.9.2(@swc/core@1.10.15(@swc/helpers@0.5.15))(@types/node@18.16.20)(typescript@5.7.3)))
nestjs-pino:
specifier: ^4.3.0
version: 4.3.0(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(pino-http@10.4.0)
@ -189,6 +201,9 @@ importers:
'@swc/helpers':
specifier: ~0.5.11
version: 0.5.15
'@types/async-lock':
specifier: ^1.4.2
version: 1.4.2
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
@ -219,6 +234,9 @@ importers:
jest-environment-node:
specifier: ^29.7.0
version: 29.7.0
jsonc-eslint-parser:
specifier: ^2.1.0
version: 2.4.0
mssql:
specifier: ^11.0.1
version: 11.0.1
@ -1798,6 +1816,10 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@phenomnomnominal/tsquery@5.0.1':
resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==}
peerDependencies:
@ -2154,6 +2176,9 @@ packages:
'@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
'@types/async-lock@1.4.2':
resolution: {integrity: sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -2590,6 +2615,10 @@ packages:
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
argon2@0.41.1:
resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==}
engines: {node: '>=16.17.0'}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@ -2613,6 +2642,9 @@ packages:
resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==}
engines: {node: ^4.7 || >=6.9 || >=7.3}
async-lock@1.4.1:
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
async@2.6.4:
resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==}
@ -4885,6 +4917,24 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
nestjs-cls@5.4.1:
resolution: {integrity: sha512-hm0CQE6hRjr07yqSTcj50eQwNuZhlOHocX1W0CHpsYo9aDqsEysGkshegvAlC/4NVedTeAU6J0dVjAvNPTMrjQ==}
engines: {node: '>=18'}
peerDependencies:
'@nestjs/common': '>= 10 < 12'
'@nestjs/core': '>= 10 < 12'
reflect-metadata: '*'
rxjs: '>= 7'
nestjs-paginate@11.1.1:
resolution: {integrity: sha512-PYNTxnQ3ZHGjXfOgqMNtEmMlad1o9M6RoiXPSQtCl3n8MDH+t2igXn9Z9Id6B5SfBnsrvRD9YEi2Zb1EU5g6Fg==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
'@nestjs/swagger': ^8.0.0 || ^11.0.0
express: ^4.21.2 || ^5.0.0
fastify: ^4.0.0 || ^5.0.0
typeorm: ^0.3.17
nestjs-pino@4.3.0:
resolution: {integrity: sha512-u/FRi+eRH+ER6cccyr9BBNaHg71qxLgWwDT7a0eGbSVZrSUzQ04O5zTNqydgzihNNHfuam6TBTz1wWdapOWYnQ==}
engines: {node: '>= 14'}
@ -4904,6 +4954,10 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-addon-api@8.3.1:
resolution: {integrity: sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==}
engines: {node: ^18 || ^20 || >= 21}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@ -4917,6 +4971,10 @@ packages:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@ -8996,6 +9054,8 @@ snapshots:
'@parcel/watcher-win32-x64': 2.5.1
optional: true
'@phc/format@1.0.0': {}
'@phenomnomnominal/tsquery@5.0.1(typescript@5.7.3)':
dependencies:
esquery: 1.6.0
@ -9600,6 +9660,8 @@ snapshots:
dependencies:
tslib: 2.8.1
'@types/async-lock@1.4.2': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.7
@ -10116,6 +10178,12 @@ snapshots:
arg@4.1.3: {}
argon2@0.41.1:
dependencies:
'@phc/format': 1.0.0
node-addon-api: 8.3.1
node-gyp-build: 4.8.4
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
@ -10134,6 +10202,8 @@ snapshots:
dependencies:
stack-chain: 1.3.7
async-lock@1.4.1: {}
async@2.6.4:
dependencies:
lodash: 4.17.21
@ -12776,6 +12846,22 @@ snapshots:
neo-async@2.6.2: {}
nestjs-cls@5.4.1(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.8)(reflect-metadata@0.2.2)(rxjs@7.8.1):
dependencies:
'@nestjs/common': 11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 11.0.8(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@11.0.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)
reflect-metadata: 0.2.2
rxjs: 7.8.1
nestjs-paginate@11.1.1(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/swagger@11.0.3(@fastify/static@8.1.0)(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.8)(reflect-metadata@0.2.2))(express@5.0.1)(fastify@5.2.1)(typeorm@0.3.20(mssql@11.0.1)(ts-node@10.9.2(@swc/core@1.10.15(@swc/helpers@0.5.15))(@types/node@18.16.20)(typescript@5.7.3))):
dependencies:
'@nestjs/common': 11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/swagger': 11.0.3(@fastify/static@8.1.0)(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.8)(reflect-metadata@0.2.2)
express: 5.0.1
fastify: 5.2.1
lodash: 4.17.21
typeorm: 0.3.20(mssql@11.0.1)(ts-node@10.9.2(@swc/core@1.10.15(@swc/helpers@0.5.15))(@types/node@18.16.20)(typescript@5.7.3))
nestjs-pino@4.3.0(@nestjs/common@11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(pino-http@10.4.0):
dependencies:
'@nestjs/common': 11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1)
@ -12793,12 +12879,16 @@ snapshots:
node-addon-api@7.1.1:
optional: true
node-addon-api@8.3.1: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-forge@1.3.1: {}
node-gyp-build@4.8.4: {}
node-int64@0.4.0: {}
node-machine-id@1.1.12: {}

View File

@ -24,7 +24,7 @@
"alias": "dir",
"x-prompt": "Which directory do you want to create the entities in?",
"default": "typeorm-model-generator-output",
"x-priority": "important"
"x-priority": "important"
},
"driver": {
"description": "The database driver that should be used by the generator.",