Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: impl dal forkDb #202

Merged
merged 1 commit into from
Apr 2, 2024
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
1 change: 1 addition & 0 deletions core/dal-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './src/SqlMapLoader';
export * from './src/DataSource';
export * from './src/MySqlDataSource';
export * from './src/TableModelInstanceBuilder';
export * from './src/DatabaseForker';
68 changes: 68 additions & 0 deletions core/dal-runtime/src/DatabaseForker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { DataSourceOptions } from './MySqlDataSource';
import { RDSClient } from '@eggjs/rds';
import path from 'node:path';
import fs from 'node:fs/promises';
import assert from 'node:assert';

export class DatabaseForker {
private readonly env: string;
private readonly options: DataSourceOptions;

constructor(env: string, options: DataSourceOptions) {
this.env = env;
this.options = options;
}

shouldFork() {
return this.env === 'unittest' && this.options.forkDb;
}

async forkDb(dalDir: string) {
assert(this.shouldFork(), 'fork db only run in unittest');
// 尽早判断不应该 fork,避免对 rds pool 配置造成污染
try {
await fs.access(dalDir);
} catch (_) {
return;

Check warning on line 26 in core/dal-runtime/src/DatabaseForker.ts

View check run for this annotation

Codecov / codecov/patch

core/dal-runtime/src/DatabaseForker.ts#L26

Added line #L26 was not covered by tests
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name, initSql, forkDb, database, ...mysqlOptions } = this.options;
const client = new RDSClient(Object.assign(mysqlOptions));
const conn = await client.getConnection();
await this.doCreateUtDb(conn);
await this.forkTables(conn, dalDir);
conn.release();
await client.end();
}

private async forkTables(conn, dalDir: string) {
const sqlDir = path.join(dalDir, 'structure');
const structureFiles = await fs.readdir(sqlDir);
const sqlFiles = structureFiles.filter(t => t.endsWith('.sql'));
for (const sqlFile of sqlFiles) {
await this.doForkTable(conn, path.join(sqlDir, sqlFile));
}
}

private async doForkTable(conn, sqlFileName: string) {
const sqlFile = await fs.readFile(sqlFileName, 'utf8');
const sqls = sqlFile.split(';').filter(t => !!t.trim());
for (const sql of sqls) {
await conn.query(sql);
}
}

private async doCreateUtDb(conn) {
await conn.query(`CREATE DATABASE IF NOT EXISTS ${this.options.database};`);
await conn.query(`use ${this.options.database};`);
}

async destroy() {
assert(this.shouldFork(), 'fork db only run in unittest');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name, initSql, forkDb, database, ...mysqlOptions } = this.options;
const client = new RDSClient(Object.assign(mysqlOptions));
await client.query(`DROP DATABASE ${database}`);
await client.end();
}
}
14 changes: 9 additions & 5 deletions core/dal-runtime/src/MySqlDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { RDSClient } from '@eggjs/rds';
// TODO fix export
import type { RDSClientOptions } from '@eggjs/rds/lib/types';
import type { RDSClientOptions } from '@eggjs/rds';
import Base from 'sdk-base';

export interface DataSourceOptions extends RDSClientOptions {
name: string;
// default is select 1 + 1;
initSql?: string;
forkDb?: boolean;
}

