Skip to content

Commit

Permalink
feat: option to execute seeds only once
Browse files Browse the repository at this point in the history
  • Loading branch information
tada5hi committed Jul 15, 2023
1 parent 762a07d commit 5f6d98f
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 14 deletions.
8 changes: 3 additions & 5 deletions src/cli/commands/seed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Arguments, Argv, CommandModule } from 'yargs';
import { buildDataSourceOptions, setDataSourceOptions, useDataSource } from '../../data-source';
import { runSeeders } from '../../seeder';
import { SeederExecutor } from '../../seeder/executor';
import { CodeTransformation, setCodeTransformation } from '../../utils';

export interface DatabaseSeedArguments extends Arguments {
Expand Down Expand Up @@ -53,10 +53,8 @@ export class SeedCommand implements CommandModule {
setDataSourceOptions(dataSourceOptions);

const dataSource = await useDataSource();

await runSeeders(dataSource, {
seedName: args.seed,
});
const executor = new SeederExecutor(dataSource);
await executor.execute(args.seed);

if (exitProcess) {
process.exit(0);
Expand Down
53 changes: 53 additions & 0 deletions src/seeder/entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Seeder, SeederConstructor } from './type';

export class SeederEntity {
/**
* Seeder id.
* Indicates order of the executed migrations.
*/
id?: number;

/**
* Timestamp of the seed.
*/
timestamp: number;

/**
* Name of the seed (class name).
*/
name: string;

/**
* Constructor that needs to be run.
*/
instance?: Seeder;

/**
* File name of the seeder.
*/
fileName?: string;

constructor(ctx: {
id?: number,
timestamp: number,
name: string,
constructor?: SeederConstructor,
fileName?: string
}) {
this.id = ctx.id;
this.timestamp = ctx.timestamp;
this.name = ctx.name;

if (ctx.constructor) {
this.instance = new ctx.constructor();
}

this.fileName = ctx.fileName;
}

isOneTime() {
return this.instance &&
typeof this.instance.oneTimeOnly === 'boolean' &&
this.instance.oneTimeOnly;
}
}
302 changes: 302 additions & 0 deletions src/seeder/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import type { ObjectLiteral } from 'rapiq';
import { MssqlParameter, Table } from 'typeorm';
import type { DataSource, DataSourceOptions, QueryRunner } from 'typeorm';
import type { MongoQueryRunner } from 'typeorm/driver/mongodb/MongoQueryRunner';
import { setDataSource } from '../data-source';
import { SeederEntity } from './entity';
import { useSeederFactoryManager } from './factory';
import { prepareSeeder } from './module';
import type { SeederOptions } from './type';

export class SeederExecutor {
protected dataSource : DataSource;

private readonly tableName: string;

constructor(dataSource: DataSource) {
this.dataSource = dataSource;

setDataSource(dataSource);

this.tableName = this.options.seedTableName || 'seeds';
}

async execute(seedName?: string) : Promise<SeederEntity[]> {
const queryRunner = this.dataSource.createQueryRunner();
await this.createTableIfNotExist(queryRunner);

const existing = await this.loadExisting(queryRunner);
const all = await this.loadAll(seedName);

const pending = all.filter((seed) => {
const index = existing.findIndex(
(el) => el.name === seed.name,
);

return index === -1 || !seed.isOneTime();
});

if (pending.length === 0) {
await queryRunner.release();

return [];
}

this.dataSource.logger.logSchemaBuild(
`${existing.length} seeds are already present in the database.`,
);
this.dataSource.logger.logSchemaBuild(
`${all.length} seeds were found in the source code.`,
);

const factoryManager = useSeederFactoryManager();

const executed : SeederEntity[] = [];

try {
for (let i = 0; i < pending.length; i++) {
const seeder = pending[i].instance;
if (!seeder) {
continue;
}

await seeder.run(this.dataSource, factoryManager);

await this.track(queryRunner, pending[i]);

this.dataSource.logger.logSchemaBuild(
`Migration ${pending[i].name} has been executed successfully.`,
);

executed.push(pending[i]);
}
} finally {
await queryRunner.release();
}

return executed;
}

protected async loadExisting(queryRunner: QueryRunner) : Promise<SeederEntity[]> {
if (this.dataSource.driver.options.type === 'mongodb') {
const mongoRunner = queryRunner as MongoQueryRunner;

return mongoRunner
.cursor(this.tableName, {})
.sort({ _id: -1 })
.toArray();
}

const raw: ObjectLiteral[] = await this.dataSource.manager
.createQueryBuilder(queryRunner)
.select()
.orderBy(this.dataSource.driver.escape('id'), 'DESC')
.from(this.table, this.tableName)
.getRawMany();

return raw.map((migrationRaw) => new SeederEntity({
id: parseInt(migrationRaw.id, 10),
timestamp: parseInt(migrationRaw.timestamp, 10),
name: migrationRaw.name,
constructor: undefined,
}));
}

/**
* Gets all migrations that setup for this connection.
*/
protected async loadAll(seedName?: string): Promise<SeederEntity[]> {
if (!this.options.seeds) {
return [];
}

const seeds = await prepareSeeder({
...this.options,
...(seedName ? { seedName } : {}),
});

const timestampFallback = Date.now();
let timestampCounter = 0;
const entities = seeds.map((element) => {
const {
constructor: seed,
fileName,
} = element;

let {
timestamp,
} = element;

const className = seed.name || (seed.constructor as any).name;

if (!timestamp) {
const match = className.match(/^(.*)([0-9]{13,})$/);
if (match) {
[,, timestamp] = match;
}
}

const entity = new SeederEntity({
fileName,
timestamp: timestamp || timestampFallback + timestampCounter,
name: className,
constructor: seed,
});

timestampCounter++;

return entity;
});

this.checkForDuplicateMigrations(entities);

// sort them by timestamp
return entities.sort((a, b) => {
if (
typeof a.fileName !== 'undefined' &&
typeof b.fileName !== 'undefined'
) {
return a.name > b.name ? 1 : -1;
}

if (
typeof a.timestamp !== 'undefined' &&
typeof b.timestamp !== 'undefined'
) {
return a.timestamp - b.timestamp;
}

return 1;
});
}

protected checkForDuplicateMigrations(entities: SeederEntity[]) {
const names = entities.map((migration) => migration.name);
const duplicates = Array.from(
new Set(
names.filter(
(migrationName, index) => names.indexOf(migrationName) < index,
),
),
);
if (duplicates.length > 0) {
throw Error(`Duplicate seeds: ${duplicates.join(', ')}`);
}
}

protected async createTableIfNotExist(queryRunner: QueryRunner) {
// If driver is mongo no need to create
if (this.dataSource.driver.options.type === 'mongodb') {
return;
}
const tableExist = await queryRunner.hasTable(this.table);
if (!tableExist) {
await queryRunner.createTable(
new Table({
database: this.database,
schema: this.schema,
name: this.table,
columns: [
{
name: 'id',
type: this.dataSource.driver.normalizeType({
type: this.dataSource.driver.mappedDataTypes
.migrationId,
}),
isGenerated: true,
generationStrategy: 'increment',
isPrimary: true,
isNullable: false,
},
{
name: 'timestamp',
type: this.dataSource.driver.normalizeType({
type: this.dataSource.driver.mappedDataTypes
.migrationTimestamp,
}),
isPrimary: false,
isNullable: false,
},
{
name: 'name',
type: this.dataSource.driver.normalizeType({
type: this.dataSource.driver.mappedDataTypes
.migrationName,
}),
isNullable: false,
},
],
}),
);
}
}

protected getLatest(
migrations: SeederEntity[],
): SeederEntity | undefined {
const entities = migrations
.map((migration) => migration)
.sort((a, b) => (a.timestamp - b.timestamp) * -1);
return entities.length > 0 ? entities[0] : undefined;
}

protected async track(
queryRunner: QueryRunner,
migration: SeederEntity,
): Promise<void> {
const values: ObjectLiteral = {};
if (this.dataSource.driver.options.type === 'mssql') {
values.timestamp = new MssqlParameter(
migration.timestamp,
this.dataSource.driver.normalizeType({
type: this.dataSource.driver.mappedDataTypes
.migrationTimestamp,
}) as any,
);
values.name = new MssqlParameter(
migration.name,
this.dataSource.driver.normalizeType({
type: this.dataSource.driver.mappedDataTypes.migrationName,
}) as any,
);
} else {
values.timestamp = migration.timestamp;
values.name = migration.name;
}

if (this.dataSource.driver.options.type === 'mongodb') {
const mongoRunner = queryRunner as MongoQueryRunner;
await mongoRunner.databaseConnection
.db(this.dataSource.driver.database)
.collection(this.tableName)
.insertOne(values);
} else {
const qb = queryRunner.manager.createQueryBuilder();
await qb
.insert()
.into(this.table)
.values(values)
.execute();
}
}

protected get options() : DataSourceOptions & SeederOptions {
return this.dataSource.options;
}

protected get database() {
return this.dataSource.driver.database;
}

protected get schema() {
return this.dataSource.driver.schema;
}

protected get table() {
return this.dataSource.driver.buildTableName(
this.tableName,
this.schema,
this.database,
);
}
}
Loading

0 comments on commit 5f6d98f

Please sign in to comment.