feat: implement Migration module with controller, service, and entity, including migration creation logic
This commit is contained in:
@ -5,6 +5,7 @@ import { QueryModule } from "src/query/query.module";
|
|||||||
import { ProjectModule } from "src/project/project.module";
|
import { ProjectModule } from "src/project/project.module";
|
||||||
import { RedisModule } from "src/redis/redis.module";
|
import { RedisModule } from "src/redis/redis.module";
|
||||||
import { DatabaseModule } from "src/database/database.module";
|
import { DatabaseModule } from "src/database/database.module";
|
||||||
|
import { MigrationModule } from "src/migration/migration.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -14,6 +15,7 @@ import { DatabaseModule } from "src/database/database.module";
|
|||||||
ProjectModule,
|
ProjectModule,
|
||||||
QueryModule,
|
QueryModule,
|
||||||
TestModule,
|
TestModule,
|
||||||
|
MigrationModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Project } from "src/project/entities/project.entity";
|
import { Project } from "../../project/entities/project.entity";
|
||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { MigrationTable } from "../migration.constants";
|
||||||
|
|
||||||
@Entity("migration")
|
@Entity("migration")
|
||||||
export class Migration {
|
export class Migration {
|
||||||
@ -21,6 +22,9 @@ export class Migration {
|
|||||||
@ManyToOne(() => Project, (project) => project.migrations)
|
@ManyToOne(() => Project, (project) => project.migrations)
|
||||||
project: Project;
|
project: Project;
|
||||||
|
|
||||||
@Column({ type: "jsonb", nullable: true })
|
@Column()
|
||||||
data: Record<string, any>;
|
sql: string;
|
||||||
|
|
||||||
|
@Column({ type: "json", nullable: true })
|
||||||
|
data: MigrationTable[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,15 @@
|
|||||||
import { Controller } from "@nestjs/common";
|
import { Body, Controller, Post } from "@nestjs/common";
|
||||||
|
import { MigrationService } from "./migration.service";
|
||||||
|
import { MigrationTable } from "./migration.constants";
|
||||||
|
|
||||||
@Controller("migrations")
|
@Controller("migrations")
|
||||||
export class MigrationController {}
|
export class MigrationController {
|
||||||
|
constructor(private readonly migrationService: MigrationService) {}
|
||||||
|
|
||||||
|
@Post("create")
|
||||||
|
async createMigration(
|
||||||
|
@Body() body: { token: string; tables: MigrationTable[] }
|
||||||
|
) {
|
||||||
|
return await this.migrationService.create(body.tables, body.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { forwardRef, Module } from "@nestjs/common";
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
import { Migration } from "./entities/migration.entity";
|
import { Migration } from "./entities/migration.entity";
|
||||||
import { MigrationController } from "./migration.controller";
|
import { MigrationController } from "./migration.controller";
|
||||||
import { MigrationService } from "./migration.service";
|
import { MigrationService } from "./migration.service";
|
||||||
|
import { ProjectModule } from "src/project/project.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Migration])],
|
imports: [
|
||||||
|
forwardRef(() => ProjectModule),
|
||||||
|
TypeOrmModule.forFeature([Migration]),
|
||||||
|
],
|
||||||
controllers: [MigrationController],
|
controllers: [MigrationController],
|
||||||
providers: [MigrationService],
|
providers: [MigrationService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,27 +1,200 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Inject, Injectable } from "@nestjs/common";
|
||||||
import { In, Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { Migration } from "./entities/migration.entity";
|
import { Migration } from "./entities/migration.entity";
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Project } from "src/project/entities/project.entity";
|
import { MigrationTable } from "./migration.constants";
|
||||||
|
import { ProjectService } from "src/project/project.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MigrationService {
|
export class MigrationService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Migration)
|
@InjectRepository(Migration)
|
||||||
private readonly migrationRepository: Repository<Migration>
|
private readonly migrationRepository: Repository<Migration>,
|
||||||
@InjectRepository(Project)
|
@Inject(ProjectService)
|
||||||
private readonly projectRepository: Repository<Project>
|
private readonly projectService: ProjectService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(tables: MigrationTable[], projectId: string): Promise<Migration> {
|
compareTable(
|
||||||
const project = this.projectRepository.findOne({
|
oldTable: MigrationTable,
|
||||||
where: { token: projectId },
|
newTable: MigrationTable
|
||||||
|
): MigrationTable {
|
||||||
|
const changes: MigrationTable = {
|
||||||
|
name: newTable.name,
|
||||||
|
fields: {},
|
||||||
|
relations: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const fieldName in newTable.fields) {
|
||||||
|
if (!oldTable.fields[fieldName]) {
|
||||||
|
changes.fields[fieldName] = newTable.fields[fieldName];
|
||||||
|
} else {
|
||||||
|
const oldField = oldTable.fields[fieldName];
|
||||||
|
const newField = newTable.fields[fieldName];
|
||||||
|
|
||||||
|
if (
|
||||||
|
oldField.type !== newField.type ||
|
||||||
|
oldField.isNullable !== newField.isNullable ||
|
||||||
|
oldField.isUnique !== newField.isUnique ||
|
||||||
|
oldField.default !== newField.default
|
||||||
|
) {
|
||||||
|
changes.fields[fieldName] = newField;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const relationName in newTable.relations) {
|
||||||
|
if (!oldTable.relations || !oldTable.relations[relationName]) {
|
||||||
|
changes.relations[relationName] = newTable.relations[relationName];
|
||||||
|
} else {
|
||||||
|
const oldRelation = oldTable.relations[relationName];
|
||||||
|
const newRelation = newTable.relations[relationName];
|
||||||
|
|
||||||
|
if (
|
||||||
|
oldRelation.table !== newRelation.table ||
|
||||||
|
oldRelation.type !== newRelation.type
|
||||||
|
) {
|
||||||
|
changes.relations[relationName] = newRelation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSQL(changes: MigrationTable[]): string {
|
||||||
|
let sql = "";
|
||||||
|
|
||||||
|
for (const table of changes) {
|
||||||
|
if (table.fields && Object.keys(table.fields).length > 0) {
|
||||||
|
if (
|
||||||
|
Object.keys(table.fields).length === Object.keys(table.fields).length
|
||||||
|
) {
|
||||||
|
sql += `CREATE TABLE ${table.name} (\n`;
|
||||||
|
const fieldDefs = [];
|
||||||
|
for (const fieldName in table.fields) {
|
||||||
|
const field = table.fields[fieldName];
|
||||||
|
let fieldDef = ` ${fieldName} ${field.type.toUpperCase()}`;
|
||||||
|
if (!field.isNullable) {
|
||||||
|
fieldDef += " NOT NULL";
|
||||||
|
}
|
||||||
|
if (field.isUnique) {
|
||||||
|
fieldDef += " UNIQUE";
|
||||||
|
}
|
||||||
|
if (field.default !== undefined) {
|
||||||
|
fieldDef += ` DEFAULT ${field.default}`;
|
||||||
|
}
|
||||||
|
fieldDefs.push(fieldDef);
|
||||||
|
}
|
||||||
|
sql += fieldDefs.join(",\n");
|
||||||
|
sql += `\n);\n\n`;
|
||||||
|
} else {
|
||||||
|
for (const fieldName in table.fields) {
|
||||||
|
const field = table.fields[fieldName];
|
||||||
|
sql += `ALTER TABLE ${
|
||||||
|
table.name
|
||||||
|
} ADD COLUMN ${fieldName} ${field.type.toUpperCase()}`;
|
||||||
|
if (!field.isNullable) {
|
||||||
|
sql += " NOT NULL";
|
||||||
|
}
|
||||||
|
if (field.isUnique) {
|
||||||
|
sql += " UNIQUE";
|
||||||
|
}
|
||||||
|
if (field.default !== undefined) {
|
||||||
|
sql += ` DEFAULT ${field.default}`;
|
||||||
|
}
|
||||||
|
sql += `;\n`;
|
||||||
|
}
|
||||||
|
sql += `\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.relations && Object.keys(table.relations).length > 0) {
|
||||||
|
for (const relationName in table.relations) {
|
||||||
|
const relation = table.relations[relationName];
|
||||||
|
if (relation.type === "many-to-one") {
|
||||||
|
sql += `ALTER TABLE ${table.name} ADD COLUMN ${relationName}_id UUID;\n`;
|
||||||
|
sql += `ALTER TABLE ${table.name} ADD CONSTRAINT fk_${table.name}_${relationName} FOREIGN KEY (${relationName}_id) REFERENCES ${relation.table}(id);\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relation.type === "one-to-many") {
|
||||||
|
sql += `ALTER TABLE ${relation.table} ADD COLUMN ${table.name}_id UUID;\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relation.type === "many-to-many") {
|
||||||
|
const junctionTable = `${table.name}_${relation.table}`;
|
||||||
|
sql += `CREATE TABLE ${junctionTable} (\n`;
|
||||||
|
sql += ` ${table.name}_id UUID NOT NULL,\n`;
|
||||||
|
sql += ` ${relation.table}_id UUID NOT NULL,\n`;
|
||||||
|
sql += ` PRIMARY KEY (${table.name}_id, ${relation.table}_id),\n`;
|
||||||
|
sql += ` FOREIGN KEY (${table.name}_id) REFERENCES ${table.name}(id),\n`;
|
||||||
|
sql += ` FOREIGN KEY (${relation.table}_id) REFERENCES ${relation.table}(id)\n`;
|
||||||
|
sql += `);\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(tables: MigrationTable[], token: string): Promise<Migration> {
|
||||||
|
const project = await this.projectService.findByToken(token);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("Project not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrations = await this.migrationRepository.find({
|
||||||
|
where: {
|
||||||
|
project: { token },
|
||||||
|
isApplied: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const migrations = this.migrationRepository.find({
|
const virtualTables = {};
|
||||||
where: {
|
for (const migration of migrations) {
|
||||||
|
for (const table of migration.data) {
|
||||||
|
if (!virtualTables[table.name]) {
|
||||||
|
virtualTables[table.name] = table;
|
||||||
|
} else {
|
||||||
|
virtualTables[table.name] = this.compareTable(
|
||||||
|
virtualTables[table.name],
|
||||||
|
table
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes: MigrationTable[] = [];
|
||||||
|
for (const table of tables) {
|
||||||
|
if (!virtualTables[table.name]) {
|
||||||
|
changes.push(table);
|
||||||
|
} else {
|
||||||
|
const tableChanges = this.compareTable(
|
||||||
|
virtualTables[table.name],
|
||||||
|
table
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
Object.keys(tableChanges.fields).length > 0 ||
|
||||||
|
(tableChanges.relations &&
|
||||||
|
Object.keys(tableChanges.relations).length > 0)
|
||||||
|
) {
|
||||||
|
changes.push(tableChanges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
throw new Error("No changes detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const migration = this.migrationRepository.create({
|
||||||
|
name: `Migration ${new Date().toISOString()}`,
|
||||||
|
project,
|
||||||
|
sql: this.generateSQL(changes),
|
||||||
|
data: changes,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.migrationRepository.save(migration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/migrations/1758912793124-Migrations.ts
Normal file
59
src/migrations/1758912793124-Migrations.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class Migrations1758912793124 implements MigrationInterface {
|
||||||
|
name = "Migrations1758912793124";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE \`migration\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(255) NOT NULL, \`appliedAt\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP(), \`isApplied\` tinyint NOT NULL DEFAULT '1', \`isValid\` tinyint NOT NULL DEFAULT '1', \`sql\` varchar(255) NOT NULL, \`data\` json NULL, \`projectToken\` varchar(36) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE \`query_modules_module\` (\`queryId\` varchar(36) NOT NULL, \`moduleId\` varchar(36) NOT NULL, INDEX \`IDX_12121324c524e12538de4948ce\` (\`queryId\`), INDEX \`IDX_7fb42dbc85874aafbd4bfb1c10\` (\`moduleId\`), PRIMARY KEY (\`queryId\`, \`moduleId\`)) ENGINE=InnoDB`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`module\` DROP COLUMN \`isInjectable\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`plugin\` ADD \`queryId\` varchar(36) NULL`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`migration\` ADD CONSTRAINT \`FK_d3d093f32ce7c968b02fc6bce65\` FOREIGN KEY (\`projectToken\`) REFERENCES \`project\`(\`token\`) ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`plugin\` ADD CONSTRAINT \`FK_5162d18c3653d35ff4d104dd940\` FOREIGN KEY (\`queryId\`) REFERENCES \`query\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`query_modules_module\` ADD CONSTRAINT \`FK_12121324c524e12538de4948cee\` FOREIGN KEY (\`queryId\`) REFERENCES \`query\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`query_modules_module\` ADD CONSTRAINT \`FK_7fb42dbc85874aafbd4bfb1c101\` FOREIGN KEY (\`moduleId\`) REFERENCES \`module\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`query_modules_module\` DROP FOREIGN KEY \`FK_7fb42dbc85874aafbd4bfb1c101\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`query_modules_module\` DROP FOREIGN KEY \`FK_12121324c524e12538de4948cee\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`plugin\` DROP FOREIGN KEY \`FK_5162d18c3653d35ff4d104dd940\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`migration\` DROP FOREIGN KEY \`FK_d3d093f32ce7c968b02fc6bce65\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`plugin\` DROP COLUMN \`queryId\``);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE \`module\` ADD \`isInjectable\` tinyint NOT NULL DEFAULT 1`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_7fb42dbc85874aafbd4bfb1c10\` ON \`query_modules_module\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_12121324c524e12538de4948ce\` ON \`query_modules_module\``
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE \`query_modules_module\``);
|
||||||
|
await queryRunner.query(`DROP TABLE \`migration\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Migration } from "src/migration/entities/migration.entity";
|
import { Migration } from "../../migration/entities/migration.entity";
|
||||||
import { Token } from "../../api/entities/token.entity";
|
import { Token } from "../../api/entities/token.entity";
|
||||||
import { Query } from "../../query/entities/query.entity";
|
import { Query } from "../../query/entities/query.entity";
|
||||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|||||||
22
tests/base/createMigration.ts
Normal file
22
tests/base/createMigration.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import createMigration from "../functions/createMigration";
|
||||||
|
import createProject from "../functions/createProject";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const project = await createProject("Test Project");
|
||||||
|
|
||||||
|
const result_1 = await createMigration(project.token, [
|
||||||
|
{
|
||||||
|
name: "users",
|
||||||
|
fields: {
|
||||||
|
name: { type: "string", isNullable: false },
|
||||||
|
age: { type: "int", isNullable: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("Migration 1:", result_1.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during test execution:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
15
tests/functions/createMigration.ts
Normal file
15
tests/functions/createMigration.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { config } from "../config";
|
||||||
|
|
||||||
|
export default async (token: string, tables: any) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${config.url}/migrations/create`, {
|
||||||
|
token,
|
||||||
|
tables,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in creating migration:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user