chore(repo) init AppDatabase module

This commit is contained in:
Francesco Spilla 2025-02-07 17:16:38 +01:00
parent 9f35377481
commit 0d60b73816
12 changed files with 367 additions and 112 deletions

View File

@ -1,11 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppConfigModule } from './modules/config/config.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { AppConfigModule } from './modules/config/config.module';
import { AppDatabaseModule } from './modules/database/database.module';
import { AppLoggerModule } from './modules/logger/logger.module'; import { AppLoggerModule } from './modules/logger/logger.module';
@Module({ @Module({
imports: [AppConfigModule, AppLoggerModule], imports: [AppConfigModule, AppLoggerModule, AppDatabaseModule],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })

View File

@ -0,0 +1,63 @@
import { registerAs } from '@nestjs/config';
import { ConnectionString } from 'connection-string';
import { first } from 'lodash';
import { z } from 'zod';
import coerceRecordTypes from '../modules/config/coerce-record-types';
export const databaseSchema = z.object({
connectionString: z.string(),
type: z.enum([
'mysql',
'postgres',
'cockroachdb',
'sap',
'mariadb',
'sqlite',
'cordova',
'react-native',
'nativescript',
'sqljs',
'oracle',
'mssql',
'mongodb',
'aurora-mysql',
'aurora-postgres',
'expo',
'better-sqlite3',
'capacitor',
'spanner',
]),
host: z.string(),
port: z.number().optional(),
username: z.string(),
password: z.string(),
database: z.string(),
secure: z.boolean(),
});
export type DatabaseConfig = z.TypeOf<typeof databaseSchema>;
const rawDatabaseSchema = z.object({
connectionString: z.string(),
secure: z.boolean().default(true),
});
export const databaseConfig = registerAs('database', () => {
const env = coerceRecordTypes(process.env);
const envParsed = rawDatabaseSchema.strict().parse({
connectionString: env['DATABASE_CONNECTION_STRING'],
secure: env['DATABASE_SECURE'],
});
const connectionString = new ConnectionString(envParsed.connectionString);
const config: DatabaseConfig = databaseSchema.strict().parse({
connectionString: connectionString.toString(),
type: connectionString.protocol,
host: first(connectionString.hosts)?.name,
port: first(connectionString.hosts)?.port,
username: connectionString.user,
password: connectionString.password,
database: first(connectionString.path),
secure: envParsed.secure,
});
return config;
});

View File

@ -3,13 +3,14 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { localConfig } from '../../config/local.config'; import { localConfig } from '../../config/local.config';
import { loggerConfig } from '../../config/logger.config'; import { loggerConfig } from '../../config/logger.config';
import { databaseConfig } from '../../config/database.config';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
expandVariables: true, expandVariables: true,
load: [localConfig, loggerConfig], load: [localConfig, loggerConfig, databaseConfig],
}), }),
], ],
}) })

View File

@ -0,0 +1 @@
export const APP_DATASOURCES = Symbol('APP_DATASOURCES');

View File

@ -0,0 +1,40 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { APP_DATASOURCES } from './database.constants';
import { DatabaseConfig, databaseConfig } from '../../config/database.config';
import { typeormTransactionalDataSourceFactory } from './typeorm-data-source-factory';
import { typeormModuleOptionsFactory } from './typeorm-module-options-factory';
import { typeormEntitiesFromImport } from './typeorm-import-entities';
const dataSources: DataSource[] = [];
const typeormModules = [
TypeOrmModule.forRootAsync({
name: 'ebitemp-api',
dataSourceFactory: typeormTransactionalDataSourceFactory('ebitemp-api', dataSources),
useFactory: async (dbConfig: DatabaseConfig) => {
// const entities = await import('./path/to/entities');
const entities = {};
const config = await typeormModuleOptionsFactory('ebitemp-api', dbConfig, [
typeormEntitiesFromImport(entities),
]);
return config;
},
inject: [databaseConfig.KEY],
}),
];
const dataSourcesProvider = {
provide: APP_DATASOURCES,
useValue: dataSources,
};
@Global()
@Module({
imports: [...typeormModules],
providers: [dataSourcesProvider],
exports: [...typeormModules, dataSourcesProvider],
})
export class AppDatabaseModule {}

