From e89af0dd20cded56da371d6ba25e2fad9642cdd8 Mon Sep 17 00:00:00 2001 From: lborv Date: Thu, 9 Oct 2025 19:35:30 +0300 Subject: [PATCH] feat: implement function management with FunctionEntity, FunctionService, and FunctionController; enhance QueryExecuterService to utilize functions; refactor CommandController and QueryController to extend BaseQueryController; update Vm class to handle functions; remove obsolete log entities --- src/migrations/1760026715562-functions.ts | 24 +++++++ src/project/entities/project.entity.ts | 4 ++ src/project/function/function.controller.ts | 22 ++++++ src/project/function/function.service.ts | 65 ++++++++++++++++++ src/query/base/base-query.controller.ts | 71 ++++++++++++++++++++ src/query/command/command.controller.ts | 63 +++-------------- src/query/entities/function.entity.ts | 24 +++++++ src/query/executer/query.executer.service.ts | 35 ++++++---- src/query/handler/query.controller.ts | 63 +++-------------- src/query/logs/entities/log.entity.ts | 20 ------ src/query/logs/logs.type.ts | 12 ---- src/query/query.module.ts | 9 ++- src/vm/vm.class.ts | 7 ++ src/vm/vm.constants.ts | 2 +- 14 files changed, 261 insertions(+), 160 deletions(-) create mode 100644 src/migrations/1760026715562-functions.ts create mode 100644 src/project/function/function.controller.ts create mode 100644 src/project/function/function.service.ts create mode 100644 src/query/base/base-query.controller.ts create mode 100644 src/query/entities/function.entity.ts delete mode 100644 src/query/logs/entities/log.entity.ts delete mode 100644 src/query/logs/logs.type.ts diff --git a/src/migrations/1760026715562-functions.ts b/src/migrations/1760026715562-functions.ts new file mode 100644 index 0000000..f6db0ae --- /dev/null +++ b/src/migrations/1760026715562-functions.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Functions1760026715562 implements MigrationInterface { + name = "Functions1760026715562"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`function\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(255) NOT NULL, \`source\` longtext NOT NULL, \`projectId\` varchar(36) NULL, UNIQUE INDEX \`IDX_FUNCTION_NAME_PROJECT\` (\`name\`, \`projectId\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`function\` ADD CONSTRAINT \`FK_ed63e3acac533604d1d4a21b1d3\` FOREIGN KEY (\`projectId\`) REFERENCES \`project\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`function\` DROP FOREIGN KEY \`FK_ed63e3acac533604d1d4a21b1d3\`` + ); + await queryRunner.query( + `DROP INDEX \`IDX_FUNCTION_NAME_PROJECT\` ON \`function\`` + ); + await queryRunner.query(`DROP TABLE \`function\``); + } +} diff --git a/src/project/entities/project.entity.ts b/src/project/entities/project.entity.ts index caab56a..e2a9ee3 100644 --- a/src/project/entities/project.entity.ts +++ b/src/project/entities/project.entity.ts @@ -9,6 +9,7 @@ import { PrimaryGeneratedColumn, } from "typeorm"; import { Database } from "../../databaseManager/entities/database.entity"; +import { FunctionEntity } from "../../query/entities/function.entity"; @Entity("project") export class Project { @@ -27,4 +28,7 @@ export class Project { @OneToMany(() => Query, (query) => query.project) queries: Query[]; + + @OneToMany(() => FunctionEntity, (functionEntity) => functionEntity.project) + functions: FunctionEntity[]; } diff --git a/src/project/function/function.controller.ts b/src/project/function/function.controller.ts new file mode 100644 index 0000000..1eb93fe --- /dev/null +++ b/src/project/function/function.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Inject, Post, UseGuards } from "@nestjs/common"; +import { ApiTokenGuard } from "src/api/guards/api-token.guard"; +import { FunctionService } from "./function.service"; + +@Controller("functions") +@UseGuards(ApiTokenGuard) +export class FunctionController { + constructor( + @Inject(FunctionService) + private readonly functionService: FunctionService + ) {} + + @Post("create") + async createFunction(projectId: string, name: string, source: string) { + return this.functionService.create(projectId, name, source); + } + + @Post("delete") + async deleteFunction(projectId: string, name: string) { + return this.functionService.deleteFunction(projectId, name); + } +} diff --git a/src/project/function/function.service.ts b/src/project/function/function.service.ts new file mode 100644 index 0000000..b30cb99 --- /dev/null +++ b/src/project/function/function.service.ts @@ -0,0 +1,65 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { FunctionEntity } from "src/query/entities/function.entity"; +import { In, Repository } from "typeorm"; +import { ProjectService } from "../project.service"; + +@Injectable() +export class FunctionService { + constructor( + @InjectRepository(FunctionEntity) + private readonly functionRepository: Repository, + @Inject(ProjectService) + private readonly projectService: ProjectService + ) {} + + async create( + projectId: string, + name: string, + source: string + ): Promise { + const project = await this.projectService.findById(projectId); + + if (!project) { + throw new Error("Project not found"); + } + + const existingFunction = await this.functionRepository.findOne({ + where: { name, project: { id: projectId } }, + }); + + if (existingFunction) { + existingFunction.source = source; + return this.functionRepository.save(existingFunction); + } + + const functionEntity = this.functionRepository.create({ + name, + source, + project, + }); + + return this.functionRepository.save(functionEntity); + } + + async findByNames( + projectId: string, + names: string[] + ): Promise { + return this.functionRepository.find({ + where: { project: { id: projectId }, name: In(names) }, + }); + } + + async deleteFunction(projectId: string, name: string): Promise { + const functionEntity = await this.functionRepository.findOne({ + where: { name, project: { id: projectId } }, + }); + + if (!functionEntity) { + throw new Error("Function not found"); + } + + await this.functionRepository.remove(functionEntity); + } +} diff --git a/src/query/base/base-query.controller.ts b/src/query/base/base-query.controller.ts new file mode 100644 index 0000000..00add3c --- /dev/null +++ b/src/query/base/base-query.controller.ts @@ -0,0 +1,71 @@ +import { + Body, + Headers, + Inject, + Param, + Post, + Res, + UseGuards, +} from "@nestjs/common"; +import { Response } from "express"; +import { QueryHandlerService } from "../handler/query.handler.service"; +import { ApiTokenGuard } from "src/api/guards/api-token.guard"; +import { QueryExecuterService } from "../executer/query.executer.service"; + +@UseGuards(ApiTokenGuard) +export abstract class BaseQueryController { + constructor( + @Inject(QueryHandlerService) + protected readonly queryHandlerService: QueryHandlerService, + @Inject(QueryExecuterService) + protected readonly queryExecuterService: QueryExecuterService + ) {} + + protected abstract getIsCommand(): boolean; + + @Post("create") + async createQuery( + @Body() queryData: { projectToken: string; source: string } + ) { + return this.queryHandlerService.createQuery(queryData, this.getIsCommand()); + } + + @Post("update/:id") + async updateQuery( + @Body() updateData: Partial<{ source: string }>, + @Inject("id") id: string + ) { + return this.queryHandlerService.updateQuery(id, updateData); + } + + @Post("/run/:token") + async runQuery( + @Param("token") token: string, + @Body() query: Record, + @Headers() headers: Record, + @Res() res: Response + ) { + const queryResult = await this.queryExecuterService.runQueryQueued( + token, + query, + headers + ); + + res.status(queryResult?.statusCode || 200); + + if ( + queryResult?.statusCode === 302 && + queryResult?.headers && + queryResult?.headers["Location"] + ) { + res.redirect(queryResult?.headers["Location"]); + return; + } + + for (const [key, value] of Object.entries(queryResult?.headers || {})) { + res.setHeader(key, value); + } + + res.send(queryResult?.response || null); + } +} diff --git a/src/query/command/command.controller.ts b/src/query/command/command.controller.ts index 0b1b835..35cf516 100644 --- a/src/query/command/command.controller.ts +++ b/src/query/command/command.controller.ts @@ -1,67 +1,20 @@ -import { - Body, - Controller, - Headers, - Inject, - Param, - Post, - Res, - UseGuards, -} from "@nestjs/common"; +import { Controller, Inject } from "@nestjs/common"; import { QueryHandlerService } from "../handler/query.handler.service"; import { QueryExecuterService } from "../executer/query.executer.service"; -import { Response } from "express"; -import { ApiTokenGuard } from "src/api/guards/api-token.guard"; +import { BaseQueryController } from "../base/base-query.controller"; @Controller("command") -@UseGuards(ApiTokenGuard) -export class CommandController { +export class CommandController extends BaseQueryController { constructor( @Inject(QueryHandlerService) - private readonly queryHandlerService: QueryHandlerService, + queryHandlerService: QueryHandlerService, @Inject(QueryExecuterService) - private readonly queryExecuterService: QueryExecuterService - ) {} - - @Post("create") - async createQuery( - @Body() queryData: { projectToken: string; source: string } + queryExecuterService: QueryExecuterService ) { - return this.queryHandlerService.createQuery(queryData, true); + super(queryHandlerService, queryExecuterService); } - @Post("update/:id") - async updateQuery( - @Body() updateData: Partial<{ source: string }>, - @Inject("id") id: string - ) { - return this.queryHandlerService.updateQuery(id, updateData); - } - - @Post("/run/:token") - async runQuery( - @Param("token") token: string, - @Body() query: Record, - @Headers() headers: Record, - @Res() res: Response - ) { - const queryResult = await this.queryExecuterService.runQueryQueued( - token, - query, - headers - ); - - res.status(queryResult.statusCode); - - if (queryResult.statusCode === 302 && queryResult.headers["Location"]) { - res.redirect(queryResult.headers["Location"]); - return; - } - - for (const [key, value] of Object.entries(queryResult.headers)) { - res.setHeader(key, value); - } - - res.send(queryResult.response); + protected getIsCommand(): boolean { + return true; } } diff --git a/src/query/entities/function.entity.ts b/src/query/entities/function.entity.ts new file mode 100644 index 0000000..cc6374d --- /dev/null +++ b/src/query/entities/function.entity.ts @@ -0,0 +1,24 @@ +import { Project } from "../../project/entities/project.entity"; +import { + Column, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, +} from "typeorm"; + +@Entity("function") +@Index("IDX_FUNCTION_NAME_PROJECT", ["name", "project"], { unique: true }) +export class FunctionEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ type: "varchar", length: 255 }) + name: string; + + @Column({ type: "longtext" }) + source: string; + + @ManyToOne(() => Project, (project) => project.queries) + project: Project; +} diff --git a/src/query/executer/query.executer.service.ts b/src/query/executer/query.executer.service.ts index c708e8d..1d0913a 100644 --- a/src/query/executer/query.executer.service.ts +++ b/src/query/executer/query.executer.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Query } from "../entities/query.entity"; import { Repository } from "typeorm"; @@ -13,6 +13,7 @@ import { DatabaseManagerService } from "src/databaseManager/database/database.ma import { InjectQueue } from "@nestjs/bullmq"; import { QUEUE_NAMES } from "src/queue/constants"; import { Queue, QueueEvents } from "bullmq"; +import { FunctionService } from "src/project/function/function.service"; @Injectable() export class QueryExecuterService { @@ -21,6 +22,8 @@ export class QueryExecuterService { constructor( @InjectRepository(Query) readonly queryRepository: Repository, + @Inject(FunctionService) + private readonly functionService: FunctionService, readonly databaseManagerService: DatabaseManagerService, @InjectQueue(QUEUE_NAMES.QUERY) private queryQueue: Queue ) { @@ -110,6 +113,16 @@ export class QueryExecuterService { const moduleNames = importsParsed.filter((imp) => imp.type === "module"); const pluginNames = importsParsed.filter((imp) => imp.type === "plugin"); + const functionNames = importsParsed.filter( + (imp) => imp.type === "function" + ); + + const functions = ( + await this.functionService.findByNames( + query.project.id, + functionNames.map((fn) => fn.name) + ) + ).map((fn) => fn.source); const modules = moduleNames.map((mod) => { if (registeredModules[mod.name]) { @@ -134,24 +147,18 @@ export class QueryExecuterService { cpuTimeLimit: BigInt(5e9), modules: modules, plugins: plugins, + functions: functions, }); return await vm.init(); } - private checkResponse(obj: any): obj is QueryResponse { - return ( - obj !== null && - typeof obj === "object" && - typeof obj.statusCode === "number" && - typeof obj.response === "object" && - obj.response !== null && - typeof obj.headers === "object" && - obj.headers !== null && - Object.keys(obj.headers).every( - (key) => typeof obj.headers[key] === "string" - ) - ); + private checkResponse(obj: any): boolean { + if (obj?.statusCode && obj.response && typeof obj.statusCode === "number") { + return true; + } + + return false; } async onModuleDestroy() { diff --git a/src/query/handler/query.controller.ts b/src/query/handler/query.controller.ts index f9a8e1a..752e3ce 100644 --- a/src/query/handler/query.controller.ts +++ b/src/query/handler/query.controller.ts @@ -1,67 +1,20 @@ -import { - Body, - Controller, - Headers, - Inject, - Param, - Post, - Res, - UseGuards, -} from "@nestjs/common"; -import { Response } from "express"; +import { Controller, Inject } from "@nestjs/common"; import { QueryHandlerService } from "./query.handler.service"; -import { ApiTokenGuard } from "src/api/guards/api-token.guard"; import { QueryExecuterService } from "../executer/query.executer.service"; +import { BaseQueryController } from "../base/base-query.controller"; @Controller("query") -@UseGuards(ApiTokenGuard) -export class QueryController { +export class QueryController extends BaseQueryController { constructor( @Inject(QueryHandlerService) - private readonly queryHandlerService: QueryHandlerService, + queryHandlerService: QueryHandlerService, @Inject(QueryExecuterService) - private readonly queryExecuterService: QueryExecuterService - ) {} - - @Post("create") - async createQuery( - @Body() queryData: { projectToken: string; source: string } + queryExecuterService: QueryExecuterService ) { - return this.queryHandlerService.createQuery(queryData); + super(queryHandlerService, queryExecuterService); } - @Post("update/:id") - async updateQuery( - @Body() updateData: Partial<{ source: string }>, - @Inject("id") id: string - ) { - return this.queryHandlerService.updateQuery(id, updateData); - } - - @Post("/run/:token") - async runQuery( - @Param("token") token: string, - @Body() query: Record, - @Headers() headers: Record, - @Res() res: Response - ) { - const queryResult = await this.queryExecuterService.runQueryQueued( - token, - query, - headers - ); - - res.status(queryResult.statusCode); - - if (queryResult.statusCode === 302 && queryResult.headers["Location"]) { - res.redirect(queryResult.headers["Location"]); - return; - } - - for (const [key, value] of Object.entries(queryResult.headers)) { - res.setHeader(key, value); - } - - res.send(queryResult.response); + protected getIsCommand(): boolean { + return false; } } diff --git a/src/query/logs/entities/log.entity.ts b/src/query/logs/entities/log.entity.ts deleted file mode 100644 index 5b2d167..0000000 --- a/src/query/logs/entities/log.entity.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; -import { LogRecord } from "../logs.type"; - -@Entity("logs") -export class LogEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ - type: "longtext", - nullable: false, - transformer: { - to: (value: any) => JSON.stringify(value), - from: (value: any) => JSON.parse(value), - }, - }) - record: LogRecord; - - // TODO: projectId -} diff --git a/src/query/logs/logs.type.ts b/src/query/logs/logs.type.ts deleted file mode 100644 index e3195ae..0000000 --- a/src/query/logs/logs.type.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "FATAL"; - -export type LogLine = { - timestamp: string; - message: string; - level: LogLevel; -}; - -export type LogRecord = { - id: string; - lines: LogLine[]; -}; diff --git a/src/query/query.module.ts b/src/query/query.module.ts index a43d3de..632af81 100644 --- a/src/query/query.module.ts +++ b/src/query/query.module.ts @@ -9,6 +9,9 @@ import { DatabaseManagerModule } from "src/databaseManager/database.manager.modu import { CommandController } from "./command/command.controller"; import { ApiModule } from "src/api/api.module"; import { QueueModule } from "src/queue/queue.module"; +import { FunctionEntity } from "./entities/function.entity"; +import { FunctionService } from "src/project/function/function.service"; +import { FunctionController } from "src/project/function/function.controller"; @Module({ imports: [ @@ -16,10 +19,10 @@ import { QueueModule } from "src/queue/queue.module"; forwardRef(() => DatabaseManagerModule), forwardRef(() => ApiModule), forwardRef(() => QueueModule), - TypeOrmModule.forFeature([Query]), + TypeOrmModule.forFeature([Query, FunctionEntity]), ], - controllers: [QueryController, CommandController], - providers: [QueryExecuterService, QueryHandlerService], + controllers: [QueryController, CommandController, FunctionController], + providers: [QueryExecuterService, QueryHandlerService, FunctionService], exports: [QueryExecuterService, TypeOrmModule], }) export class QueryModule {} diff --git a/src/vm/vm.class.ts b/src/vm/vm.class.ts index f8a7453..9a0b361 100644 --- a/src/vm/vm.class.ts +++ b/src/vm/vm.class.ts @@ -9,6 +9,7 @@ export class Vm { private context: any; private jail: any; private plugins: Plugin[]; + private functions: string[]; private isolate: ivm.Isolate; private timeLimit?: bigint; private cpuTimeLimit?: bigint; @@ -19,12 +20,14 @@ export class Vm { cpuTimeLimit?: bigint; modules: VModule[]; plugins: Plugin[]; + functions: string[]; }) { this.memoryLimit = configs.memoryLimit; this.modules = configs.modules; this.plugins = configs.plugins; this.timeLimit = configs.timeLimit; this.cpuTimeLimit = configs.cpuTimeLimit; + this.functions = configs.functions; } async init(): Promise { @@ -38,6 +41,10 @@ export class Vm { await this.context.eval(mod.getSource()); } + for (const fn of this.functions) { + await this.context.eval(fn); + } + for (const plugin of this.plugins) { const pluginName = plugin.getName(); diff --git a/src/vm/vm.constants.ts b/src/vm/vm.constants.ts index f09a715..bb9d09a 100644 --- a/src/vm/vm.constants.ts +++ b/src/vm/vm.constants.ts @@ -35,6 +35,6 @@ export const registeredModules = { export type QueryResponse = { statusCode: number; - response: Record; + response: any; headers: Record; };