feat: implement Database Manager module with encryption, CRUD operations, and migration management
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user