import * as fakerjs from '@faker-js/faker'; 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 { Ora } from 'ora'; import { DataSource, DataSourceOptions, EntityManager, EntityTarget, ObjectLiteral, ObjectType, Repository, SelectQueryBuilder, } from 'typeorm'; import { Configure } from '@/modules/config/configure'; import { AppConfig } from '@/modules/core/types'; import { DBFactoryBuilder, FactoryOptions, Seeder, SeederConstructor, SeederOptions, } from '@/modules/database/commands/types'; import { DataFactory } from '@/modules/database/resolver/data.factory'; import { DBOptions, DefineFactory, OrderQueryType, PaginateOptions, PaginateReturn, TypeormOption, } from '@/modules/database/types'; import { CUSTOM_REPOSITORY_METADATA } from './constants'; export const paginate = async ( qb: SelectQueryBuilder, options: PaginateOptions, ): Promise> => { const limit = isNil(options.limit) || options.limit < 1 ? 1 : options.limit; const page = isNil(options.page) || options.page < 1 ? 1 : options.page; const start = page >= 1 ? page - 1 : 0; const totalItems = await qb.getCount(); qb.take(limit).skip(start * limit); const items = await qb.getMany(); const totalPages = totalItems % limit === 0 ? Math.floor(totalItems / limit) : Math.floor(totalItems / limit) + 1; const remainder = totalItems % limit === 0 ? limit : totalItems % limit; const itemCount = page < totalPages ? limit : remainder; return { items, meta: { totalItems, itemCount, perPage: limit, totalPages, currentPage: page, }, }; }; export function treePaginate( options: PaginateOptions, data: T[], ): PaginateReturn { const { page, limit } = options; let items: T[] = []; const totalItems = data.length; const totalRst = totalItems / limit; const totalPages = totalRst > Math.floor(totalRst) ? Math.floor(totalRst) + 1 : Math.floor(totalRst); let itemCount = 0; if (page <= totalPages) { itemCount = page === totalPages ? totalItems - (totalPages - 1) * limit : limit; const start = (page - 1) * limit; items = data.slice(start, start + itemCount); } return { meta: { itemCount, totalItems, perPage: limit, totalPages, currentPage: page, }, items, }; } export const getOrderByQuery = ( qb: SelectQueryBuilder, alias: string, orderBy?: OrderQueryType, ) => { if (isNil(orderBy)) { return qb; } if (typeof orderBy === 'string') { return qb.orderBy(`${alias}.${orderBy}`, 'DESC'); } if (isArray(orderBy)) { for (const item of orderBy) { typeof item === 'string' ? qb.addOrderBy(`${alias}.${item}`, 'DESC') : qb.addOrderBy(`${alias}.${item.name}`, item.order); } return qb; } return qb.orderBy(`${alias}.${(orderBy as any).name}`, (orderBy as any).order); }; export const getCustomRepository =

, T extends ObjectLiteral>( dataSource: DataSource, Repo: ClassType

, ): P => { if (isNil(Repo)) { return null; } const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo); if (!entity) { return null; } const base = dataSource.getRepository>(entity); 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('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[] = [], dataSource = 'default', ) { const database = await configure.get('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; } /** * 忽略外键 * @param em EntityManager实例 * @param type 数据库类型 * @param disabled 是否禁用外键 */ export async function resetForeignKey( em: EntityManager, type = 'mysql', disabled = true, ): Promise { let key: string; let query: string; if (type === 'sqlite') { key = disabled ? 'OFF' : 'ON'; query = `PRAGMA foreign_keys = ${key}`; } else { key = disabled ? '0' : '1'; query = `SET FOREIGN_KEY_CHECKS = ${key}`; } await em.query(query); return em; } /** * 数据填充函数 * @param Clazz 填充类 * @param args 填充命令参数 * @param spinner Ora雪碧图标 * @param configure 配置对象 * @param dbConfig 当前数据库连接池的配置 */ export async function runSeeder( Clazz: SeederConstructor, args: SeederOptions, spinner: Ora, configure: Configure, dbConfig: TypeormOption, ): Promise { const seeder: Seeder = new Clazz(spinner, args); const dataSource: DataSource = new DataSource({ ...dbConfig } as DataSourceOptions); await dataSource.initialize(); const factoryMaps: FactoryOptions = {}; for (const factory of dbConfig.factories) { const { entity, handler } = factory(); factoryMaps[entity.name] = { entity, handler }; } if (typeof args.transaction === 'boolean' && !args.transaction) { const em = await resetForeignKey(dataSource.manager, dataSource.options.type); await seeder.load({ dataSource, em, configure, connection: args.connection ?? 'default', ignoreLock: args.ignorelock, factory: factoryBuilder(configure, dataSource, factoryMaps), factories: factoryMaps, }); await resetForeignKey(em, dataSource.options.type, false); } else { // 在事务中运行 const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const em = await resetForeignKey(dataSource.manager, dataSource.options.type); await seeder.load({ dataSource, em, configure, connection: args.connection ?? 'default', ignoreLock: args.ignorelock, factory: factoryBuilder(configure, dataSource, factoryMaps), factories: factoryMaps, }); await resetForeignKey(em, dataSource.options.type, false); await queryRunner.commitTransaction(); } catch (e) { console.error(e); await queryRunner.rollbackTransaction(); } finally { await queryRunner.release(); } } if (dataSource && dataSource.isInitialized) { await dataSource.destroy(); } return dataSource; } /** * 定义factory用于生成数据 * @param entity * @param handler */ export const defineFactory: DefineFactory = (entity, handler) => () => ({ entity, handler }); /** * 获取Entity类名 * @param entity */ export function entityName(entity: EntityTarget): string { if (isNil(entity)) { throw new Error('Entity is not defined'); } if (entity instanceof Function) { return entity.name; } return new (entity as any)().constructor.name; } export const factoryBuilder: DBFactoryBuilder = (configure, dataSource, factories) => (entity) => (settings) => { const name = entityName(entity); if (!factories[name]) { throw new Error(`has none factory for entity named ${name}`); } return new DataFactory( name, configure, entity, dataSource.createEntityManager(), factories[name].handler, settings, ); }; /** * 本地化假数据 * @param configure */ export async function getFakerLocales(configure: Configure) { const app = await configure.get('app'); const locales: fakerjs.LocaleDefinition[] = []; const locale = app.locale as keyof typeof fakerjs; const fallbackLocale = app.fallbackLocale as keyof typeof fakerjs; if (!isNil(fakerjs[locale])) { locales.push(fakerjs[locale] as fakerjs.LocaleDefinition); } if (!isNil(fakerjs[fallbackLocale])) { locales.push(fakerjs[fallbackLocale] as fakerjs.LocaleDefinition); } return locales; }