diff --git a/.env.example b/.env.example index 8c21593..5831193 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,6 @@ DB_DATABASE=low_code_engine # Application Configuration NODE_ENV=development +PORT=3054 +ENCRYPTION_KEY=12345678901234567890123456789012 +IV_LENGTH=16 \ No newline at end of file diff --git a/package.json b/package.json index 38d4cf8..ffa27fd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/api.controller.ts b/src/api/api.controller.ts index 23cd213..467ff31 100644 --- a/src/api/api.controller.ts +++ b/src/api/api.controller.ts @@ -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") diff --git a/src/api/api.service.ts b/src/api/api.service.ts index fbb9946..1b6ce01 100644 --- a/src/api/api.service.ts +++ b/src/api/api.service.ts @@ -14,9 +14,9 @@ export class ApiService { private readonly projectRepository: Repository ) {} - async generateToken(projectToken: string) { + async generateToken(projectId: string) { const project = await this.projectRepository.findOne({ - where: { token: projectToken }, + where: { id: projectId }, }); if (!project) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ecc9a2c..bd06bc3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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: [], diff --git a/src/databaseManager/database.encryption.service.ts b/src/databaseManager/database.encryption.service.ts new file mode 100644 index 0000000..8498f9a --- /dev/null +++ b/src/databaseManager/database.encryption.service.ts @@ -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; + } +} diff --git a/src/databaseManager/database/database.manager.controller.ts b/src/databaseManager/database/database.manager.controller.ts new file mode 100644 index 0000000..94a61b2 --- /dev/null +++ b/src/databaseManager/database/database.manager.controller.ts @@ -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); + } +} diff --git a/src/databaseManager/database/database.manager.module.ts b/src/databaseManager/database/database.manager.module.ts new file mode 100644 index 0000000..5414f6a --- /dev/null +++ b/src/databaseManager/database/database.manager.module.ts @@ -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 {} diff --git a/src/databaseManager/database/database.manager.service.ts b/src/databaseManager/database/database.manager.service.ts new file mode 100644 index 0000000..c4d386e --- /dev/null +++ b/src/databaseManager/database/database.manager.service.ts @@ -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, + @Inject(ProjectService) + private readonly projectService: ProjectService, + @Inject(DatabaseNodeService) + private readonly databaseNodeService: DatabaseNodeService + ) { + super(); + } + + async findById(id: string): Promise { + 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 { + 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); + } +} diff --git a/src/databaseManager/databaseNode/database.node.service.ts b/src/databaseManager/databaseNode/database.node.service.ts new file mode 100644 index 0000000..774617e --- /dev/null +++ b/src/databaseManager/databaseNode/database.node.service.ts @@ -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 + ) { + super(); + } + + async findById(id: string): Promise { + return this.databaseNodeRepository.findOne({ where: { id } }); + } + + async initDatabase( + data: { + database: string; + c_username: string; + q_username: string; + password: string; + }, + databaseNodeId: string + ): Promise { + 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 { + 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), + }; + } +} diff --git a/src/databaseManager/entities/database.entity.ts b/src/databaseManager/entities/database.entity.ts new file mode 100644 index 0000000..d0ba5d8 --- /dev/null +++ b/src/databaseManager/entities/database.entity.ts @@ -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; +} diff --git a/src/databaseManager/entities/database.node.entity.ts b/src/databaseManager/entities/database.node.entity.ts new file mode 100644 index 0000000..68f7c30 --- /dev/null +++ b/src/databaseManager/entities/database.node.entity.ts @@ -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[]; +} diff --git a/src/databaseManager/entities/migration.entity.ts b/src/databaseManager/entities/migration.entity.ts new file mode 100644 index 0000000..2bb8f72 --- /dev/null +++ b/src/databaseManager/entities/migration.entity.ts @@ -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; +} diff --git a/src/databaseManager/migration/migration.module.ts b/src/databaseManager/migration/migration.module.ts new file mode 100644 index 0000000..010063d --- /dev/null +++ b/src/databaseManager/migration/migration.module.ts @@ -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 {} diff --git a/src/databaseManager/migration/migration.service.ts b/src/databaseManager/migration/migration.service.ts new file mode 100644 index 0000000..55f00c0 --- /dev/null +++ b/src/databaseManager/migration/migration.service.ts @@ -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, + @Inject(DatabaseManagerService) + private readonly databaseService: DatabaseManagerService + ) {} + + async create( + up: string, + down: string, + databaseId: string + ): Promise { + 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 { + 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 { + 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; + } +} diff --git a/src/migration/entities/migration.entity.ts b/src/migration/entities/migration.entity.ts deleted file mode 100644 index a64ebb5..0000000 --- a/src/migration/entities/migration.entity.ts +++ /dev/null @@ -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[]; -} diff --git a/src/migration/migration.constants.ts b/src/migration/migration.constants.ts deleted file mode 100644 index b170d9b..0000000 --- a/src/migration/migration.constants.ts +++ /dev/null @@ -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; - relations?: Record; -}; diff --git a/src/migration/migration.controller.ts b/src/migration/migration.controller.ts deleted file mode 100644 index a5860a1..0000000 --- a/src/migration/migration.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/src/migration/migration.module.ts b/src/migration/migration.module.ts deleted file mode 100644 index 8c038d2..0000000 --- a/src/migration/migration.module.ts +++ /dev/null @@ -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 {} diff --git a/src/migration/migration.service.ts b/src/migration/migration.service.ts deleted file mode 100644 index 3ff06d6..0000000 --- a/src/migration/migration.service.ts +++ /dev/null @@ -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, - @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 { - 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); - } -} diff --git a/src/project/entities/project.entity.ts b/src/project/entities/project.entity.ts index 4b75518..2ef0328 100644 --- a/src/project/entities/project.entity.ts +++ b/src/project/entities/project.entity.ts @@ -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[]; diff --git a/src/project/project.service.ts b/src/project/project.service.ts index ab72992..ca2e7fb 100644 --- a/src/project/project.service.ts +++ b/src/project/project.service.ts @@ -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 } }); } } diff --git a/src/query/handler/query.handler.service.ts b/src/query/handler/query.handler.service.ts index caaf40c..91dedeb 100644 --- a/src/query/handler/query.handler.service.ts +++ b/src/query/handler/query.handler.service.ts @@ -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"); diff --git a/yarn.lock b/yarn.lock index ab18b4b..f09d1cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"