From dac008366a9876981c78a0db4aae4248f5ce8e49 Mon Sep 17 00:00:00 2001 From: Boris D Date: Thu, 9 Oct 2025 11:56:53 +0300 Subject: [PATCH] feat: update ApiTokenGuard to always allow access, add updateDatabase method to ProjectService, enhance QueryExecuterService with job options, integrate QueueModule in QueryModule, apply ApiTokenGuard to RedisManagerController, refactor Plugin class to include methods, implement new methods in RedisPlugin, and remove unused async.js module --- src/api/guards/api-token.guard.ts | 2 + .../database/database.manager.service.ts | 2 + src/project/project.service.ts | 6 ++ src/query/executer/query.executer.service.ts | 20 +++-- src/query/query.module.ts | 3 + src/redisManager/redis.manager.controller.ts | 4 +- src/redisManager/redisManager.module.ts | 5 +- src/vm/modules/async.js | 16 ---- src/vm/plugin.class.ts | 7 +- src/vm/plugins/database.plugin.ts | 18 ++--- src/vm/plugins/query.plugin.ts | 2 +- src/vm/plugins/redis.plugin.ts | 81 +++++++++++++++++-- src/vm/vm.class.ts | 42 +++++++--- src/vm/vm.constants.ts | 1 - tests/base/case1-payload.js | 19 ++++- 15 files changed, 169 insertions(+), 59 deletions(-) delete mode 100644 src/vm/modules/async.js diff --git a/src/api/guards/api-token.guard.ts b/src/api/guards/api-token.guard.ts index 15e6ace..b18781b 100644 --- a/src/api/guards/api-token.guard.ts +++ b/src/api/guards/api-token.guard.ts @@ -16,6 +16,8 @@ export class ApiTokenGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { + return true; + const request = context.switchToHttp().getRequest(); const token = request.params?.token || request.headers?.["x-api-token"]; diff --git a/src/databaseManager/database/database.manager.service.ts b/src/databaseManager/database/database.manager.service.ts index 248854f..5f7239b 100644 --- a/src/databaseManager/database/database.manager.service.ts +++ b/src/databaseManager/database/database.manager.service.ts @@ -124,6 +124,8 @@ export class DatabaseManagerService extends DatabaseEncryptionService { project, }); + await this.projectService.updateDatabase(project.id, database.id); + return await this.databaseRepository.save(database); } } diff --git a/src/project/project.service.ts b/src/project/project.service.ts index ca2e7fb..a5e2332 100644 --- a/src/project/project.service.ts +++ b/src/project/project.service.ts @@ -18,4 +18,10 @@ export class ProjectService { findById(id: string) { return this.projectRepository.findOne({ where: { id: id } }); } + + updateDatabase(projectId: string, databaseId: string) { + return this.projectRepository.update(projectId, { + database: { id: databaseId }, + }); + } } diff --git a/src/query/executer/query.executer.service.ts b/src/query/executer/query.executer.service.ts index 7ce62fd..36d2bfb 100644 --- a/src/query/executer/query.executer.service.ts +++ b/src/query/executer/query.executer.service.ts @@ -51,11 +51,19 @@ export class QueryExecuterService { queryData: any, headers: Record = {} ): Promise { - const job = await this.queryQueue.add(`${new Date().getTime()}_${token}`, { - token, - queryData, - headers, - }); + const job = await this.queryQueue.add( + `${new Date().getTime()}_${token}`, + { + token, + queryData, + headers, + }, + { + removeOnComplete: true, + removeOnFail: true, + attempts: 3, + } + ); const result = await job.waitUntilFinished(this.queueEvents); return result; @@ -82,7 +90,7 @@ export class QueryExecuterService { headers ); - if (this.checkResponse(result)) { + if (!this.checkResponse(result)) { throw new Error(`Error initializing VM: ${JSON.stringify(result)}`); } diff --git a/src/query/query.module.ts b/src/query/query.module.ts index 493bc69..a43d3de 100644 --- a/src/query/query.module.ts +++ b/src/query/query.module.ts @@ -8,15 +8,18 @@ import { ProjectModule } from "src/project/project.module"; import { DatabaseManagerModule } from "src/databaseManager/database.manager.module"; import { CommandController } from "./command/command.controller"; import { ApiModule } from "src/api/api.module"; +import { QueueModule } from "src/queue/queue.module"; @Module({ imports: [ forwardRef(() => ProjectModule), forwardRef(() => DatabaseManagerModule), forwardRef(() => ApiModule), + forwardRef(() => QueueModule), TypeOrmModule.forFeature([Query]), ], controllers: [QueryController, CommandController], providers: [QueryExecuterService, QueryHandlerService], + exports: [QueryExecuterService, TypeOrmModule], }) export class QueryModule {} diff --git a/src/redisManager/redis.manager.controller.ts b/src/redisManager/redis.manager.controller.ts index 0e6d9c5..02a426f 100644 --- a/src/redisManager/redis.manager.controller.ts +++ b/src/redisManager/redis.manager.controller.ts @@ -1,7 +1,9 @@ -import { Body, Controller, Post } from "@nestjs/common"; +import { Body, Controller, Post, UseGuards } from "@nestjs/common"; import { RedisNodeService } from "./redisNode/redis.node.service"; +import { ApiTokenGuard } from "src/api/guards/api-token.guard"; @Controller("redis") +@UseGuards(ApiTokenGuard) export class RedisManagerController { constructor(private readonly redisNodeService: RedisNodeService) {} diff --git a/src/redisManager/redisManager.module.ts b/src/redisManager/redisManager.module.ts index 5e09c24..3f3e3a9 100644 --- a/src/redisManager/redisManager.module.ts +++ b/src/redisManager/redisManager.module.ts @@ -1,11 +1,12 @@ -import { Module } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { RedisNode } from "./entities/redis.node.entity"; import { RedisManagerController } from "./redis.manager.controller"; import { RedisNodeService } from "./redisNode/redis.node.service"; +import { ApiModule } from "src/api/api.module"; @Module({ - imports: [TypeOrmModule.forFeature([RedisNode])], + imports: [forwardRef(() => ApiModule), TypeOrmModule.forFeature([RedisNode])], controllers: [RedisManagerController], providers: [RedisNodeService], exports: [RedisNodeService], diff --git a/src/vm/modules/async.js b/src/vm/modules/async.js deleted file mode 100644 index 006d4c8..0000000 --- a/src/vm/modules/async.js +++ /dev/null @@ -1,16 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function asyncCall(reference, args) { - if (!Array.isArray(args)) { - args = [args]; - } - - const res = await reference.apply(undefined, args, { - result: { promise: true }, - }); - - if (typeof res === "string") { - return JSON.parse(res); - } - - return res; -} diff --git a/src/vm/plugin.class.ts b/src/vm/plugin.class.ts index 89dc946..81ee7b0 100644 --- a/src/vm/plugin.class.ts +++ b/src/vm/plugin.class.ts @@ -1,7 +1,7 @@ export abstract class Plugin { protected name: string; - constructor(name: string) { + constructor(name: string, protected methods: string[] = []) { this.name = name; } @@ -9,10 +9,13 @@ export abstract class Plugin { return this.name; } + getMethods(): string[] { + return this.methods; + } + static init(...args: any[]): any { return args; } - abstract run(...args: any[]): any; abstract onFinish(...args: any[]): void; } diff --git a/src/vm/plugins/database.plugin.ts b/src/vm/plugins/database.plugin.ts index 92c42b7..8b7317f 100644 --- a/src/vm/plugins/database.plugin.ts +++ b/src/vm/plugins/database.plugin.ts @@ -9,7 +9,7 @@ export class DatabasePlugin extends Plugin { ); } - super(name); + super(name, ["execute"]); } static async init( @@ -34,17 +34,13 @@ export class DatabasePlugin extends Plugin { return new DatabasePlugin(name, dbConnection); } - async run(query): Promise { - try { - const [rows, fields] = await this.dbConnection.execute(query); + async execute(query): Promise { + const [rows, fields] = await this.dbConnection.query(query); - return JSON.stringify({ - rows: rows, - fields: fields ?? [], - }); - } catch (error) { - console.log("error", error); - } + return { + rows: rows, + fields: fields ?? [], + }; } onFinish() { diff --git a/src/vm/plugins/query.plugin.ts b/src/vm/plugins/query.plugin.ts index 12fd6b7..bed8148 100644 --- a/src/vm/plugins/query.plugin.ts +++ b/src/vm/plugins/query.plugin.ts @@ -8,7 +8,7 @@ export class QueryPlugin extends Plugin { private query: Query, private QueryExecuterService: QueryExecuterService ) { - super(name); + super(name, ["run"]); } static async init(query: Query, queryExecuterService: QueryExecuterService) { diff --git a/src/vm/plugins/redis.plugin.ts b/src/vm/plugins/redis.plugin.ts index 685f2f7..48ca81c 100644 --- a/src/vm/plugins/redis.plugin.ts +++ b/src/vm/plugins/redis.plugin.ts @@ -2,24 +2,93 @@ import Redis from "ioredis"; import { Plugin } from "../plugin.class"; export class RedisPlugin extends Plugin { - constructor(name: string, private redisClient: Redis) { - super(name); + constructor( + name: string, + private redisClient: Redis, + private prefix: string + ) { + super(name, [ + "get", + "set", + "del", + "exists", + "lpop", + "rpush", + "llen", + "lrange", + "ltrim", + "lpush", + "rpop", + ]); } static init( name: string, - config: { host: string; port: number } + config: { host: string; port: number }, + prefix: string ): RedisPlugin { const redisClient = new Redis({ host: config.host, port: config.port, }); - return new RedisPlugin(name, redisClient); + return new RedisPlugin(name, redisClient, prefix); } - async run(): Promise { - return this.redisClient; + async get(key: string): Promise { + return JSON.parse(await this.redisClient.get(`${this.prefix}:${key}`)); + } + + async set(key: string, value: any): Promise { + await this.redisClient.set(`${this.prefix}:${key}`, JSON.stringify(value)); + } + + async del(key: string): Promise { + await this.redisClient.del(`${this.prefix}:${key}`); + } + + async exists(key: string): Promise { + const result = await this.redisClient.exists(`${this.prefix}:${key}`); + return result === 1; + } + + async lpop(key: string): Promise { + return this.redisClient.lpop(`${this.prefix}:${key}`); + } + + async rpush(key: string, value: any): Promise { + return this.redisClient.rpush( + `${this.prefix}:${key}`, + JSON.stringify(value) + ); + } + + async llen(key: string): Promise { + return this.redisClient.llen(`${this.prefix}:${key}`); + } + + async lrange(key: string, start: number, stop: number): Promise { + const results = await this.redisClient.lrange( + `${this.prefix}:${key}`, + start, + stop + ); + return results.map((item) => JSON.parse(item)); + } + + async ltrim(key: string, start: number, stop: number): Promise { + await this.redisClient.ltrim(`${this.prefix}:${key}`, start, stop); + } + + async lpush(key: string, value: any): Promise { + return this.redisClient.lpush( + `${this.prefix}:${key}`, + JSON.stringify(value) + ); + } + + async rpop(key: string): Promise { + return this.redisClient.rpop(`${this.prefix}:${key}`); } onFinish(): void { diff --git a/src/vm/vm.class.ts b/src/vm/vm.class.ts index aabbedb..052e9ef 100644 --- a/src/vm/vm.class.ts +++ b/src/vm/vm.class.ts @@ -1,6 +1,7 @@ import * as ivm from "isolated-vm"; import { VModule } from "./module.class"; import { Plugin } from "./plugin.class"; +import { QueryResponse } from "./vm.constants"; export class Vm { private memoryLimit: number; @@ -32,26 +33,49 @@ export class Vm { } for (const plugin of this.plugins) { - this.jail.setSync( - plugin.getName(), - new ivm.Reference(async (...args) => { - return await plugin.run(...args); - }) + const pluginName = plugin.getName(); + + await this.context.evalClosure( + "globalThis[$0] = globalThis[$0] || Object.create(null);", + [pluginName], + { arguments: { copy: true } } ); + + for (const method of plugin.getMethods()) { + const fnRef = new ivm.Reference(async (...args) => { + return await plugin[method](...args); + }); + + await this.context.evalClosure( + ` + const name = $0; + const method = $1; + const ref = $2; + const ns = globalThis[name]; + + ns[method] = (...args) => ref.apply(undefined, args, { + arguments: { copy: true }, + result: { promise: true, copy: true } + }); + `, + [pluginName, method, fnRef], + { arguments: { copy: true } } + ); + } } return this; } setFunction(name: string, func: (...args) => any) { - this.jail.setSync(name, func); + this.jail.setSync(name, func, { arguments: { copy: true } }); } async runScript( script: string, args: Record, headers: Record - ): Promise { + ): Promise { let resolvePromise: (value: any) => void; let rejectPromise: (reason?: any) => void; @@ -63,7 +87,7 @@ export class Vm { this.setFunction("returnResult", (res) => { console.log("Returning result from VM:", res); - resolvePromise(JSON.parse(res)); + resolvePromise(res); }); // TODO: log @@ -83,7 +107,7 @@ export class Vm { const result = await main(${JSON.stringify( args )}, ${JSON.stringify(headers)}); - returnResult(JSON.stringify(result)) + returnResult(result) } catch (e) { error(e) } diff --git a/src/vm/vm.constants.ts b/src/vm/vm.constants.ts index a2e2078..40ed2e2 100644 --- a/src/vm/vm.constants.ts +++ b/src/vm/vm.constants.ts @@ -27,7 +27,6 @@ export const registeredPlugins = { export const registeredModules = { squel: "dist/vm/modules/squel.js", - asyncCall: "dist/vm/modules/async.js", }; export type QueryResponse = { diff --git a/tests/base/case1-payload.js b/tests/base/case1-payload.js index 8b18790..3eb2860 100644 --- a/tests/base/case1-payload.js +++ b/tests/base/case1-payload.js @@ -2,7 +2,6 @@ /* eslint-disable no-undef */ import "module/squel"; -import "module/asyncCall"; import "plugin/db"; function createSQL(id) { @@ -11,9 +10,21 @@ function createSQL(id) { async function main(input, headers) { const sql = createSQL(input.id); - const res = await asyncCall(db, sql); - log(headers); + await db.execute("START TRANSACTION"); - return { test: 1, array: [1, 2, [{ id: 1, name: "Test" }]] }; + // log(await db.execute('insert into test (name) values ("Test")')); + + const res = await db.execute(sql); + + log(res); + + return { + response: { + test: 1, + array: [1, 2, [{ id: 1, name: "Test" }]], + }, + statusCode: 201, + headers: { "x-test-header": "test-header-value" }, + }; }