Skip to content

Commit

Permalink
feat: add generate migraiton helper utility
Browse files Browse the repository at this point in the history
  • Loading branch information
tada5hi committed Mar 25, 2023
1 parent 5ef4d74 commit c9e3736
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 5 deletions.
59 changes: 55 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"dependencies": {
"@faker-js/faker": "^7.6.0",
"locter": "^1.0.9",
"pascal-case": "^3.1.2",
"rapiq": "^0.8.0",
"reflect-metadata": "^0.1.13",
"yargs": "^17.7.1"
Expand Down
2 changes: 2 additions & 0 deletions src/database/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './context';
export * from './migration';
export * from './query';
export * from './schema';
export * from './type';
129 changes: 129 additions & 0 deletions src/database/utils/migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { pascalCase } from 'pascal-case';
import path from 'node:path';
import fs from 'node:fs';
import process from 'node:process';
import { MigrationGenerateCommand } from 'typeorm/commands/MigrationGenerateCommand';
import type { MigrationGenerateCommandContext, MigrationGenerateResult } from './type';

class GenerateCommand extends MigrationGenerateCommand {
static prettify(query: string) {
return this.prettifyQuery(query);
}
}

function queryParams(parameters: any[] | undefined): string {
if (!parameters || !parameters.length) {
return '';
}

return `, ${JSON.stringify(parameters)}`;
}

function buildTemplate(
name: string,
timestamp: number,
upStatements: string[],
downStatements: string[],
): string {
const migrationName = `${pascalCase(name)}${timestamp}`;

const up = upStatements.map((statement) => ` ${statement}`);
const down = downStatements.map((statement) => ` ${statement}`);

return `import { MigrationInterface, QueryRunner } from 'typeorm';
export class ${migrationName} implements MigrationInterface {
name = '${migrationName}';
public async up(queryRunner: QueryRunner): Promise<void> {
${up.join(`
`)}
}
public async down(queryRunner: QueryRunner): Promise<void> {
${down.join(`
`)}
}
}
`;
}

export async function generateMigration(
context: MigrationGenerateCommandContext,
) : Promise<MigrationGenerateResult> {
context.name = context.name || 'Default';

const timestamp = context.timestamp || new Date().getTime();
const fileName = `${timestamp}-${context.name}.ts`;

const { dataSource } = context;

const up: string[] = []; const
down: string[] = [];

if (!dataSource.isInitialized) {
await dataSource.initialize();
}

const sqlInMemory = await dataSource.driver.createSchemaBuilder().log();

if (context.prettify) {
sqlInMemory.upQueries.forEach((upQuery) => {
upQuery.query = GenerateCommand.prettify(
upQuery.query,
);
});
sqlInMemory.downQueries.forEach((downQuery) => {
downQuery.query = GenerateCommand.prettify(
downQuery.query,
);
});
}

sqlInMemory.upQueries.forEach((upQuery) => {
up.push(`await queryRunner.query(\`${upQuery.query.replace(/`/g, '\\`')}\`${queryParams(upQuery.parameters)});`);
});

sqlInMemory.downQueries.forEach((downQuery) => {
down.push(`await queryRunner.query(\`${downQuery.query.replace(/`/g, '\\`')}\`${queryParams(downQuery.parameters)});`);
});

await dataSource.destroy();

if (
up.length === 0 &&
down.length === 0
) {
return { up, down };
}

const content = buildTemplate(context.name, timestamp, up, down.reverse());

if (!context.preview) {
let directoryPath : string;
if (context.directoryPath) {
if (!path.isAbsolute(context.directoryPath)) {
directoryPath = path.join(process.cwd(), context.directoryPath);
} else {
directoryPath = context.directoryPath;
}
} else {
directoryPath = path.join(process.cwd(), 'migrations');
}

try {
await fs.promises.access(directoryPath, fs.constants.R_OK | fs.constants.W_OK);
} catch (e) {
await fs.promises.mkdir(directoryPath, { recursive: true });
}

const filePath = path.join(directoryPath, fileName);

await fs.promises.writeFile(filePath, content, { encoding: 'utf-8' });
}

return {
up,
down,
content,
};
}
37 changes: 37 additions & 0 deletions src/database/utils/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { DataSource } from 'typeorm';

export type MigrationGenerateResult = {
up: string[],
down: string[],
content?: string
};

export type MigrationGenerateCommandContext = {
/**
* Directory where the migration(s) should be stored.
*/
directoryPath?: string,
/**
* Name of the migration class.
*/
name?: string,
/**
* DataSource used for reference of existing schema.
*/
dataSource: DataSource,

/**
* Timestamp in milliseconds.
*/
timestamp?: number,

/**
* Prettify sql statements.
*/
prettify?: boolean,

/**
* Only return up- & down-statements instead of backing up the migration to the file system.
*/
preview?: boolean
};
2 changes: 1 addition & 1 deletion test/data/typeorm/data-source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {DataSource, DataSourceOptions} from "typeorm";
import path from "path";
import path from "node:path";
import {User} from "../entity/user";
import {SeederOptions} from "../../../src";

Expand Down
31 changes: 31 additions & 0 deletions test/unit/database/migration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {DataSource, DataSourceOptions} from "typeorm";
import {generateMigration} from "../../../src";
import {User} from "../../data/entity/user";

describe('src/database/migration', () => {
it('should generate migration file', async () => {
const options : DataSourceOptions = {
type: 'better-sqlite3',
entities: [User],
database: ':memory:',
extra: {
charset: "UTF8_GENERAL_CI"
}
}
const dataSource = new DataSource(options);

const output = await generateMigration({
dataSource,
preview: true
});

expect(output).toBeDefined();
expect(output.up).toBeDefined();
expect(output.up.length).toBeGreaterThanOrEqual(1);
expect(output.up[0]).toEqual('await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "firstName" varchar NOT NULL, "lastName" varchar NOT NULL, "email" varchar NOT NULL, "foo" varchar NOT NULL)`);')

expect(output.down).toBeDefined();
expect(output.down.length).toBeGreaterThanOrEqual(1);
expect(output.down[0]).toEqual('await queryRunner.query(`DROP TABLE "user"`);')
})
})

0 comments on commit c9e3736

Please sign in to comment.