From 6a8ccdc19ff5728c6281231cc43b5f145fd473d3 Mon Sep 17 00:00:00 2001 From: Etienne Bechara Date: Tue, 20 Apr 2021 13:15:41 -0300 Subject: [PATCH] feat: add sync schema blacklist --- README.md | 13 ++-- source/orm/orm.config.ts | 8 --- .../orm/orm.interface/orm.module.options.ts | 5 +- source/orm/orm.module.ts | 18 ++--- source/schema/schema.enum/index.ts | 1 - source/schema/schema.interface/index.ts | 1 - .../schema.interface/schema.module.options.ts | 11 --- source/schema/schema.service.ts | 55 --------------- source/sync/sync.enum/index.ts | 1 + .../sync.enum/sync.injection.token.ts} | 2 +- source/sync/sync.interface/index.ts | 1 + .../sync.interface/sync.module.options.ts | 12 ++++ .../schema.module.ts => sync/sync.module.ts} | 30 ++++---- source/sync/sync.service.ts | 70 +++++++++++++++++++ source/test/test.main.ts | 8 ++- 15 files changed, 123 insertions(+), 113 deletions(-) delete mode 100644 source/schema/schema.enum/index.ts delete mode 100644 source/schema/schema.interface/index.ts delete mode 100644 source/schema/schema.interface/schema.module.options.ts delete mode 100644 source/schema/schema.service.ts create mode 100644 source/sync/sync.enum/index.ts rename source/{schema/schema.enum/schema.injection.token.ts => sync/sync.enum/sync.injection.token.ts} (65%) create mode 100644 source/sync/sync.interface/index.ts create mode 100644 source/sync/sync.interface/sync.module.options.ts rename source/{schema/schema.module.ts => sync/sync.module.ts} (51%) create mode 100644 source/sync/sync.service.ts diff --git a/README.md b/README.md index b28ac53..463fbe4 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,6 @@ ORM_PORT=3306 ORM_USERNAME='root' ORM_PASSWORD='1234' ORM_DATABASE='test' -ORM_SYNC_SCHEMA=true -ORM_SYNC_SAFE=false ``` It is recommended that you have a local database in order to test connectivity. @@ -46,9 +44,8 @@ It is recommended that you have a local database in order to test connectivity. 3\. Import `OrmModule` and `OrmConfig` into you boot script and configure asynchronously: ```ts -import { AppModule } from '@bechara/nestjs-core'; -import { OrmConfig } from '@bechara/nestjs-orm'; -import { OrmModule } from '@bechara/nestjs-orm'; +import { AppEnvironment, AppModule } from '@bechara/nestjs-core'; +import { OrmConfig. OrmModule } from '@bechara/nestjs-orm'; void AppModule.bootServer({ configs: [ OrmConfig ], @@ -62,8 +59,10 @@ void AppModule.bootServer({ database: ormConfig.ORM_DATABASE, username: ormConfig.ORM_USERNAME, password: ormConfig.ORM_PASSWORD, - schemaSync: ormConfig.ORM_SYNC_SCHEMA, - safeSync: ormConfig.ORM_SYNC_SAFE, + sync: { + enable: true, + safe: ormConfig.NODE_ENV === AppEnvironment.PRODUCTION, + }, }), }), ], diff --git a/source/orm/orm.config.ts b/source/orm/orm.config.ts index 3036468..294a027 100644 --- a/source/orm/orm.config.ts +++ b/source/orm/orm.config.ts @@ -35,12 +35,4 @@ export class OrmConfig { @IsString() @IsNotEmpty() public readonly ORM_DATABASE: string; - @InjectSecret() - @Transform((o) => o.value === 'true') - public readonly ORM_SYNC_SCHEMA: boolean; - - @InjectSecret() - @Transform((o) => o.value === 'true') - public readonly ORM_SYNC_SAFE: boolean; - } diff --git a/source/orm/orm.interface/orm.module.options.ts b/source/orm/orm.interface/orm.module.options.ts index 2f4c38b..232597c 100644 --- a/source/orm/orm.interface/orm.module.options.ts +++ b/source/orm/orm.interface/orm.module.options.ts @@ -1,6 +1,8 @@ import { ModuleMetadata } from '@bechara/nestjs-core'; import { MikroOrmModuleOptions } from '@mikro-orm/nestjs'; +import { SyncModuleOptions } from '../../sync/sync.interface'; + export interface OrmAsyncModuleOptions extends Pick { disableEntityScan?: boolean; entities?: any[]; @@ -15,7 +17,6 @@ export interface OrmModuleOptions { username: string; password?: string; database?: string; - schemaSync?: boolean; - safeSync?: boolean; + sync?: SyncModuleOptions; extras?: MikroOrmModuleOptions; } diff --git a/source/orm/orm.module.ts b/source/orm/orm.module.ts index 61a9868..518c3da 100644 --- a/source/orm/orm.module.ts +++ b/source/orm/orm.module.ts @@ -2,8 +2,8 @@ import { AppConfig, AppEnvironment, DynamicModule, LoggerService, Module, UtilMo import { MikroORMOptions } from '@mikro-orm/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { SchemaModuleOptions } from '../schema/schema.interface'; -import { SchemaModule } from '../schema/schema.module'; +import { SyncModuleOptions } from '../sync/sync.interface'; +import { SyncModule } from '../sync/sync.module'; import { OrmIdEntity, OrmTimestampEntity, OrmUuidEntity } from './orm.entity'; import { OrmInjectionToken } from './orm.enum'; import { OrmAsyncModuleOptions, OrmModuleOptions } from './orm.interface'; @@ -35,19 +35,21 @@ export class OrmModule { return { module: OrmModule, + imports: [ MikroOrmModule.forRootAsync({ inject: [ OrmInjectionToken.ORM_PROVIDER_OPTIONS ], - useFactory: (mikroOrmOptions: OrmModuleOptions) => ({ ...mikroOrmOptions }), + useFactory: (mikroOrmOptions: OrmModuleOptions) => mikroOrmOptions, }), - SchemaModule.registerAsync({ + SyncModule.registerAsync({ inject: [ OrmInjectionToken.ORM_SCHEMA_OPTIONS ], - useFactory: (schemaModuleOptions: SchemaModuleOptions) => ({ ...schemaModuleOptions }), + useFactory: (syncModuleOptions: SyncModuleOptions) => syncModuleOptions, }), MikroOrmModule.forFeature({ entities }), ], + providers: [ AppConfig, { @@ -78,12 +80,10 @@ export class OrmModule { { provide: OrmInjectionToken.ORM_SCHEMA_OPTIONS, inject: [ OrmInjectionToken.ORM_MODULE_OPTIONS ], - useFactory: (ormModuleOptions: OrmModuleOptions): SchemaModuleOptions => ({ - safeSync: ormModuleOptions.safeSync, - schemaSync: ormModuleOptions.schemaSync, - }), + useFactory: (ormModuleOptions: OrmModuleOptions): SyncModuleOptions => ormModuleOptions.sync, }, ], + exports: [ OrmInjectionToken.ORM_PROVIDER_OPTIONS, OrmInjectionToken.ORM_SCHEMA_OPTIONS, diff --git a/source/schema/schema.enum/index.ts b/source/schema/schema.enum/index.ts deleted file mode 100644 index f91b63d..0000000 --- a/source/schema/schema.enum/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './schema.injection.token'; diff --git a/source/schema/schema.interface/index.ts b/source/schema/schema.interface/index.ts deleted file mode 100644 index 2673cb8..0000000 --- a/source/schema/schema.interface/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './schema.module.options'; diff --git a/source/schema/schema.interface/schema.module.options.ts b/source/schema/schema.interface/schema.module.options.ts deleted file mode 100644 index 9e77e9a..0000000 --- a/source/schema/schema.interface/schema.module.options.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ModuleMetadata } from '@bechara/nestjs-core'; - -export interface SchemaAsyncModuleOptions extends Pick { - inject?: any[]; - useFactory?: (...args: any[]) => Promise | SchemaModuleOptions; -} - -export interface SchemaModuleOptions { - schemaSync: boolean; - safeSync: boolean; -} diff --git a/source/schema/schema.service.ts b/source/schema/schema.service.ts deleted file mode 100644 index b826dd0..0000000 --- a/source/schema/schema.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Inject, Injectable, LoggerService } from '@bechara/nestjs-core'; -import { MikroORM } from '@mikro-orm/core'; - -import { SchemaInjectionToken } from './schema.enum'; -import { SchemaModuleOptions } from './schema.interface'; - -@Injectable() -export class SchemaService { - - public constructor( - @Inject(SchemaInjectionToken.MODULE_OPTIONS) - private readonly schemaModuleOptions: SchemaModuleOptions, - private readonly mikroOrm: MikroORM, - private readonly loggerService: LoggerService, - ) { - const options = this.schemaModuleOptions; - - if (options.schemaSync) { - void this.syncSchema(options.safeSync); - } - } - - /** - * Automatically sync current database schema with - * configured entities. - * @param safe - */ - public async syncSchema(safe?: boolean): Promise { - this.loggerService.info('[OrmService] Starting database schema sync...'); - - const generator = this.mikroOrm.getSchemaGenerator(); - const updateDump = await generator.getUpdateSchemaSQL(false, safe); - - if (updateDump.length === 0) { - return this.loggerService.notice('[OrmService] Database schema is up to date'); - } - - await generator.updateSchema(true, safe); - this.loggerService.notice('[OrmService] Database schema successfully updated'); - } - - /** - * Erase current database schema and recreate it. - */ - public async resetSchema(): Promise { - this.loggerService.info('[OrmService] Starting database schema reset...'); - - const generator = this.mikroOrm.getSchemaGenerator(); - await generator.dropSchema(); - await generator.createSchema(); - - this.loggerService.notice('[OrmService] Database schema successfully reset'); - } - -} diff --git a/source/sync/sync.enum/index.ts b/source/sync/sync.enum/index.ts new file mode 100644 index 0000000..87ad8ca --- /dev/null +++ b/source/sync/sync.enum/index.ts @@ -0,0 +1 @@ +export * from './sync.injection.token'; diff --git a/source/schema/schema.enum/schema.injection.token.ts b/source/sync/sync.enum/sync.injection.token.ts similarity index 65% rename from source/schema/schema.enum/schema.injection.token.ts rename to source/sync/sync.enum/sync.injection.token.ts index df3a9d3..fd56442 100644 --- a/source/schema/schema.enum/schema.injection.token.ts +++ b/source/sync/sync.enum/sync.injection.token.ts @@ -1,4 +1,4 @@ -export enum SchemaInjectionToken { +export enum SyncInjectionToken { MODULE_ID = 'MODULE_ID', MODULE_OPTIONS = 'MODULE_OPTIONS', } diff --git a/source/sync/sync.interface/index.ts b/source/sync/sync.interface/index.ts new file mode 100644 index 0000000..9b9c876 --- /dev/null +++ b/source/sync/sync.interface/index.ts @@ -0,0 +1 @@ +export * from './sync.module.options'; diff --git a/source/sync/sync.interface/sync.module.options.ts b/source/sync/sync.interface/sync.module.options.ts new file mode 100644 index 0000000..e782ed8 --- /dev/null +++ b/source/sync/sync.interface/sync.module.options.ts @@ -0,0 +1,12 @@ +import { ModuleMetadata } from '@bechara/nestjs-core'; + +export interface SyncAsyncModuleOptions extends Pick { + inject?: any[]; + useFactory?: (...args: any[]) => Promise | SyncModuleOptions; +} + +export interface SyncModuleOptions { + enable?: boolean; + safe?: boolean; + blacklist?: string[]; +} diff --git a/source/schema/schema.module.ts b/source/sync/sync.module.ts similarity index 51% rename from source/schema/schema.module.ts rename to source/sync/sync.module.ts index 9ecb49a..3dbc7ab 100644 --- a/source/schema/schema.module.ts +++ b/source/sync/sync.module.ts @@ -1,39 +1,39 @@ import { DynamicModule, LoggerModule, Module } from '@bechara/nestjs-core'; import { v4 } from 'uuid'; -import { SchemaInjectionToken } from './schema.enum'; -import { SchemaAsyncModuleOptions, SchemaModuleOptions } from './schema.interface'; -import { SchemaService } from './schema.service'; +import { SyncInjectionToken } from './sync.enum'; +import { SyncAsyncModuleOptions, SyncModuleOptions } from './sync.interface'; +import { SyncService } from './sync.service'; @Module({ imports: [ LoggerModule ], providers: [ - SchemaService, + SyncService, { - provide: SchemaInjectionToken.MODULE_OPTIONS, + provide: SyncInjectionToken.MODULE_OPTIONS, useValue: { }, }, ], exports: [ - SchemaService, + SyncService, ], }) -export class SchemaModule { +export class SyncModule { /** * Registers underlying service with provided options. * @param options */ - public static register(options: SchemaModuleOptions): DynamicModule { + public static register(options: SyncModuleOptions): DynamicModule { return { - module: SchemaModule, + module: SyncModule, providers: [ { - provide: SchemaInjectionToken.MODULE_ID, + provide: SyncInjectionToken.MODULE_ID, useValue: v4(), }, { - provide: SchemaInjectionToken.MODULE_OPTIONS, + provide: SyncInjectionToken.MODULE_OPTIONS, useValue: options, }, ], @@ -44,17 +44,17 @@ export class SchemaModule { * Register underlying service with provided options asynchronously. * @param options */ - public static registerAsync(options: SchemaAsyncModuleOptions = { }): DynamicModule { + public static registerAsync(options: SyncAsyncModuleOptions = { }): DynamicModule { return { - module: SchemaModule, + module: SyncModule, imports: options.imports, providers: [ { - provide: SchemaInjectionToken.MODULE_ID, + provide: SyncInjectionToken.MODULE_ID, useValue: v4(), }, { - provide: SchemaInjectionToken.MODULE_OPTIONS, + provide: SyncInjectionToken.MODULE_OPTIONS, inject: options.inject, useFactory: options.useFactory, }, diff --git a/source/sync/sync.service.ts b/source/sync/sync.service.ts new file mode 100644 index 0000000..9136ea0 --- /dev/null +++ b/source/sync/sync.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable, LoggerService } from '@bechara/nestjs-core'; +import { MikroORM } from '@mikro-orm/core'; + +import { SyncInjectionToken } from './sync.enum'; +import { SyncModuleOptions } from './sync.interface'; + +@Injectable() +export class SyncService { + + public constructor( + @Inject(SyncInjectionToken.MODULE_OPTIONS) + private readonly syncModuleOptions: SyncModuleOptions, + private readonly mikroOrm: MikroORM, + private readonly loggerService: LoggerService, + ) { + const options = this.syncModuleOptions; + + if (options.enable) { + void this.syncSync(options); + } + } + + /** + * Remove from schema queries that has been blacklisted. + * @param queries + * @param options + */ + private removeBlacklistedQueries(queries: string, options: SyncModuleOptions): string { + options.blacklist ??= [ ]; + queries = queries.replace(/\n+/g, '\n'); + return queries.split('\n').filter((q) => !options.blacklist.includes(q)).join('\n'); + } + + /** + * Automatically sync current database schema with + * configured entities. + * @param options + */ + public async syncSync(options: SyncModuleOptions): Promise { + this.loggerService.info('[OrmService] Starting database schema sync...'); + + const generator = this.mikroOrm.getSchemaGenerator(); + let syncDump = await generator.getUpdateSchemaSQL(false, options.safe); + syncDump = this.removeBlacklistedQueries(syncDump, options); + + if (syncDump.length === 0) { + return this.loggerService.notice('[OrmService] Database schema is up to date'); + } + + let syncQueries = await generator.getUpdateSchemaSQL(true, options.safe); + syncQueries = this.removeBlacklistedQueries(syncQueries, options); + await generator.execute(syncQueries); + + this.loggerService.notice('[OrmService] Database schema successfully updated'); + } + + /** + * Erase current database schema and recreate it. + */ + public async resetSync(): Promise { + this.loggerService.info('[OrmService] Starting database schema reset...'); + + const generator = this.mikroOrm.getSchemaGenerator(); + await generator.dropSchema(); + await generator.createSchema(); + + this.loggerService.notice('[OrmService] Database schema successfully reset'); + } + +} diff --git a/source/test/test.main.ts b/source/test/test.main.ts index 23447e5..147baf9 100644 --- a/source/test/test.main.ts +++ b/source/test/test.main.ts @@ -1,4 +1,4 @@ -import { AppModule } from '@bechara/nestjs-core'; +import { AppEnvironment, AppModule } from '@bechara/nestjs-core'; import { OrmConfig } from '../orm/orm.config'; import { OrmModule } from '../orm/orm.module'; @@ -38,8 +38,10 @@ void AppModule.bootServer({ database: ormConfig.ORM_DATABASE, username: ormConfig.ORM_USERNAME, password: ormConfig.ORM_PASSWORD, - schemaSync: ormConfig.ORM_SYNC_SCHEMA, - safeSync: ormConfig.ORM_SYNC_SAFE, + sync: { + enable: true, + safe: ormConfig.NODE_ENV === AppEnvironment.PRODUCTION, + }, }), }), ],