From e0ef15df1eda566f9500b030b8b8806a49c3161a Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 22 Feb 2026 00:10:08 -0800 Subject: [PATCH] fix: add SnakeNamingStrategy for TypeORM to match snake_case DB columns TypeORM entities use camelCase properties (tenantId, passwordHash) but database tables use snake_case columns (tenant_id, password_hash). The naming strategy automatically converts between the two conventions. Co-Authored-By: Claude Opus 4.6 --- .../shared/database/src/database.module.ts | 2 ++ packages/shared/database/src/index.ts | 1 + .../database/src/snake-naming.strategy.ts | 28 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 packages/shared/database/src/snake-naming.strategy.ts diff --git a/packages/shared/database/src/database.module.ts b/packages/shared/database/src/database.module.ts index 5878e3e..e95a67d 100644 --- a/packages/shared/database/src/database.module.ts +++ b/packages/shared/database/src/database.module.ts @@ -2,6 +2,7 @@ import { DynamicModule, MiddlewareConsumer, Module, NestModule } from '@nestjs/c import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigService } from '@nestjs/config'; import { TenantContextMiddleware } from './tenant-context.middleware'; +import { SnakeNamingStrategy } from './snake-naming.strategy'; @Module({}) export class DatabaseModule implements NestModule { @@ -25,6 +26,7 @@ export class DatabaseModule implements NestModule { autoLoadEntities: true, synchronize: config.get('NODE_ENV') === 'development', logging: config.get('DB_LOGGING', 'false') === 'true', + namingStrategy: new SnakeNamingStrategy(), }), }), ], diff --git a/packages/shared/database/src/index.ts b/packages/shared/database/src/index.ts index 232512f..cb3043e 100644 --- a/packages/shared/database/src/index.ts +++ b/packages/shared/database/src/index.ts @@ -2,3 +2,4 @@ export * from './tenant-aware.repository'; export * from './database.module'; export * from './tenant-provisioning.service'; export * from './tenant-context.middleware'; +export * from './snake-naming.strategy'; diff --git a/packages/shared/database/src/snake-naming.strategy.ts b/packages/shared/database/src/snake-naming.strategy.ts new file mode 100644 index 0000000..8f1f275 --- /dev/null +++ b/packages/shared/database/src/snake-naming.strategy.ts @@ -0,0 +1,28 @@ +import { DefaultNamingStrategy, NamingStrategyInterface } from 'typeorm'; + +function toSnakeCase(str: string): string { + return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); +} + +export class SnakeNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface { + tableName(className: string, customName: string): string { + return customName || toSnakeCase(className); + } + + columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string { + const prefix = embeddedPrefixes.length ? toSnakeCase(embeddedPrefixes.join('_')) + '_' : ''; + return prefix + (customName || toSnakeCase(propertyName)); + } + + relationName(propertyName: string): string { + return toSnakeCase(propertyName); + } + + joinColumnName(relationName: string, referencedColumnName: string): string { + return toSnakeCase(relationName) + '_' + referencedColumnName; + } + + joinTableColumnName(tableName: string, propertyName: string, columnName?: string): string { + return toSnakeCase(tableName) + '_' + (columnName || toSnakeCase(propertyName)); + } +}