const DEFAULT_OPTIONS: RDSClientOptions = {
Expand All @@ -16,18 +16,22 @@ const DEFAULT_OPTIONS: RDSClientOptions = {
};

export class MysqlDataSource extends Base {
private readonly client: RDSClient;
private client: RDSClient;
private readonly initSql: string;
readonly name: string;
readonly timezone?: string;
readonly rdsOptions: RDSClientOptions;
readonly forkDb?: boolean;

constructor(options: DataSourceOptions) {
super({ initMethod: '_init' });
const { name, initSql, ...mysqlOptions } = options;
this.client = new RDSClient(Object.assign({}, DEFAULT_OPTIONS, mysqlOptions));
const { name, initSql, forkDb, ...mysqlOptions } = options;
this.forkDb = forkDb;
this.initSql = initSql ?? 'SELECT 1 + 1';
this.name = name;
this.timezone = options.timezone;
this.rdsOptions = Object.assign({}, DEFAULT_OPTIONS, mysqlOptions);
this.client = new RDSClient(this.rdsOptions);
}

protected async _init() {
Expand Down
25 changes: 14 additions & 11 deletions core/dal-runtime/test/DAO.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,40 @@ import { Foo } from './fixtures/modules/dal/Foo';
import { TableModel } from '@eggjs/dal-decorator';
import path from 'node:path';
import { DataSource } from '../src/DataSource';
import { SqlGenerator } from '../src/SqlGenerator';
import FooDAO from './fixtures/modules/dal/dal/dao/FooDAO';
import { DatabaseForker } from '../src/DatabaseForker';

describe('test/DAO.test.ts', () => {
let dataSource: DataSource<Foo>;
let tableModel: TableModel<Foo>;
let forker: DatabaseForker;

before(async () => {
const mysql = new MysqlDataSource({
const mysqlOptions = {
name: 'foo',
host: '127.0.0.1',
user: 'root',
database: 'test',
database: 'test_runtime',
timezone: '+08:00',
initSql: 'SET GLOBAL time_zone = \'+08:00\';',
});
forkDb: true,
};
forker = new DatabaseForker('unittest', mysqlOptions);
await forker.forkDb(path.join(__dirname, './fixtures/modules/dal/dal'));

const mysql = new MysqlDataSource(mysqlOptions);
await mysql.ready();
await mysql.query('DROP TABLE IF EXISTS egg_foo');

tableModel = TableModel.build(Foo);

const sqlGenerator = new SqlGenerator();
const createTableSql = sqlGenerator.generate(tableModel);

await mysql.query(createTableSql);

const sqlMapLoader = new SqlMapLoader(tableModel, path.join(__dirname, './fixtures/modules/dal'), console as any);
const sqlMap = sqlMapLoader.load();
dataSource = new DataSource(tableModel, mysql, sqlMap);
});

after(async () => {
await forker.destroy();
});

it('execute should work', async () => {
const foo = new Foo();
foo.name = 'name';
Expand Down
24 changes: 13 additions & 11 deletions core/dal-runtime/test/DataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,38 @@ import path from 'node:path';
import { DataSource } from '../src/DataSource';
import { TableModelInstanceBuilder } from '../src/TableModelInstanceBuilder';
import { DeleteResult, InsertResult, UpdateResult } from '@eggjs/rds/lib/types';
import { SqlGenerator } from '../src/SqlGenerator';
import { DatabaseForker } from '../src/DatabaseForker';

describe('test/Datasource.test.ts', () => {
let dataSource: DataSource<Foo>;
let tableModel: TableModel<Foo>;
let forker: DatabaseForker;

before(async () => {
const mysql = new MysqlDataSource({
const mysqlOptions = {
name: 'foo',
host: '127.0.0.1',
user: 'root',
database: 'test',
database: 'test_runtime',
timezone: '+08:00',
initSql: 'SET GLOBAL time_zone = \'+08:00\';',
});
forkDb: true,
};
forker = new DatabaseForker('unittest', mysqlOptions);
await forker.forkDb(path.join(__dirname, './fixtures/modules/dal/dal'));
const mysql = new MysqlDataSource(mysqlOptions);
await mysql.ready();
await mysql.query('DROP TABLE IF EXISTS egg_foo');

tableModel = TableModel.build(Foo);

const sqlGenerator = new SqlGenerator();
const createTableSql = sqlGenerator.generate(tableModel);

await mysql.query(createTableSql);

const sqlMapLoader = new SqlMapLoader(tableModel, path.join(__dirname, './fixtures/modules/dal'), console as any);
const sqlMap = sqlMapLoader.load();
dataSource = new DataSource(tableModel, mysql, sqlMap);
});

after(async () => {
await forker.destroy();
});

it('execute should work', async () => {
const foo = new Foo();
foo.name = 'name';
Expand Down
12 changes: 12 additions & 0 deletions plugin/dal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,15 @@ dataSource:
```sql
SELECT @@GLOBAL.time_zone;
```

## Unittest
可以在 `module.yml` 中开启 forkDb 配置,即可实现 unittest 环境自动创建数据库

```yaml
# module.yml
dataSource:
foo:
# 开启 ci 环境自动创建数据库
forkDb: true

```
2 changes: 1 addition & 1 deletion plugin/dal/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default class ControllerAppBootHook {
}

configWillLoad() {
this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.app.moduleConfigs);
this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.app.config.env, this.app.moduleConfigs);
this.dalTableEggPrototypeHook = new DalTableEggPrototypeHook(this.app.logger);
this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.dalTableEggPrototypeHook);
this.app.loadUnitLifecycleUtil.registerLifecycle(this.dalModuleLoadUnitHook);
Expand Down
21 changes: 15 additions & 6 deletions plugin/dal/lib/DalModuleLoadUnitHook.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { MysqlDataSourceManager } from './MysqlDataSourceManager';
import path from 'node:path';
import { LifecycleHook } from '@eggjs/tegg-lifecycle';
import { ModuleConfigHolder } from '@eggjs/tegg-common-util';
import { DataSourceOptions } from '@eggjs/dal-runtime';
import { DatabaseForker, DataSourceOptions } from '@eggjs/dal-runtime';
import { LoadUnit, LoadUnitLifecycleContext } from '@eggjs/tegg/helper';

export class DalModuleLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
private readonly moduleConfigs: Record<string, ModuleConfigHolder>;
private readonly env: string;

constructor(moduleConfigs: Record<string, ModuleConfigHolder>) {
constructor(env: string, moduleConfigs: Record<string, ModuleConfigHolder>) {
this.env = env;
this.moduleConfigs = moduleConfigs;
}

Expand All @@ -17,11 +20,17 @@ export class DalModuleLoadUnitHook implements LifecycleHook<LoadUnitLifecycleCon
const dataSourceConfig: Record<string, DataSourceOptions> | undefined = (moduleConfigHolder.config as any).dataSource;
if (!dataSourceConfig) return;
await Promise.all(Object.entries(dataSourceConfig).map(async ([ name, config ]) => {
const dataSourceOptions = {
...config,
name,
};
const forker = new DatabaseForker(this.env, dataSourceOptions);
if (forker.shouldFork()) {
await forker.forkDb(path.join(loadUnit.unitPath, 'dal'));
}

try {
await MysqlDataSourceManager.instance.createDataSource(loadUnit.name, name, {
...config,
name,
});
await MysqlDataSourceManager.instance.createDataSource(loadUnit.name, name, dataSourceOptions);
} catch (e) {
e.message = `create module ${loadUnit.name} datasource ${name} failed: ` + e.message;
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = function() {
security: {
csrf: {
ignoreJSON: false,
}
},
},
};
return config;
Expand Down
3 changes: 2 additions & 1 deletion plugin/dal/test/fixtures/apps/dal-app/modules/dal/module.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
dataSource:
foo:
connectionLimit: 100
database: 'test'
database: 'test_dal_plugin'
host: '127.0.0.1'
user: root
port: 3306
timezone: '+08:00'
forkDb: true
2 changes: 1 addition & 1 deletion standalone/standalone/src/Runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class Runner {
this.loadUnitMultiInstanceProtoHook = new LoadUnitMultiInstanceProtoHook();
LoadUnitLifecycleUtil.registerLifecycle(this.loadUnitMultiInstanceProtoHook);

this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.moduleConfigs);
this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.env ?? '', this.moduleConfigs);
const loggerInnerObject = this.innerObjects.logger && this.innerObjects.logger[0];
const logger = loggerInnerObject?.obj || console;
this.dalTableEggPrototypeHook = new DalTableEggPrototypeHook(logger as Logger);
Expand Down
3 changes: 2 additions & 1 deletion standalone/standalone/test/fixtures/dal-module/module.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
dataSource:
foo:
connectionLimit: 100
database: 'test'
database: 'test_dal_standalone'
host: '127.0.0.1'
user: root
port: 3306
timezone: '+08:00'
forkDb: true
32 changes: 3 additions & 29 deletions standalone/standalone/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { ModuleConfigs } from '@eggjs/tegg';
import { ModuleConfig } from 'egg';
import { crosscutAdviceParams, pointcutAdviceParams } from './fixtures/aop-module/Hello';
import { Foo } from './fixtures/dal-module/Foo';
import { MysqlDataSource, SqlGenerator } from '@eggjs/dal-runtime';
import { TableModel } from '@eggjs/dal-decorator';

describe('test/index.test.ts', () => {
describe('simple runner', () => {
Expand Down Expand Up @@ -217,34 +215,10 @@ describe('test/index.test.ts', () => {
});

describe('dal runner', () => {
let mysqlDataSource: MysqlDataSource;

before(async () => {
mysqlDataSource = new MysqlDataSource({
name: 'foo',
host: '127.0.0.1',
user: 'root',
database: 'test',
timezone: '+08:00',
initSql: 'SET GLOBAL time_zone = \'+08:00\';',
});
await mysqlDataSource.ready();
await mysqlDataSource.query('DROP TABLE IF EXISTS egg_foo');

const tableModel = TableModel.build(Foo);

const sqlGenerator = new SqlGenerator();
const createTableSql = sqlGenerator.generate(tableModel);

await mysqlDataSource.query(createTableSql);
});

after(async () => {
await mysqlDataSource.query('DROP TABLE IF EXISTS egg_foo');
});

it('should work', async () => {
const foo: Foo = await main(path.join(__dirname, './fixtures/dal-module'));
const foo: Foo = await main(path.join(__dirname, './fixtures/dal-module'), {
env: 'unittest',
});
assert(foo);
assert.equal(foo.col1, '2333');
});
Expand Down
Loading