add db migration

This commit is contained in:
liuyi 2025-06-20 11:06:39 +08:00
parent 67e4466687
commit 13b5bc629a
8 changed files with 119 additions and 23 deletions

View File

@ -9,12 +9,15 @@ import { SearchService } from '@/modules/content/services';
import { SanitizeService } from '@/modules/content/services/SanitizeService'; import { SanitizeService } from '@/modules/content/services/SanitizeService';
import { PostService } from '@/modules/content/services/post.service'; import { PostService } from '@/modules/content/services/post.service';
import { PostSubscriber } from '@/modules/content/subscribers/post.subscriber';
import { DatabaseModule } from '@/modules/database/database.module'; import { DatabaseModule } from '@/modules/database/database.module';
import { addSubscribers } from '@/modules/database/utils';
import { Configure } from '../config/configure'; import { Configure } from '../config/configure';
import { defauleContentConfig } from './config'; import { defauleContentConfig } from './config';
import * as subscribers from './subscribers';
import { ContentConfig } from './types'; import { ContentConfig } from './types';
@Module({}) @Module({})
@ -23,7 +26,7 @@ export class ContentModule {
const config = await configure.get<ContentConfig>('content', defauleContentConfig); const config = await configure.get<ContentConfig>('content', defauleContentConfig);
const providers: ModuleMetadata['providers'] = [ const providers: ModuleMetadata['providers'] = [
...Object.values(services), ...Object.values(services),
PostSubscriber, ...(await addSubscribers(configure, Object.values(subscribers))),
{ {
provide: PostService, provide: PostService,
inject: [ inject: [

View File

@ -0,0 +1 @@
export * from './post.subscriber';

View File

@ -1,33 +1,33 @@
import { Optional } from '@nestjs/common';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { DataSource, EventSubscriber, ObjectType } from 'typeorm'; import { DataSource, EventSubscriber, ObjectType } from 'typeorm';
import { Configure } from '@/modules/config/configure'; import { Configure } from '@/modules/config/configure';
import { PostBodyType } from '@/modules/content/constants'; import { PostBodyType } from '@/modules/content/constants';
import { PostEntity } from '@/modules/content/entities/post.entity'; import { PostEntity } from '@/modules/content/entities/post.entity';
import { PostRepository } from '@/modules/content/repositories/post.repository';
import { SanitizeService } from '@/modules/content/services/SanitizeService'; import { SanitizeService } from '@/modules/content/services/SanitizeService';
import { BaseSubscriber } from '@/modules/database/base/subscriber'; import { BaseSubscriber } from '@/modules/database/base/subscriber';
@EventSubscriber() @EventSubscriber()
export class PostSubscriber extends BaseSubscriber<PostEntity> { export class PostSubscriber extends BaseSubscriber<PostEntity> {
protected entity: ObjectType<PostEntity> = PostEntity; protected entity: ObjectType<PostEntity> = PostEntity;
constructor( constructor(
protected dataSource: DataSource, protected dataSource: DataSource,
protected postRepository: PostRepository, protected _configure: Configure,
protected configure: Configure,
@Optional() protected sanitizeService: SanitizeService,
) { ) {
super(dataSource); super(dataSource, _configure);
}
get configure(): Configure {
return this._configure;
} }
async afterLoad(entity: PostEntity) { async afterLoad(entity: PostEntity) {
if ( const sanitizeService = (await this.configure.get('content.htmlEnabled'))
(await this.configure.get('content.htmlEnabled')) && ? this.container.get(SanitizeService)
!isNil(this.sanitizeService) && : undefined;
entity.type === PostBodyType.HTML if (!isNil(sanitizeService) && entity.type === PostBodyType.HTML) {
) { entity.body = sanitizeService.sanitize(entity.body);
entity.body = this.sanitizeService.sanitize(entity.body);
} }
} }
} }

View File

@ -76,8 +76,14 @@ export async function panic(option: PanicOption | string) {
console.log(chalk.red(`\n❌ ${option}`)); console.log(chalk.red(`\n❌ ${option}`));
process.exit(1); process.exit(1);
} }
const { error, message, exit = true } = option; const { error, message, spinner, exit = true } = option;
isNil(error) ? console.log(chalk.red(`\n❌ ${message}`)) : console.log(chalk.red(error)); if (isNil(error)) {
isNil(spinner)
? console.log(chalk.red(`\n❌ ${message}`))
: spinner.succeed(chalk.red(`\n❌ ${message}`));
} else {
isNil(spinner) ? console.log(chalk.red(error)) : spinner.fail(chalk.red(error));
}
if (exit) { if (exit) {
process.exit(1); process.exit(1);
} }

View File

@ -1,4 +1,5 @@
import { Optional } from '@nestjs/common'; import { Optional } from '@nestjs/common';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { import {
DataSource, DataSource,
@ -17,6 +18,10 @@ import {
UpdateEvent, UpdateEvent,
} from 'typeorm'; } from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { app } from '@/modules/core/helpers/app';
import { RepositoryType } from '../types'; import { RepositoryType } from '../types';
import { getCustomRepository } from '../utils'; import { getCustomRepository } from '../utils';
@ -36,12 +41,25 @@ export abstract class BaseSubscriber<T extends ObjectLiteral>
{ {
protected abstract entity: ObjectType<T>; protected abstract entity: ObjectType<T>;
protected constructor(@Optional() protected dataSource?: DataSource) { protected constructor(
@Optional() protected dataSource?: DataSource,
@Optional() protected _configure?: Configure,
) {
if (!isNil(this.dataSource)) { if (!isNil(this.dataSource)) {
this.dataSource.subscribers.push(this); this.dataSource.subscribers.push(this);
} }
} }
get configure() {
return isNil(this._configure)
? this.container.get(Configure, { strict: false })
: this._configure;
}
get container(): NestFastifyApplication {
return app.container;
}
protected getDataSource(event: SubscriberEvent<T>) { protected getDataSource(event: SubscriberEvent<T>) {
return this.dataSource ?? event.connection; return this.dataSource ?? event.connection;
} }

View File

@ -1,3 +1,5 @@
import { resolve } from 'path';
import { ConfigureFactory, ConfigureRegister } from '../config/types'; import { ConfigureFactory, ConfigureRegister } from '../config/types';
import { createConnectionOptions } from '../config/utils'; import { createConnectionOptions } from '../config/utils';
import { deepMerge } from '../core/helpers'; import { deepMerge } from '../core/helpers';
@ -18,7 +20,11 @@ export const createDBConfig: (
export const createDBOptions = (options: DBConfig) => { export const createDBOptions = (options: DBConfig) => {
const newOptions: DBOptions = { const newOptions: DBOptions = {
common: deepMerge( common: deepMerge(
{ charset: 'utf8mb4', logging: ['error'] }, {
charset: 'utf8mb4',
logging: ['error'],
paths: { migration: resolve(__dirname, '../../database/migrations') },
},
options.common ?? {}, options.common ?? {},
'replace', 'replace',
), ),
@ -29,7 +35,7 @@ export const createDBOptions = (options: DBConfig) => {
const newOption = { ...connection, entities }; const newOption = { ...connection, entities };
return deepMerge( return deepMerge(
newOptions.common, newOptions.common,
{ ...newOption, autoLoadEntities: true } as any, { ...newOption, autoLoadEntities: true, synchronize: false } as any,
'replace', 'replace',
) as TypeormOption; ) as TypeormOption;
}); });

View File

@ -2,8 +2,8 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { import {
FindTreeOptions, FindTreeOptions,
ObjectLiteral, ObjectLiteral,
SelectQueryBuilder,
Repository, Repository,
SelectQueryBuilder,
TreeRepository, TreeRepository,
} from 'typeorm'; } from 'typeorm';
@ -70,13 +70,21 @@ export type RepositoryType<T extends ObjectLiteral> =
| BaseTreeRepository<T>; | BaseTreeRepository<T>;
export type DBConfig = { export type DBConfig = {
common: RecordAny; common: RecordAny & DBAdditionalOption;
connections: Array<TypeOrmModuleOptions & { name?: string }>; connections: Array<TypeOrmModuleOptions & { name?: string }>;
}; };
export type TypeormOption = Omit<TypeOrmModuleOptions, 'name' | 'migrations'> & { name: string }; export type TypeormOption = Omit<TypeOrmModuleOptions, 'name' | 'migrations'> & {
name: string;
} & DBAdditionalOption;
export type DBOptions = RecordAny & { export type DBOptions = RecordAny & {
common: RecordAny; common: RecordAny;
connections: TypeormOption[]; connections: TypeormOption[];
}; };
type DBAdditionalOption = {
path?: {
migration?: string;
};
};

View File

@ -1,7 +1,16 @@
import { Type } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
import { isArray, isNil } from 'lodash'; import { isArray, isNil } from 'lodash';
import { DataSource, ObjectLiteral, ObjectType, Repository, SelectQueryBuilder } from 'typeorm'; import { DataSource, ObjectLiteral, ObjectType, Repository, SelectQueryBuilder } from 'typeorm';
import { OrderQueryType, PaginateOptions, PaginateReturn } from '@/modules/database/types'; import { Configure } from '@/modules/config/configure';
import {
DBOptions,
OrderQueryType,
PaginateOptions,
PaginateReturn,
} from '@/modules/database/types';
import { CUSTOM_REPOSITORY_METADATA } from './constants'; import { CUSTOM_REPOSITORY_METADATA } from './constants';
@ -97,3 +106,48 @@ export const getCustomRepository = <P extends Repository<T>, T extends ObjectLit
const base = dataSource.getRepository<ObjectType<any>>(entity); const base = dataSource.getRepository<ObjectType<any>>(entity);
return new Repo(base.target, base.manager, base.queryRunner) as P; return new Repo(base.target, base.manager, base.queryRunner) as P;
}; };
export const addEntities = async (
configure: Configure,
entities: EntityClassOrSchema[] = [],
dataSource = 'default',
) => {
const database = await configure.get<DBOptions>('database');
if (isNil(database)) {
throw new Error('Database not exists');
}
const dbConfig = database.connections.find(({ name }) => name === dataSource);
if (isNil(dbConfig)) {
throw new Error(`Database connection ${dataSource} not exists`);
}
const oldEntities = (dbConfig.entities ?? []) as ObjectLiteral[];
const newEntities = database.connections.map((conn) =>
conn.name === dataSource ? { ...conn, entities: [...oldEntities, ...entities] } : conn,
);
configure.set('database.connections', newEntities);
return TypeOrmModule.forFeature(entities, dataSource);
};
export async function addSubscribers(
configure: Configure,
subscribers: Type<any>[] = [],
dataSource = 'default',
) {
const database = await configure.get<DBOptions>('database');
if (isNil(database)) {
throw new Error('Database not exists');
}
const dbConfig = database.connections.find(({ name }) => name === dataSource);
if (isNil(dbConfig)) {
throw new Error(`Database connection ${dataSource} not exists`);
}
const oldSubscribers = (dbConfig.subscribers ?? []) as any[];
const newSubscribers = database.connections.map((conn) =>
conn.name === dataSource
? { ...conn, subscribers: [...oldSubscribers, subscribers] }
: conn,
);
configure.set('database.connections', newSubscribers);
return subscribers;
}