chore(repo) init Enumify module + ref. AppConfigModule
This commit is contained in:
parent
3042e693be
commit
ef50d7075f
@ -4,9 +4,10 @@ import { AppService } from './app.service';
|
|||||||
|
|
||||||
import { AppConfigModule } from './modules/config/config.module';
|
import { AppConfigModule } from './modules/config/config.module';
|
||||||
import { AppDatabaseModule } from './modules/database/database.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 { AppFileTransactionsModule } from './modules/file-transactions/file-transactions.module';
|
||||||
import { AppLoggerModule } from './modules/logger/logger.module';
|
|
||||||
import { AppHealthModule } from './modules/health/health.module';
|
import { AppHealthModule } from './modules/health/health.module';
|
||||||
|
import { AppLoggerModule } from './modules/logger/logger.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -15,6 +16,7 @@ import { AppHealthModule } from './modules/health/health.module';
|
|||||||
AppDatabaseModule,
|
AppDatabaseModule,
|
||||||
AppHealthModule,
|
AppHealthModule,
|
||||||
AppFileTransactionsModule,
|
AppFileTransactionsModule,
|
||||||
|
EnumifyModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { databaseConfig } from '../database/database.config';
|
|
||||||
import { keyvConfig } from '../keyv/keyv.config';
|
|
||||||
import { loggerConfig } from '../logger/logger.config';
|
|
||||||
import { localConfig } from './local.config';
|
import { localConfig } from './local.config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
expandVariables: true,
|
load: [localConfig],
|
||||||
load: [localConfig, loggerConfig, databaseConfig, keyvConfig],
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { APP_DATASOURCES } from './database.constants';
|
|
||||||
import { DatabaseConfig, databaseConfig } from './database.config';
|
import { DatabaseConfig, databaseConfig } from './database.config';
|
||||||
|
import { APP_DATASOURCES } from './database.constants';
|
||||||
import { typeormTransactionalDataSourceFactory } from './utils/typeorm-data-source-factory';
|
import { typeormTransactionalDataSourceFactory } from './utils/typeorm-data-source-factory';
|
||||||
import { typeormModuleOptionsFactory } from './utils/typeorm-module-options-factory';
|
|
||||||
import { typeormEntitiesFromImport } from './utils/typeorm-import-entities';
|
import { typeormEntitiesFromImport } from './utils/typeorm-import-entities';
|
||||||
|
import { typeormModuleOptionsFactory } from './utils/typeorm-module-options-factory';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
const dataSources: DataSource[] = [];
|
const dataSources: DataSource[] = [];
|
||||||
|
|
||||||
const typeormModules = [
|
const typeormModules = [
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
dataSourceFactory: typeormTransactionalDataSourceFactory(dataSources),
|
imports: databaseConfig.asProvider().imports,
|
||||||
|
dataSourceFactory: typeormTransactionalDataSourceFactory(),
|
||||||
useFactory: async (dbConfig: DatabaseConfig) => {
|
useFactory: async (dbConfig: DatabaseConfig) => {
|
||||||
// const entities = await import('./path/to/entities');
|
// const entities = await import('./path/to/entities');
|
||||||
const entities = {};
|
const entities = {};
|
||||||
|
|
||||||
const config = await typeormModuleOptionsFactory(dbConfig, [
|
const config = await typeormModuleOptionsFactory(dbConfig, [typeormEntitiesFromImport(entities)]);
|
||||||
typeormEntitiesFromImport(entities),
|
|
||||||
]);
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
inject: [databaseConfig.KEY],
|
inject: [databaseConfig.KEY],
|
||||||
@ -32,7 +32,7 @@ const dataSourcesProvider = {
|
|||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [...typeormModules],
|
imports: [ConfigModule.forFeature(databaseConfig), ...typeormModules],
|
||||||
providers: [dataSourcesProvider],
|
providers: [dataSourcesProvider],
|
||||||
exports: [...typeormModules, dataSourcesProvider],
|
exports: [...typeormModules, dataSourcesProvider],
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
|
|
||||||
|
export const dataSources: DataSource[] = [];
|
||||||
|
|
||||||
export const typeormDataSourceFactory = (dataSources: DataSource[]) => async (options?: DataSourceOptions) => {
|
export const typeormDataSourceFactory = (dataSources: DataSource[]) => async (options?: DataSourceOptions) => {
|
||||||
const dataSource = await new DataSource(options!).initialize();
|
const dataSource = await new DataSource(options!).initialize();
|
||||||
dataSources.push(dataSource);
|
dataSources.push(dataSource);
|
||||||
@ -7,7 +9,7 @@ export const typeormDataSourceFactory = (dataSources: DataSource[]) => async (op
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const typeormTransactionalDataSourceFactory =
|
export const typeormTransactionalDataSourceFactory =
|
||||||
(dataSources: DataSource[], name?: string) => async (options?: DataSourceOptions) => {
|
(name?: string) => async (options?: DataSourceOptions) => {
|
||||||
const tt = await import('typeorm-transactional');
|
const tt = await import('typeorm-transactional');
|
||||||
const dataSource = tt.addTransactionalDataSource({ name: name, dataSource: new DataSource(options!) });
|
const dataSource = tt.addTransactionalDataSource({ name: name, dataSource: new DataSource(options!) });
|
||||||
dataSources.push(dataSource);
|
dataSources.push(dataSource);
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
|
import { differenceBy, get } from 'lodash';
|
||||||
|
import { DataSource, QueryFailedError } from 'typeorm';
|
||||||
|
import { APP_DATASOURCES } from '../database/database.constants';
|
||||||
|
import { Enumify, registeredEnums } from './enumify';
|
||||||
|
import { EnumifyConfig, enumifyConfig } from './enumify.config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EnumifyUpdateFromDatabaseService implements OnApplicationBootstrap {
|
||||||
|
constructor(
|
||||||
|
@Inject(APP_DATASOURCES) private readonly datasources: DataSource[],
|
||||||
|
@Inject(enumifyConfig.KEY) private readonly config: EnumifyConfig
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
try {
|
||||||
|
const enumUpdateFromDbOnStart = this.config.shouldUpdateEnumFromDbOnStartup;
|
||||||
|
if (!enumUpdateFromDbOnStart) return;
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const dataSource of this.datasources) {
|
||||||
|
const logging = dataSource.options.logging;
|
||||||
|
dataSource.setOptions({ logging: false });
|
||||||
|
for (const _registeredEnum of get(registeredEnums, dataSource.name, [])) {
|
||||||
|
const registeredEnum = _registeredEnum as typeof Enumify;
|
||||||
|
|
||||||
|
const repo = dataSource.getRepository(registeredEnum);
|
||||||
|
|
||||||
|
const enumValues = registeredEnum.enumValues;
|
||||||
|
let dbValues: Enumify[];
|
||||||
|
try {
|
||||||
|
dbValues = await repo.find();
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof QueryFailedError &&
|
||||||
|
err.message === `Error: Invalid object name '${repo.metadata.tableName}'.`
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`[${dataSource.name}] ${registeredEnum.name}: Table present in code but missing on database: ${enumValues}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (err instanceof QueryFailedError && err.message.startsWith('Error: Invalid column name')) {
|
||||||
|
errors.push(`[${dataSource.name}] ${registeredEnum.name}: [${repo.metadata.tableName}] ${err.message}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const differenceByDbValues = differenceBy(dbValues, enumValues, (x) => (x.id, x.nome));
|
||||||
|
const differenceByEnumValues = differenceBy(enumValues, dbValues, (x) => (x.id, x.nome));
|
||||||
|
if (differenceByDbValues.length > 0) {
|
||||||
|
errors.push(
|
||||||
|
`[${dataSource.name}] ${
|
||||||
|
registeredEnum.name
|
||||||
|
}: Values present on database but missing in code: ${differenceBy(
|
||||||
|
dbValues,
|
||||||
|
enumValues,
|
||||||
|
(x) => (x.id, x.nome)
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (differenceByEnumValues.length > 0) {
|
||||||
|
errors.push(
|
||||||
|
`[${dataSource.name}] ${
|
||||||
|
registeredEnum.name
|
||||||
|
}: Values present in code but missing in database: ${differenceBy(
|
||||||
|
enumValues,
|
||||||
|
dbValues,
|
||||||
|
(x) => (x.id, x.nome)
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dbValue of dbValues) {
|
||||||
|
const valueOfByName = registeredEnum.fromKey(dbValue.nome!);
|
||||||
|
const keyOfByCode = registeredEnum.fromValue(dbValue.id!);
|
||||||
|
if (valueOfByName != null && dbValue.id != valueOfByName?.id) {
|
||||||
|
errors.push(
|
||||||
|
`[${dataSource.name}] ${registeredEnum.name}: Different values between database (${dbValue.id}, ${dbValue.nome}) and code (${valueOfByName.id}, ${valueOfByName.nome})`
|
||||||
|
);
|
||||||
|
} else if (keyOfByCode != null && dbValue.nome != keyOfByCode?.nome) {
|
||||||
|
errors.push(
|
||||||
|
`[${dataSource.name}] ${registeredEnum.name}: Different values between database (${dbValue.id}, ${dbValue.nome}) and code (${keyOfByCode.id}, ${keyOfByCode.nome})`
|
||||||
|
);
|
||||||
|
} else if (valueOfByName != null || keyOfByCode != null) {
|
||||||
|
const enumValue = (valueOfByName ?? keyOfByCode)!;
|
||||||
|
Object.assign(enumValue, dbValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.setOptions({ logging: logging });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.join('\n\t* '));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
console.warn(err.message, EnumifyUpdateFromDatabaseService.name);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
apps/ebitemp-api/src/app/modules/enumify/enumify.config.ts
Normal file
17
apps/ebitemp-api/src/app/modules/enumify/enumify.config.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import coerceRecordTypes from '../config/utils/coerce-record-types';
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const enumifySchema = z.object({
|
||||||
|
shouldUpdateEnumFromDbOnStartup: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
export type EnumifyConfig = z.infer<typeof enumifySchema>;
|
||||||
|
|
||||||
|
export const enumifyConfig = registerAs('enumify', () => {
|
||||||
|
const env = coerceRecordTypes(process.env);
|
||||||
|
|
||||||
|
const config: EnumifyConfig = enumifySchema.strict().parse({
|
||||||
|
shouldUpdateEnumFromDbOnStartup: env['ENUM_UPDATE_FROM_DB_ON_START']
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
});
|
12
apps/ebitemp-api/src/app/modules/enumify/enumify.module.ts
Normal file
12
apps/ebitemp-api/src/app/modules/enumify/enumify.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module, Global } from '@nestjs/common';
|
||||||
|
import { EnumifyUpdateFromDatabaseService } from './enumify-update-from-database.service';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { enumifyConfig } from './enumify.config';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule.forFeature(enumifyConfig)],
|
||||||
|
providers: [EnumifyUpdateFromDatabaseService],
|
||||||
|
exports: [EnumifyUpdateFromDatabaseService],
|
||||||
|
})
|
||||||
|
export class EnumifyModule {}
|
74
apps/ebitemp-api/src/app/modules/enumify/enumify.ts
Normal file
74
apps/ebitemp-api/src/app/modules/enumify/enumify.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// Based on https://github.com/rauschma/enumify
|
||||||
|
// Original license: MIT License - Copyright (c) 2020 Axel Rauschmayer
|
||||||
|
|
||||||
|
export const registeredEnums: Record<string, typeof Enumify[]> = {};
|
||||||
|
export function Enum(schema: string) {
|
||||||
|
return function (target: typeof Enumify) {
|
||||||
|
registeredEnums[schema] ??= [];
|
||||||
|
registeredEnums[schema].push(target);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Enumify {
|
||||||
|
static enumKeys: Array<string>;
|
||||||
|
static enumValues: Array<Enumify>;
|
||||||
|
|
||||||
|
public readonly id?: number;
|
||||||
|
public readonly nome?: string;
|
||||||
|
|
||||||
|
static closeEnum() {
|
||||||
|
const enumKeys: Array<string> = [];
|
||||||
|
const enumValues: Array<Enumify> = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(this)) {
|
||||||
|
value.nome ??= key;
|
||||||
|
value.id ??= enumValues.length;
|
||||||
|
|
||||||
|
if (value.id == null || value.id === '') {
|
||||||
|
throw new Error(`${this.name}.id`);
|
||||||
|
}
|
||||||
|
if (value.nome == null || value.nome === '') {
|
||||||
|
throw new Error(`${this.name}.nome`);
|
||||||
|
}
|
||||||
|
|
||||||
|
enumKeys.push(value.nome);
|
||||||
|
enumValues.push(value);
|
||||||
|
}
|
||||||
|
this.enumKeys = enumKeys;
|
||||||
|
this.enumValues = enumValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromKey(key: string): undefined | Enumify {
|
||||||
|
if (this.enumKeys == undefined) {
|
||||||
|
throw new Error(`Did you forget to call static _ = ${this.name}.closeEnum() after setup?`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.enumKeys.findIndex((enumKey) => enumKey.toUpperCase() === key.toString().toUpperCase());
|
||||||
|
if (index >= 0) {
|
||||||
|
return this.enumValues[index];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromValue(value: number): undefined | Enumify {
|
||||||
|
if (this.enumValues == undefined) {
|
||||||
|
throw new Error(`Did you forget to call static _ = ${this.name}.closeEnum() after setup?`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.enumValues.map((x) => x.id).indexOf(value);
|
||||||
|
if (index >= 0) {
|
||||||
|
const key = this.enumKeys[index];
|
||||||
|
return this.fromKey(key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected constructor(id?: number, nome?: string) {
|
||||||
|
this.id = id;
|
||||||
|
this.nome = nome;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `(${this.id}, ${this.nome})`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { FileTransactionsService } from './file-transactions.service';
|
|
||||||
import { KeyvModule } from '../keyv/keyv.module';
|
import { KeyvModule } from '../keyv/keyv.module';
|
||||||
|
import { FileTransactionsService } from './file-transactions.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { keyvConfig } from './keyv.config';
|
||||||
import { KeyvService } from './keyv.service';
|
import { KeyvService } from './keyv.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [ConfigModule.forFeature(keyvConfig)],
|
||||||
providers: [KeyvService],
|
providers: [KeyvService],
|
||||||
exports: [KeyvService],
|
exports: [KeyvService],
|
||||||
})
|
})
|
||||||
|
@ -2,10 +2,13 @@ import { Module } from '@nestjs/common';
|
|||||||
import { NestApplication } from '@nestjs/core';
|
import { NestApplication } from '@nestjs/core';
|
||||||
import { LoggerModule } from 'nestjs-pino';
|
import { LoggerModule } from 'nestjs-pino';
|
||||||
import { LoggerConfig, loggerConfig } from './logger.config';
|
import { LoggerConfig, loggerConfig } from './logger.config';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigModule.forFeature(loggerConfig),
|
||||||
LoggerModule.forRootAsync({
|
LoggerModule.forRootAsync({
|
||||||
|
imports: loggerConfig.asProvider().imports,
|
||||||
useFactory: (loggerConfig: LoggerConfig) => ({
|
useFactory: (loggerConfig: LoggerConfig) => ({
|
||||||
pinoHttp: {
|
pinoHttp: {
|
||||||
autoLogging: true,
|
autoLogging: true,
|
||||||
|
Reference in New Issue
Block a user