Skip to content

Commit 3404307

Browse files
Merge pull request #118 from ayomidearegbeshola29-dev/feature/database-module-issue-104
feat: database module for issue #104
2 parents 5a7fc5f + b162ed0 commit 3404307

29 files changed

Lines changed: 2858 additions & 0 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Test, TestingModule } from "@nestjs/testing";
2+
import { ConfigModule, ConfigService } from "@nestjs/config";
3+
import { DataSource } from "typeorm";
4+
import { DatabaseConfigService } from "./database.config";
5+
6+
describe("DatabaseConfigService", () => {
7+
let service: DatabaseConfigService;
8+
9+
beforeEach(async () => {
10+
const module: TestingModule = await Test.createTestingModule({
11+
imports: [ConfigModule.forRoot({ isGlobal: true, ignoreEnvVars: true })],
12+
providers: [DatabaseConfigService],
13+
}).compile();
14+
15+
service = module.get<DatabaseConfigService>(DatabaseConfigService);
16+
jest.clearAllMocks();
17+
});
18+
19+
describe("getConnectionOptions", () => {
20+
it("returns development config by default", () => {
21+
const config = service.getConnectionOptions();
22+
expect(config).toBeDefined();
23+
expect(config.pool).toBeDefined();
24+
expect(config.pool.max).toBeGreaterThan(0);
25+
});
26+
27+
it("returns test config for test environment", () => {
28+
process.env.NODE_ENV = "test";
29+
const config = service.getConnectionOptions();
30+
expect(config.database).toBe(":memory:");
31+
process.env.NODE_ENV = "development";
32+
});
33+
});
34+
35+
describe("getDataSourceOptions", () => {
36+
it("returns TypeORM DataSourceOptions", () => {
37+
const options = service.getDataSourceOptions();
38+
expect(options).toBeDefined();
39+
expect((options as any).pool).toBeDefined();
40+
});
41+
});
42+
});
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Injectable, Logger } from "@nestjs/common";
2+
import { ConfigService } from "@nestjs/config";
3+
import { DataSource, DataSourceOptions } from "typeorm";
4+
5+
export interface ConnectionPoolConfig {
6+
max: number;
7+
min: number;
8+
idleTimeoutMillis: number;
9+
connectionTimeoutMillis: number;
10+
acquireTimeoutMillis?: number;
11+
createTimeoutMillis?: number;
12+
destroyTimeoutMillis?: number;
13+
reapIntervalMillis?: number;
14+
createRetryIntervalMillis?: number;
15+
}
16+
17+
export interface DatabaseConfig {
18+
type: "postgres" | "sqlite";
19+
host?: string;
20+
port?: number;
21+
username?: string;
22+
password?: string;
23+
database: string;
24+
url?: string;
25+
pool: ConnectionPoolConfig;
26+
ssl?: boolean | { rejectUnauthorized?: boolean };
27+
synchronize?: boolean;
28+
logging?: boolean | string[];
29+
migrations?: string[];
30+
entities: string[];
31+
}
32+
33+
@Injectable()
34+
export class DatabaseConfigService {
35+
private readonly logger = new Logger(DatabaseConfigService.name);
36+
private readonly environment: string;
37+
38+
constructor(private readonly configService: ConfigService) {
39+
this.environment = this.configService.get<string>("NODE_ENV") ?? "development";
40+
}
41+
42+
getConnectionOptions(): DatabaseConfig {
43+
const configs: Record<string, () => DatabaseConfig> = {
44+
development: () => this.getDevelopmentConfig(),
45+
staging: () => this.getStagingConfig(),
46+
production: () => this.getProductionConfig(),
47+
test: () => this.getTestConfig(),
48+
};
49+
50+
const envConfig = configs[this.environment] || configs.development;
51+
return envConfig();
52+
}
53+
54+
getDataSourceOptions(): DataSourceOptions {
55+
const config = this.getConnectionOptions();
56+
return {
57+
type: config.type,
58+
host: config.host,
59+
port: config.port,
60+
username: config.username,
61+
password: config.password,
62+
database: config.database,
63+
url: config.url,
64+
entities: config.entities,
65+
synchronize: config.synchronize,
66+
logging: this.getLoggingLevel(config.logging),
67+
pool: config.pool,
68+
ssl: config.ssl,
69+
migrations: config.migrations,
70+
} as DataSourceOptions;
71+
}
72+
73+
private getLoggingLevel(
74+
logging?: boolean | string[],
75+
): boolean | string[] {
76+
if (this.environment === "production") {
77+
return ["error", "warn", "migration", "query-slow"];
78+
}
79+
if (this.environment === "development") {
80+
return ["query", "schema", "error", "warn", "migration"];
81+
}
82+
return logging ?? true;
83+
}
84+
85+
private getDevelopmentConfig(): DatabaseConfig {
86+
const databaseUrl = this.configService.get<string>("DATABASE_URL");
87+
if (databaseUrl) {
88+
return {
89+
type: "postgres",
90+
url: databaseUrl,
91+
database: "alian-structure",
92+
pool: {
93+
max: 10,
94+
min: 2,
95+
idleTimeoutMillis: 30000,
96+
connectionTimeoutMillis: 10000,
97+
acquireTimeoutMillis: 10000,
98+
createTimeoutMillis: 5000,
99+
destroyTimeoutMillis: 5000,
100+
reapIntervalMillis: 10000,
101+
createRetryIntervalMillis: 200,
102+
},
103+
ssl: false,
104+
synchronize: false,
105+
logging: false,
106+
migrations: ["src/migrations/**/*.ts"],
107+
entities: ["src/common/database/entities/**/*.entity.ts"],
108+
};
109+
}
110+
return {
111+
type: "sqlite",
112+
database: "data/dev.db",
113+
pool: {
114+
max: 5,
115+
min: 1,
116+
idleTimeoutMillis: 30000,
117+
connectionTimeoutMillis: 10000,
118+
},
119+
synchronize: true,
120+
logging: false,
121+
migrations: [],
122+
entities: ["src/common/database/entities/**/*.entity.ts"],
123+
};
124+
}
125+
126+
private getStagingConfig(): DatabaseConfig {
127+
const databaseUrl = this.configService.get<string>("DATABASE_URL");
128+
if (!databaseUrl) {
129+
throw new Error("DATABASE_URL is required for staging environment");
130+
}
131+
return {
132+
type: "postgres",
133+
url: databaseUrl,
134+
database: "alian-structure-staging",
135+
pool: {
136+
max: 20,
137+
min: 5,
138+
idleTimeoutMillis: 30000,
139+
connectionTimeoutMillis: 5000,
140+
acquireTimeoutMillis: 8000,
141+
createTimeoutMillis: 5000,
142+
destroyTimeoutMillis: 5000,
143+
reapIntervalMillis: 5000,
144+
createRetryIntervalMillis: 200,
145+
},
146+
ssl: { rejectUnauthorized: false },
147+
synchronize: false,
148+
logging: true,
149+
migrations: ["src/migrations/**/*.ts"],
150+
entities: ["src/common/database/entities/**/*.entity.ts"],
151+
};
152+
}
153+
154+
private getProductionConfig(): DatabaseConfig {
155+
const databaseUrl = this.configService.get<string>("DATABASE_URL");
156+
if (!databaseUrl) {
157+
throw new Error("DATABASE_URL is required for production environment");
158+
}
159+
return {
160+
type: "postgres",
161+
url: databaseUrl,
162+
database: "alian-structure-production",
163+
pool: {
164+
max: 50,
165+
min: 10,
166+
idleTimeoutMillis: 60000,
167+
connectionTimeoutMillis: 10000,
168+
acquireTimeoutMillis: 15000,
169+
createTimeoutMillis: 10000,
170+
destroyTimeoutMillis: 10000,
171+
reapIntervalMillis: 5000,
172+
createRetryIntervalMillis: 1000,
173+
},
174+
ssl: { rejectUnauthorized: true },
175+
synchronize: false,
176+
logging: true,
177+
migrations: ["dist/migrations/**/*.js"],
178+
entities: ["dist/common/database/entities/**/*.entity.js"],
179+
};
180+
}
181+
182+
private getTestConfig(): DatabaseConfig {
183+
return {
184+
type: "sqlite",
185+
database: ":memory:",
186+
pool: {
187+
max: 5,
188+
min: 1,
189+
idleTimeoutMillis: 30000,
190+
connectionTimeoutMillis: 10000,
191+
},
192+
synchronize: true,
193+
logging: false,
194+
migrations: [],
195+
entities: ["src/common/database/entities/**/*.entity.ts"],
196+
};
197+
}
198+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Test, TestingModule } from "@nestjs/testing";
2+
import { DataSource } from "typeorm";
3+
import { DatabaseModule } from "./database.module";
4+
5+
describe("DatabaseModule", () => {
6+
let moduleRef: TestingModule;
7+
8+
beforeEach(async () => {
9+
moduleRef = await Test.createTestingModule({
10+
imports: [],
11+
}).compile();
12+
});
13+
14+
afterEach(async () => {
15+
await moduleRef.close();
16+
});
17+
18+
it("should compile", () => {
19+
expect(moduleRef).toBeDefined();
20+
});
21+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { DynamicModule, Module, Provider } from "@nestjs/common";
2+
import { ConfigModule, ConfigService } from "@nestjs/config";
3+
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
4+
import { DataSource, DataSourceOptions } from "typeorm";
5+
import { SnakeNamingStrategy } from "./strategies/snake-naming.strategy";
6+
7+
export const DATABASE_CONFIG = "DATABASE_CONFIG";
8+
export const DATABASE_DATA_SOURCE = "DATABASE_DATA_SOURCE";
9+
10+
export interface DatabaseModuleOptions {
11+
configName?: string;
12+
}
13+
14+
@Module({})
15+
export class DatabaseModule {
16+
static forRootAsync(options: {
17+
imports?: any[];
18+
inject?: any[];
19+
useFactory: (...args: any[]) => Promise<TypeOrmModuleOptions> | TypeOrmModuleOptions;
20+
}): DynamicModule {
21+
const dataSourceProvider: Provider = {
22+
provide: DATABASE_DATA_SOURCE,
23+
useFactory: async (configService: ConfigService) => {
24+
const config: TypeOrmModuleOptions = await options.useFactory(configService);
25+
const dataSource = new DataSource(config as any);
26+
try {
27+
await dataSource.initialize();
28+
} catch (error) {
29+
throw new Error(`Failed to initialize database: ${error.message}`);
30+
}
31+
return dataSource;
32+
},
33+
inject: options.inject || [],
34+
};
35+
36+
return {
37+
module: DatabaseModule,
38+
imports: [
39+
ConfigModule,
40+
TypeOrmModule.forRootAsync({
41+
imports: options.imports || [],
42+
inject: options.inject || [],
43+
useFactory: options.useFactory,
44+
}),
45+
],
46+
providers: [dataSourceProvider],
47+
exports: [DATABASE_DATA_SOURCE, TypeOrmModule],
48+
};
49+
}
50+
51+
static forFeature(options: { entities?: any[]; imports?: any[] }): DynamicModule {
52+
return {
53+
module: DatabaseModule,
54+
imports: [
55+
...(options.imports || []),
56+
TypeOrmModule.forFeature(options.entities || []),
57+
],
58+
exports: [TypeOrmModule],
59+
};
60+
}
61+
}

0 commit comments

Comments
 (0)