View File

@ -0,0 +1,15 @@
import { DataSource, DataSourceOptions } from 'typeorm';
export const typeormDataSourceFactory = (dataSources: DataSource[]) => async (options?: DataSourceOptions) => {
const dataSource = await new DataSource(options!).initialize();
dataSources.push(dataSource);
return dataSource;
};
export const typeormTransactionalDataSourceFactory =
(name: string, dataSources: DataSource[]) => async (options?: DataSourceOptions) => {
const tt = await import('typeorm-transactional');
const dataSource = tt.addTransactionalDataSource({ name: name, dataSource: new DataSource(options!) });
dataSources.push(dataSource);
return dataSource;
};

View File

@ -0,0 +1,5 @@
export const typeormEntitiesFromImport = async <T>(entities: T) => {
return (Object.keys(entities) as Array<keyof typeof entities>).map(
(entity: keyof typeof entities) => entities[entity]
);
};

View File

@ -0,0 +1,24 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { DatabaseConfig } from '../../config/database.config';
export const typeormModuleOptionsFactory = async (
name: string,
databaseConfig: DatabaseConfig,
entities: any[]
): Promise<TypeOrmModuleOptions> => {
return {
name: name,
type: databaseConfig.type as any,
host: databaseConfig.host,
port: databaseConfig.port,
username: databaseConfig.username,
password: databaseConfig.password,
database: databaseConfig.database,
entities: [...entities],
synchronize: false,
logging: process.env['NODE_ENV'] !== 'production',
options: {
...(!databaseConfig.secure ? { trustServerCertificate: true } : {}),
}
};
};

View File

@ -0,0 +1,5 @@
export async function patchTypeOrm() {
(await import('typeorm-transactional')).initializeTransactionalContext();
(await import('typeorm-scoped')).patchSelectQueryBuilder();
};

View File

@ -3,14 +3,17 @@
* This is only a minimal backend to get started. * This is only a minimal backend to get started.
*/ */
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Logger as PinoLogger } from 'nestjs-pino'; import { Logger as PinoLogger } from 'nestjs-pino';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { LocalConfig, localConfig } from './app/config/local.config'; import { LocalConfig, localConfig } from './app/config/local.config';
import { patchTypeOrm } from './app/modules/database/typeorm-patch';
async function bootstrap() { async function bootstrap() {
await patchTypeOrm();
const appOpts = (() => { const appOpts = (() => {
const loggerOpts = { bufferLogs: true }; const loggerOpts = { bufferLogs: true };
return { return {

View File

@ -10,13 +10,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"10": "^0.0.1",
"@autotelic/pino-seq-transport": "^0.1.0", "@autotelic/pino-seq-transport": "^0.1.0",
"@nestjs/common": "^10.0.2", "@nestjs/common": "^10.0.2",
"@nestjs/config": "^4.0.0", "@nestjs/config": "^4.0.0",
"@nestjs/core": "^10.0.2", "@nestjs/core": "^10.0.2",
"@nestjs/platform-express": "^10.0.2", "@nestjs/platform-express": "^10.0.2",
"@nestjs/typeorm": "^11.0.0",
"@nx/devkit": "20.4.1", "@nx/devkit": "20.4.1",
"axios": "^1.6.0", "axios": "^1.6.0",
"connection-string": "^4.4.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nestjs-pino": "^4.3.0", "nestjs-pino": "^4.3.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
@ -24,6 +27,9 @@
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
"typeorm": "^0.3.20",
"typeorm-scoped": "^1.2.0",
"typeorm-transactional": "^0.5.0",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {

307
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff