Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ export default defineConfig({
schema: './src/shared/entities/index.ts',
out: './migrations',
dialect: 'postgresql',
breakpoints: false,
casing: 'snake_case',
migrations: {
prefix: 'index',
table: 'migrations',
schema: 'drizzle',
},
strict: true,
verbose: true,
dbCredentials: {
url: process.env.DATABASE_URL!,
},
Expand Down
12 changes: 6 additions & 6 deletions infra/k6/scripts/clear-k6-data.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import Redis from 'ioredis';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import * as sc from '../../../src/shared/entities';
import { sql } from 'drizzle-orm';
import { Pool } from 'pg';
import postgres from 'postgres';
import { assertEnv, DB_URL, REDIS_URL } from './k6-env';
import { KEYS } from './k6-data-keys';

async function clearDB(db: NodePgDatabase<typeof sc>) {
async function clearDB(db: PostgresJsDatabase<typeof sc>) {
console.log('Cleaning up ONLY k6 test data from DB...');
return await db.transaction(async (tx) => {
await tx.delete(sc.users).where(sql`${sc.users.email} LIKE 'k6_user_%'`);
Expand All @@ -32,8 +32,8 @@ async function clearRedis(redis: Redis) {
async function main() {
assertEnv();
const redis = new Redis(REDIS_URL);
const pool = new Pool({ connectionString: DB_URL });
const db = drizzle(pool, { schema: sc });
const queryClient = postgres(DB_URL, { max: 1 });
const db = drizzle(queryClient, { schema: sc });

try {
await clearDB(db);
Expand All @@ -42,7 +42,7 @@ async function main() {
console.error('Error:', e);
process.exit(1);
} finally {
await pool.end();
await queryClient.end();
await redis.quit();
}
}
Expand Down
4 changes: 1 addition & 3 deletions infra/k6/scripts/k6-env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as dotenv from 'dotenv';

dotenv.config();
import 'dotenv/config';

export const DB_URL = process.env.DATABASE_URL;
export const REDIS_URL =
Expand Down
13 changes: 7 additions & 6 deletions infra/k6/scripts/seed-k6-data.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { createId } from '@paralleldrive/cuid2';
import * as argon from 'argon2';
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import * as sc from '../../../src/shared/entities/index';
import Redis from 'ioredis';
import { assertEnv, DB_URL, REDIS_URL } from './k6-env';
import { KEYS } from './k6-data-keys';

async function seed_db(db: NodePgDatabase<typeof sc>) {
async function seed_db(db: PostgresJsDatabase<typeof sc>) {
const COUNT = 1000;
const OUT_USERS_FILE = resolve(process.cwd(), 'infra/k6/data/users.json');
const OUT_TEAMS_FILE = resolve(process.cwd(), 'infra/k6/data/teams.json');
Expand Down Expand Up @@ -149,6 +149,7 @@ async function seed_redis(redis: Redis) {
const INVITES_PER_TEAM = 10;

const invitesData = [];

teams.forEach((team, teamIdx) => {
for (let j = 1; j <= INVITES_PER_TEAM; j++) {
const inviteeIdx = (teamIdx + j) % users.length;
Expand Down Expand Up @@ -193,8 +194,8 @@ async function seed_redis(redis: Redis) {
async function main() {
assertEnv();
const redis = new Redis(REDIS_URL);
const pool = new Pool({ connectionString: DB_URL });
const db = drizzle(pool, { schema: sc });
const queryClient = postgres(DB_URL, { max: 1 });
const db = drizzle(queryClient, { schema: sc });

try {
await seed_db(db);
Expand All @@ -203,7 +204,7 @@ async function main() {
console.error('Error:', e);
process.exit(1);
} finally {
await pool.end();
await queryClient.end();
await redis.quit();
}
}
Expand Down
3 changes: 1 addition & 2 deletions libs/bootstrap/src/configs/throttler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as dotenv from 'dotenv';
dotenv.config();
import 'dotenv/config';
import type { ThrottlerModuleOptions } from '@nestjs/throttler';

export const DEFAULT_THROTTLER_OPTIONS: ThrottlerModuleOptions = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const DATABASE_OPTIONS = 'DATABASE_OPTIONS';
export const DATABASE_SERVICE = 'DATABASE_SERVICE';
export const SQL_CLIENT = 'SQL_CLIENT';
16 changes: 16 additions & 0 deletions libs/database/src/database.module-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import type { DatabaseModuleOptions } from './interfaces';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } =
new ConfigurableModuleBuilder<DatabaseModuleOptions>()
.setClassMethodName('register')
.setExtras(
{
global: false,
},
(definition, extras) => ({
...definition,
global: extras.global,
}),
)
.build();
200 changes: 59 additions & 141 deletions libs/database/src/database.module.ts
Original file line number Diff line number Diff line change
@@ -1,162 +1,80 @@
import {
type DynamicModule,
Logger,
Module,
OnApplicationShutdown,
type Provider,
type Type,
} from '@nestjs/common';
import { Inject, Logger, Module, OnApplicationShutdown } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { DATABASE_OPTIONS, DATABASE_SERVICE } from './database.constants';
import type {
DatabaseModuleAsyncOptions,
DatabaseModuleOptions,
DatabaseModuleOptionsFactory,
} from './interfaces';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { DATABASE_SERVICE, SQL_CLIENT } from './constants';
import { MigrationService } from './migration.service';
import {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
OPTIONS_TYPE,
} from './database.module-definition';

@Module({
providers: [],
})
export class DatabaseModule implements OnApplicationShutdown {
private static logger = new Logger(DatabaseModule.name);

private static pool: Pool;

static register(config: DatabaseModuleOptions): DynamicModule {
return {
module: DatabaseModule,
global: config.global ?? false,
providers: [
this.createOptionsProvider(config),
this.createDatabaseProvider(),
MigrationService,
],
exports: [DATABASE_SERVICE],
};
}

static registerAsync(config: DatabaseModuleAsyncOptions): DynamicModule {
return {
module: DatabaseModule,
global: config.global ?? false,
imports: config.imports ?? [],
providers: [
...this.createAsyncProviders(config),
this.createDatabaseProvider(),
MigrationService,
],
exports: [DATABASE_SERVICE],
};
}

private static createOptionsProvider(options: DatabaseModuleOptions): Provider {
return {
provide: DATABASE_OPTIONS,
useValue: options,
};
}

private static createDatabaseProvider(): Provider {
return {
provide: DATABASE_SERVICE,
useFactory: async (cfg: ConfigService, opts: DatabaseModuleOptions) => {
const baseUrl = cfg.get<string>('DATABASE_URL');
providers: [
MigrationService,
{
provide: SQL_CLIENT,
inject: [ConfigService, MODULE_OPTIONS_TOKEN],
useFactory: (configService: ConfigService, opts: typeof OPTIONS_TYPE) => {
const baseUrl = configService.getOrThrow<string>('DATABASE_URL');
const url = new URL(baseUrl);
url.searchParams.set('options', `-c search_path=${opts.schemaName || 'public'}`);

const pool = new Pool({
connectionString: url.toString(),
max: 20,
min: 2,
connectionTimeoutMillis: 2000,
idleTimeoutMillis: 10000,
maxUses: 5000,
keepAlive: true,
if (opts.schemaName) {
url.searchParams.set('options', `-c search_path=${opts.schemaName}`);
}

return postgres(url.toString(), {
onnotice: (msg) => new Logger('PostgresJS').verbose(msg),
backoff: (attempt) => Math.min(attempt * 100, 3000),
target_session_attrs: 'read-write',
publications: 'alltables',
connect_timeout: 2,
idle_timeout: 5,
max_lifetime: 60 * 60,
keep_alive: 30,
transform: {
undefined: null,
},
...opts.pool,
});
},
},
{
provide: DATABASE_SERVICE,
inject: [SQL_CLIENT, MODULE_OPTIONS_TOKEN],
useFactory: (sql: postgres.Sql, opts: typeof OPTIONS_TYPE) => {
const logger = new Logger('Drizzle');

pool.on('error', (err) => {
DatabaseModule.logger.error('Database pool connection lost or reset', err);
});

this.pool = pool;

return drizzle(pool, {
return drizzle(sql, {
schema: opts.schema,
logger: opts.logging
? {
logQuery(query, params) {
const start = Date.now();
DatabaseModule.logger.debug(`SQL: ${query}`);

if (params?.length) {
DatabaseModule.logger.debug(
`Params: ${JSON.stringify(params)}`,
);
}

const duration = Date.now() - start;
DatabaseModule.logger.debug(`Execution time: ${duration}ms`);
logger.debug(`SQL: ${query}`);
if (params?.length)
logger.debug(`Params: ${JSON.stringify(params)}`);
},
}
: false,
});
},
inject: [ConfigService, DATABASE_OPTIONS],
};
}

private static createAsyncProviders(options: DatabaseModuleAsyncOptions): Provider[] {
if (options.useFactory) {
return [
{
provide: DATABASE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
},
...(options.extraProviders || []),
];
}

const providers: Provider[] = [];

const useClass = options.useClass || options.useExisting;
if (!useClass) {
throw new Error(
'You must provide either useClass, useExisting or useFactory in DatabaseModuleAsyncOptions',
);
}

providers.push(this.createAsyncOptionsProvider(useClass));

if (options.useClass) {
providers.push({ provide: useClass, useClass });
}

if (options.extraProviders) {
providers.push(...options.extraProviders);
}

return providers;
}

private static createAsyncOptionsProvider(
useClass: Type<DatabaseModuleOptionsFactory>,
): Provider {
return {
provide: DATABASE_OPTIONS,
useFactory: async (optionsFactory: DatabaseModuleOptionsFactory) =>
optionsFactory.createDatabaseOptions(),
inject: [useClass],
};
},
],
exports: [DATABASE_SERVICE],
})
export class DatabaseModule extends ConfigurableModuleClass implements OnApplicationShutdown {
private readonly logger = new Logger(DatabaseModule.name);

constructor(
@Inject(SQL_CLIENT)
private readonly sql: postgres.Sql,
) {
super();
}

async onApplicationShutdown(_signal?: string) {
if (DatabaseModule.pool) {
DatabaseModule.logger.log('Closing database connections...');
await DatabaseModule.pool.end();
}
async onApplicationShutdown() {
this.logger.log('Closing database connections...');
await this.sql.end();
}
}
4 changes: 2 additions & 2 deletions libs/database/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './database.module';
export * from './database.constants';
export type { DatabaseService } from './interfaces/database-module.interface';
export { DATABASE_SERVICE } from './constants';
export type { DatabaseService } from './interfaces';
34 changes: 0 additions & 34 deletions libs/database/src/interfaces/database-module.interface.ts

This file was deleted.

Loading
Loading