feat: implement Database Manager module with encryption, CRUD operations, and migration management
This commit is contained in:
@ -7,3 +7,6 @@ DB_DATABASE=low_code_engine
|
||||
|
||||
# Application Configuration
|
||||
NODE_ENV=development
|
||||
PORT=3054
|
||||
ENCRYPTION_KEY=12345678901234567890123456789012
|
||||
IV_LENGTH=16
|
||||
@ -37,6 +37,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"axios": "^1.12.2",
|
||||
"crypto": "^1.0.1",
|
||||
"ioredis": "^5.7.0",
|
||||
"isolated-vm": "^6.0.1",
|
||||
"mariadb": "^3.4.5",
|
||||
|
||||
@ -9,8 +9,8 @@ export class ApiController {
|
||||
) {}
|
||||
|
||||
@Post("token/generate")
|
||||
generateToken(@Body() body: { token: string }) {
|
||||
return this.apiService.generateToken(body.token);
|
||||
generateToken(@Body() body: { id: string }) {
|
||||
return this.apiService.generateToken(body.id);
|
||||
}
|
||||
|
||||
@Delete("token/revoke")
|
||||
|
||||
@ -14,9 +14,9 @@ export class ApiService {
|
||||
private readonly projectRepository: Repository<Project>
|
||||
) {}
|
||||
|
||||
async generateToken(projectToken: string) {
|
||||
async generateToken(projectId: string) {
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { token: projectToken },
|
||||
where: { id: projectId },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
|
||||
@ -5,17 +5,17 @@ import { QueryModule } from "src/query/query.module";
|
||||
import { ProjectModule } from "src/project/project.module";
|
||||
import { RedisModule } from "src/redis/redis.module";
|
||||
import { DatabaseModule } from "src/database/database.module";
|
||||
import { MigrationModule } from "src/migration/migration.module";
|
||||
import { DatabaseManagerModule } from "src/databaseManager/database/database.manager.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
RedisModule,
|
||||
DatabaseManagerModule,
|
||||
ApiModule,
|
||||
ProjectModule,
|
||||
QueryModule,
|
||||
TestModule,
|
||||
MigrationModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
||||
31
src/databaseManager/database.encryption.service.ts
Normal file
31
src/databaseManager/database.encryption.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as crypto from "crypto";
|
||||
|
||||
export class DatabaseEncryptionService {
|
||||
protected encryptPassword(password: string): string {
|
||||
const iv = crypto.randomBytes(
|
||||
process.env.IV_LENGTH ? parseInt(process.env.IV_LENGTH) : 16
|
||||
);
|
||||
const cipher = crypto.createCipheriv(
|
||||
"aes-256-cbc",
|
||||
process.env.ENCRYPTION_KEY,
|
||||
iv
|
||||
);
|
||||
let encrypted = cipher.update(password, "utf8", "base64");
|
||||
encrypted += cipher.final("base64");
|
||||
return iv.toString("base64") + ":" + encrypted;
|
||||
}
|
||||
|
||||
protected decryptPassword(encrypted: string): string {
|
||||
const parts = encrypted.split(":");
|
||||
const iv = Buffer.from(parts[0], "base64");
|
||||
const encryptedText = parts[1];
|
||||
const decipher = crypto.createDecipheriv(
|
||||
"aes-256-cbc",
|
||||
process.env.ENCRYPTION_KEY,
|
||||
iv
|
||||
);
|
||||
let decrypted = decipher.update(encryptedText, "base64", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
74
src/databaseManager/database/database.manager.controller.ts
Normal file
74
src/databaseManager/database/database.manager.controller.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { Controller, Get, Inject, Post } from "@nestjs/common";
|
||||
import { DatabaseManagerService } from "./database.manager.service";
|
||||
import { DatabaseNodeService } from "../databaseNode/database.node.service";
|
||||
import { MigrationService } from "../migration/migration.service";
|
||||
|
||||
@Controller("database")
|
||||
export class DatabaseManagerController {
|
||||
constructor(
|
||||
@Inject("DatabaseService")
|
||||
private readonly databaseManagerService: DatabaseManagerService,
|
||||
@Inject("DatabaseNodeService")
|
||||
private readonly databaseNodeService: DatabaseNodeService,
|
||||
@Inject("MigrationService")
|
||||
private readonly migrationService: MigrationService
|
||||
) {}
|
||||
|
||||
@Post("create")
|
||||
createDatabase(
|
||||
@Inject("body") body: { projectId: string; databaseNodeId: string }
|
||||
) {
|
||||
return this.databaseManagerService.createDatabase(
|
||||
body.projectId,
|
||||
body.databaseNodeId
|
||||
);
|
||||
}
|
||||
|
||||
@Post("node/create")
|
||||
addDatabaseNode(
|
||||
@Inject("body")
|
||||
body: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
) {
|
||||
return this.databaseNodeService.create(
|
||||
body.host,
|
||||
body.port,
|
||||
body.username,
|
||||
body.password
|
||||
);
|
||||
}
|
||||
|
||||
@Get("migration/up/:databaseId")
|
||||
migrateUp(@Inject("params") params: { databaseId: string }) {
|
||||
return this.migrationService.up(params.databaseId);
|
||||
}
|
||||
|
||||
@Get("migration/down/:databaseId")
|
||||
migrateDown(@Inject("params") params: { databaseId: string }) {
|
||||
return this.migrationService.down(params.databaseId);
|
||||
}
|
||||
|
||||
@Post("migration/create")
|
||||
createMigration(
|
||||
@Inject("body")
|
||||
body: {
|
||||
up: string;
|
||||
down: string;
|
||||
databaseId: string;
|
||||
}
|
||||
) {
|
||||
return this.migrationService.create(body.up, body.down, body.databaseId);
|
||||
}
|
||||
|
||||
@Post("query/:databaseId")
|
||||
runQuery(
|
||||
@Inject("params") params: { databaseId: string },
|
||||
@Inject("body") body: { query: string }
|
||||
) {
|
||||
return this.databaseManagerService.runQuery(params.databaseId, body.query);
|
||||
}
|
||||
}
|
||||
22
src/databaseManager/database/database.manager.module.ts
Normal file
22
src/databaseManager/database/database.manager.module.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { Database } from "../entities/database.entity";
|
||||
import { MigrationModule } from "../migration/migration.module";
|
||||
import { ProjectModule } from "src/project/project.module";
|
||||
import { DatabaseManagerController } from "./database.manager.controller";
|
||||
import { DatabaseManagerService } from "./database.manager.service";
|
||||
import { DatabaseNode } from "../entities/database.node.entity";
|
||||
import { Project } from "src/project/entities/project.entity";
|
||||
import { DatabaseNodeService } from "../databaseNode/database.node.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => ProjectModule),
|
||||
MigrationModule,
|
||||
TypeOrmModule.forFeature([Database, DatabaseNode, Project]),
|
||||
],
|
||||
controllers: [DatabaseManagerController],
|
||||
providers: [DatabaseManagerService, DatabaseNodeService],
|
||||
exports: [DatabaseManagerService, DatabaseNodeService],
|
||||
})
|
||||
export class DatabaseManagerModule {}
|
||||
95
src/databaseManager/database/database.manager.service.ts
Normal file
95
src/databaseManager/database/database.manager.service.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Database } from "../entities/database.entity";
|
||||
import { Repository } from "typeorm";
|
||||
import { ProjectService } from "src/project/project.service";
|
||||
import { DatabaseEncryptionService } from "../database.encryption.service";
|
||||
import { DatabaseNodeService } from "../databaseNode/database.node.service";
|
||||
import * as mysql from "mysql2/promise";
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseManagerService extends DatabaseEncryptionService {
|
||||
constructor(
|
||||
@InjectRepository(Database)
|
||||
private databaseRepository: Repository<Database>,
|
||||
@Inject(ProjectService)
|
||||
private readonly projectService: ProjectService,
|
||||
@Inject(DatabaseNodeService)
|
||||
private readonly databaseNodeService: DatabaseNodeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Database | null> {
|
||||
return this.databaseRepository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async runQuery(databaseId: string, query: string) {
|
||||
const database = await this.findById(databaseId);
|
||||
|
||||
if (!database) {
|
||||
throw new Error("Database not found");
|
||||
}
|
||||
|
||||
const dbConnection = await mysql.createConnection({
|
||||
host: database.node.host,
|
||||
port: database.node.port,
|
||||
user: database.c_username,
|
||||
password: this.decryptPassword(database.password),
|
||||
database: database.database,
|
||||
enableKeepAlive: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const [results] = await dbConnection.execute(query);
|
||||
return results;
|
||||
} finally {
|
||||
await dbConnection.end();
|
||||
}
|
||||
}
|
||||
|
||||
async createDatabase(
|
||||
databaseNodeId: string,
|
||||
projectId: string
|
||||
): Promise<Database> {
|
||||
const node = await this.databaseNodeService.findById(databaseNodeId);
|
||||
|
||||
if (!node) {
|
||||
throw new Error("Database node not found");
|
||||
}
|
||||
|
||||
const project = await this.projectService.findById(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const c_username = `c_user_${Math.random().toString(36).substring(2, 8)}`;
|
||||
const q_username = `q_user_${Math.random().toString(36).substring(2, 8)}`;
|
||||
const databaseName = `db_${Math.random().toString(36).substring(2, 8)}`;
|
||||
const password = this.encryptPassword(
|
||||
Math.random().toString(36).substring(2, 10)
|
||||
);
|
||||
|
||||
await this.databaseNodeService.initDatabase(
|
||||
{
|
||||
database: databaseName,
|
||||
c_username,
|
||||
q_username,
|
||||
password: this.decryptPassword(password),
|
||||
},
|
||||
databaseNodeId
|
||||
);
|
||||
|
||||
const database = this.databaseRepository.create({
|
||||
q_username,
|
||||
c_username,
|
||||
database: databaseName,
|
||||
password,
|
||||
node,
|
||||
project,
|
||||
});
|
||||
|
||||
return await this.databaseRepository.save(database);
|
||||
}
|
||||
}
|
||||
88
src/databaseManager/databaseNode/database.node.service.ts
Normal file
88
src/databaseManager/databaseNode/database.node.service.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { DatabaseNode } from "../entities/database.node.entity";
|
||||
import { Repository } from "typeorm";
|
||||
import { DatabaseEncryptionService } from "../database.encryption.service";
|
||||
import * as mysql from "mysql2/promise";
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseNodeService extends DatabaseEncryptionService {
|
||||
constructor(
|
||||
@InjectRepository(DatabaseNode)
|
||||
private databaseNodeRepository: Repository<DatabaseNode>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<DatabaseNode | null> {
|
||||
return this.databaseNodeRepository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async initDatabase(
|
||||
data: {
|
||||
database: string;
|
||||
c_username: string;
|
||||
q_username: string;
|
||||
password: string;
|
||||
},
|
||||
databaseNodeId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const dbConnection = await mysql.createConnection({
|
||||
...(await this.getConnectionOptions(databaseNodeId)),
|
||||
enableKeepAlive: true,
|
||||
});
|
||||
|
||||
await dbConnection.execute(`CREATE DATABASE \`${data.database}\`;`);
|
||||
await dbConnection.execute(
|
||||
`CREATE USER '${data.c_username}'@'%' IDENTIFIED BY '${data.password}';`
|
||||
);
|
||||
await dbConnection.execute(
|
||||
`CREATE USER '${data.q_username}'@'%' IDENTIFIED BY '${data.password}';`
|
||||
);
|
||||
await dbConnection.execute(
|
||||
`GRANT ALL PRIVILEGES ON \`${data.database}\`.* TO '${data.c_username}'@'%';`
|
||||
);
|
||||
await dbConnection.execute(
|
||||
`GRANT SELECT, SHOW VIEW ON \`${data.database}\`.* TO '${data.q_username}'@'%';`
|
||||
);
|
||||
await dbConnection.execute(`FLUSH PRIVILEGES;`);
|
||||
|
||||
await dbConnection.end();
|
||||
} catch (error) {
|
||||
console.error("Error initializing database:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
host: string,
|
||||
port: number,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<DatabaseNode> {
|
||||
const encryptedPassword = this.encryptPassword(password);
|
||||
const databaseNode = this.databaseNodeRepository.create({
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password: encryptedPassword,
|
||||
});
|
||||
return this.databaseNodeRepository.save(databaseNode);
|
||||
}
|
||||
|
||||
async getConnectionOptions(id: string) {
|
||||
const node = await this.databaseNodeRepository.findOne({ where: { id } });
|
||||
|
||||
if (!node) {
|
||||
throw new Error("Database node not found");
|
||||
}
|
||||
|
||||
return {
|
||||
host: node.host,
|
||||
port: node.port,
|
||||
username: node.username,
|
||||
password: this.decryptPassword(node.password),
|
||||
};
|
||||
}
|
||||
}
|
||||
38
src/databaseManager/entities/database.entity.ts
Normal file
38
src/databaseManager/entities/database.entity.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { Migration } from "./migration.entity";
|
||||
import { Project } from "src/project/entities/project.entity";
|
||||
import { DatabaseNode } from "./database.node.entity";
|
||||
|
||||
@Entity("database")
|
||||
export class Database {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
q_username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
c_username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
password: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
database: string;
|
||||
|
||||
@OneToMany(() => Migration, (migration) => migration.database)
|
||||
migrations: Migration[];
|
||||
|
||||
@OneToOne(() => Project, (project) => project.database)
|
||||
project: Project;
|
||||
|
||||
@ManyToOne(() => DatabaseNode, (node) => node.databases)
|
||||
node: DatabaseNode;
|
||||
}
|
||||
23
src/databaseManager/entities/database.node.entity.ts
Normal file
23
src/databaseManager/entities/database.node.entity.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { Database } from "./database.entity";
|
||||
|
||||
@Entity("databaseNode")
|
||||
export class DatabaseNode {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
host: string;
|
||||
|
||||
@Column({ type: "int" })
|
||||
port: number;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
password: string;
|
||||
|
||||
@OneToMany(() => Database, (database) => database.node)
|
||||
databases: Database[];
|
||||
}
|
||||
35
src/databaseManager/entities/migration.entity.ts
Normal file
35
src/databaseManager/entities/migration.entity.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { Database } from "./database.entity";
|
||||
|
||||
@Entity("migration")
|
||||
export class Migration {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
||||
appliedAt: Date;
|
||||
|
||||
@Column({ type: "tinyint", default: 1 })
|
||||
isApplied: number;
|
||||
|
||||
@Column({ type: "tinyint", default: 1 })
|
||||
isValid: number;
|
||||
|
||||
@Column({ type: "longtext", nullable: false })
|
||||
up: string;
|
||||
|
||||
@Column({ type: "longtext", nullable: false })
|
||||
down: string;
|
||||
|
||||
@ManyToOne(() => Database, (database) => database.id)
|
||||
database: Database;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
11
src/databaseManager/migration/migration.module.ts
Normal file
11
src/databaseManager/migration/migration.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { MigrationService } from "./migration.service";
|
||||
import { DatabaseModule } from "src/database/database.module";
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => DatabaseModule)],
|
||||
controllers: [],
|
||||
providers: [MigrationService],
|
||||
exports: [MigrationService],
|
||||
})
|
||||
export class MigrationModule {}
|
||||
95
src/databaseManager/migration/migration.service.ts
Normal file
95
src/databaseManager/migration/migration.service.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { IsNull, Not, Repository } from "typeorm";
|
||||
import { Migration } from "../entities/migration.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { DatabaseManagerService } from "../database/database.manager.service";
|
||||
|
||||
@Injectable()
|
||||
export class MigrationService {
|
||||
constructor(
|
||||
@InjectRepository(Migration)
|
||||
private readonly migrationRepository: Repository<Migration>,
|
||||
@Inject(DatabaseManagerService)
|
||||
private readonly databaseService: DatabaseManagerService
|
||||
) {}
|
||||
|
||||
async create(
|
||||
up: string,
|
||||
down: string,
|
||||
databaseId: string
|
||||
): Promise<Migration> {
|
||||
const database = await this.databaseService.findById(databaseId);
|
||||
|
||||
if (!database) {
|
||||
throw new Error("Database not found");
|
||||
}
|
||||
|
||||
const migration = this.migrationRepository.create({
|
||||
up,
|
||||
down,
|
||||
database,
|
||||
});
|
||||
|
||||
return await this.migrationRepository.save(migration);
|
||||
}
|
||||
|
||||
async up(databaseId: string): Promise<Migration[]> {
|
||||
const database = await this.databaseService.findById(databaseId);
|
||||
|
||||
if (!database) {
|
||||
throw new Error("Database not found");
|
||||
}
|
||||
|
||||
const migrations = await this.migrationRepository.find({
|
||||
where: { database: { id: database.id }, appliedAt: null },
|
||||
order: { createdAt: "ASC" },
|
||||
});
|
||||
|
||||
const completedMigrations: Migration[] = [];
|
||||
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
await this.databaseService.runQuery(database.id, migration.up);
|
||||
migration.appliedAt = new Date();
|
||||
await this.migrationRepository.save(migration);
|
||||
completedMigrations.push(migration);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to apply migration ${migration.id}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return completedMigrations;
|
||||
}
|
||||
|
||||
async down(databaseId: string): Promise<Migration[]> {
|
||||
const database = await this.databaseService.findById(databaseId);
|
||||
|
||||
if (!database) {
|
||||
throw new Error("Database not found");
|
||||
}
|
||||
|
||||
const migrations = await this.migrationRepository.find({
|
||||
where: { database: { id: database.id }, appliedAt: Not(IsNull()) },
|
||||
order: { appliedAt: "DESC" },
|
||||
});
|
||||
|
||||
const revertedMigrations: Migration[] = [];
|
||||
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
await this.databaseService.runQuery(database.id, migration.down);
|
||||
migration.appliedAt = null;
|
||||
await this.migrationRepository.save(migration);
|
||||
revertedMigrations.push(migration);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to revert migration ${migration.id}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return revertedMigrations;
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import { Project } from "../../project/entities/project.entity";
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { MigrationTable } from "../migration.constants";
|
||||
|
||||
@Entity("migration")
|
||||
export class Migration {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
||||
appliedAt: Date;
|
||||
|
||||
@Column({ type: "tinyint", default: 1 })
|
||||
isApplied: number;
|
||||
|
||||
@Column({ type: "tinyint", default: 1 })
|
||||
isValid: number;
|
||||
|
||||
@ManyToOne(() => Project, (project) => project.migrations)
|
||||
project: Project;
|
||||
|
||||
@Column()
|
||||
sql: string;
|
||||
|
||||
@Column({ type: "json", nullable: true })
|
||||
data: MigrationTable[];
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
export type MigrationFieldType =
|
||||
| "int"
|
||||
| "float"
|
||||
| "bigint"
|
||||
| "boolean"
|
||||
| "text"
|
||||
| "uuid"
|
||||
| "datetime";
|
||||
|
||||
export type MigrationField = {
|
||||
type: MigrationFieldType;
|
||||
isNullable: boolean;
|
||||
isUnique: boolean;
|
||||
default?: any;
|
||||
};
|
||||
|
||||
export type MigrationRelationType =
|
||||
| "one-to-many"
|
||||
| "many-to-one"
|
||||
| "many-to-many";
|
||||
|
||||
export type MigrationTable = {
|
||||
name: string;
|
||||
fields: Record<string, MigrationField>;
|
||||
relations?: Record<string, { table: string; type: MigrationRelationType }>;
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { MigrationService } from "./migration.service";
|
||||
import { MigrationTable } from "./migration.constants";
|
||||
|
||||
@Controller("migrations")
|
||||
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,16 +0,0 @@
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { Migration } from "./entities/migration.entity";
|
||||
import { MigrationController } from "./migration.controller";
|
||||
import { MigrationService } from "./migration.service";
|
||||
import { ProjectModule } from "src/project/project.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => ProjectModule),
|
||||
TypeOrmModule.forFeature([Migration]),
|
||||
],
|
||||
controllers: [MigrationController],
|
||||
providers: [MigrationService],
|
||||
})
|
||||
export class MigrationModule {}
|
||||
@ -1,200 +0,0 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { Repository } from "typeorm";
|
||||
import { Migration } from "./entities/migration.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { MigrationTable } from "./migration.constants";
|
||||
import { ProjectService } from "src/project/project.service";
|
||||
|
||||
@Injectable()
|
||||
export class MigrationService {
|
||||
constructor(
|
||||
@InjectRepository(Migration)
|
||||
private readonly migrationRepository: Repository<Migration>,
|
||||
@Inject(ProjectService)
|
||||
private readonly projectService: ProjectService
|
||||
) {}
|
||||
|
||||
compareTable(
|
||||
oldTable: MigrationTable,
|
||||
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 virtualTables = {};
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,18 @@
|
||||
import { Migration } from "../../migration/entities/migration.entity";
|
||||
import { Token } from "../../api/entities/token.entity";
|
||||
import { Query } from "../../query/entities/query.entity";
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { Database } from "src/databaseManager/entities/database.entity";
|
||||
|
||||
@Entity("project")
|
||||
export class Project {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
token: string;
|
||||
id: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
name: string;
|
||||
@ -14,8 +20,8 @@ export class Project {
|
||||
@OneToMany(() => Token, (token) => token.project)
|
||||
apiTokens: Token[];
|
||||
|
||||
@OneToMany(() => Migration, (migration) => migration.project)
|
||||
migrations: Migration[];
|
||||
@OneToOne(() => Database, (database) => database.project)
|
||||
database: Database;
|
||||
|
||||
@OneToMany(() => Query, (query) => query.project)
|
||||
queries: Query[];
|
||||
|
||||
@ -15,7 +15,7 @@ export class ProjectService {
|
||||
return this.projectRepository.save(project);
|
||||
}
|
||||
|
||||
findByToken(token: string) {
|
||||
return this.projectRepository.findOne({ where: { token } });
|
||||
findById(id: string) {
|
||||
return this.projectRepository.findOne({ where: { id: id } });
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,9 +98,7 @@ export class QueryHandlerService {
|
||||
}
|
||||
|
||||
async createQuery(queryData: { projectToken: string; source: string }) {
|
||||
const project = await this.projectService.findByToken(
|
||||
queryData.projectToken
|
||||
);
|
||||
const project = await this.projectService.findById(queryData.projectToken);
|
||||
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
|
||||
@ -1270,6 +1270,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.6:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
|
||||
integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
|
||||
|
||||
dayjs@^1.11.13:
|
||||
version "1.11.18"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.18.tgz#835fa712aac52ab9dec8b1494098774ed7070a11"
|
||||
|
||||
Reference in New Issue
Block a user