From 08a62fa2c5b6f845a105d97df8f8843255638f25 Mon Sep 17 00:00:00 2001 From: lborv Date: Sat, 11 Oct 2025 17:59:06 +0300 Subject: [PATCH] feat: add isAdmin column to token entity; implement migration for isAdmin; enhance logging and error handling in query execution; update query plugin to support new logging structure --- src/api/entities/token.entity.ts | 4 +- src/migrations/1760191339215-adminToken.ts | 15 ++++++++ src/query/base/base-query.controller.ts | 39 +++++++++++++------ src/query/executer/query.executer.service.ts | 13 ++++--- src/query/logger/entities/log.entity.ts | 8 +++- src/vm/plugin.class.ts | 17 +++++++++ src/vm/plugins/database.plugin.ts | 2 +- src/vm/plugins/query.plugin.ts | 14 +++---- src/vm/vm.class.ts | 40 +++++++++++++++----- tests/base/case1-payload.js | 10 +++-- tests/base/case1.ts | 22 ++++++----- tests/functions/createQuery.ts | 2 +- tests/functions/runQuery.ts | 4 +- 13 files changed, 135 insertions(+), 55 deletions(-) create mode 100644 src/migrations/1760191339215-adminToken.ts diff --git a/src/api/entities/token.entity.ts b/src/api/entities/token.entity.ts index 77b8c40..0a018c4 100644 --- a/src/api/entities/token.entity.ts +++ b/src/api/entities/token.entity.ts @@ -9,8 +9,8 @@ export class Token { @Column({ type: "tinyint", default: 1 }) isActive: boolean; - // @Column({ type: "tinyint", default: 0 }) - // isAdmin: boolean; + @Column({ type: "tinyint", default: 0 }) + isAdmin: boolean; @ManyToOne(() => Project, (project) => project.apiTokens) project: Project; diff --git a/src/migrations/1760191339215-adminToken.ts b/src/migrations/1760191339215-adminToken.ts new file mode 100644 index 0000000..7283c6f --- /dev/null +++ b/src/migrations/1760191339215-adminToken.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AdminToken1760191339215 implements MigrationInterface { + name = "AdminToken1760191339215"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`token\` ADD \`isAdmin\` tinyint NOT NULL DEFAULT '0'` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`token\` DROP COLUMN \`isAdmin\``); + } +} diff --git a/src/query/base/base-query.controller.ts b/src/query/base/base-query.controller.ts index 896523b..273d049 100644 --- a/src/query/base/base-query.controller.ts +++ b/src/query/base/base-query.controller.ts @@ -15,6 +15,7 @@ import { QueryExecuterService } from "../executer/query.executer.service"; import { QueryGuard } from "src/query/guards/query.guard"; import { LoggerService } from "../logger/logger.service"; import { TLogType } from "../logger/logger.types"; +import { QueryResponse } from "src/vm/vm.constants"; @UseGuards(ApiTokenGuard) export abstract class BaseQueryController { @@ -53,6 +54,7 @@ export abstract class BaseQueryController { @Headers() headers: Record, @Res() res: Response ) { + let queryResult: QueryResponse; const loggerTraceId = headers["x-trace-id"] || LoggerService.generateTraceId(); const log = LoggerService.log( @@ -69,20 +71,35 @@ export abstract class BaseQueryController { { content: "", type: TLogType.info, timeStamp: new Date().getTime() } ); - const queryResult = await this.queryExecuterService.runQueryQueued( - id, - query, - log, - headers, - headers.cookie.split("; ").reduce((acc, cookie) => { - const [key, value] = cookie.split("="); - acc[key] = value; - return acc; - }, {}) - ); + try { + queryResult = await this.queryExecuterService.runQueryQueued( + id, + query, + log, + headers, + headers.cookie.split("; ").reduce((acc, cookie) => { + const [key, value] = cookie.split("="); + acc[key] = value; + return acc; + }, {}) + ); + } catch (error) { + log.content.push({ + content: `Query execution failed: ${error.message}`, + type: TLogType.error, + timeStamp: new Date().getTime(), + }); + log.endTime = new Date().getTime(); + await this.loggerService.create(log.traceId, log); + res.status(500).send({ error: "Internal Server Error" }); + return; + } if (queryResult?.log) { queryResult.log.endTime = new Date().getTime(); + const res = JSON.parse(JSON.stringify(queryResult)); + delete res.log; + queryResult.log.response = res; await this.loggerService.create(queryResult.log.traceId, queryResult.log); } diff --git a/src/query/executer/query.executer.service.ts b/src/query/executer/query.executer.service.ts index 040516d..eecf788 100644 --- a/src/query/executer/query.executer.service.ts +++ b/src/query/executer/query.executer.service.ts @@ -1,5 +1,5 @@ import { RedisNodeService } from "./../../redisManager/redisNode/redis.node.service"; -import { Inject, Injectable } from "@nestjs/common"; +import { Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Query } from "../entities/query.entity"; import { Repository } from "typeorm"; @@ -16,7 +16,8 @@ import { QUEUE_NAMES } from "src/queue/constants"; import { Queue, QueueEvents } from "bullmq"; import { FunctionService } from "src/query/function/function.service"; import { SessionService } from "../session/session.service"; -import { TLog } from "../logger/logger.types"; +import { TLog, TLogType } from "../logger/logger.types"; +import { LoggerService } from "../logger/logger.service"; @Injectable() export class QueryExecuterService { @@ -104,12 +105,11 @@ export class QueryExecuterService { cookies["x-session-id"] = session.sessionId; } - const vm = await this.createVm(query, cookies["x-session-id"]); + const vm = await this.createVm(query, log, cookies["x-session-id"]); const result = await vm.runScript( this.clearImports(query.source), queryData, - headers, - log + headers ); if (!this.checkResponse(result)) { @@ -129,7 +129,7 @@ export class QueryExecuterService { return result; } - private async createVm(query: Query, sessionId: string = null) { + private async createVm(query: Query, log: TLog, sessionId: string = null) { const imports = this.parseImports(query.source); const importsParsed = imports.map((imp) => { const item = imp.split("/"); @@ -179,6 +179,7 @@ export class QueryExecuterService { modules: modules, plugins: plugins, functions: functions, + log, }); return await vm.init(); diff --git a/src/query/logger/entities/log.entity.ts b/src/query/logger/entities/log.entity.ts index 2607283..b031526 100644 --- a/src/query/logger/entities/log.entity.ts +++ b/src/query/logger/entities/log.entity.ts @@ -9,7 +9,13 @@ export class Log { @Column({ type: "varchar", length: 255 }) traceId: string; - @Column({ type: "longtext" }) + @Column({ + type: "longtext", + transformer: { + to: (value: TLog) => JSON.stringify(value), + from: (value: string) => JSON.parse(value) as TLog, + }, + }) content: TLog; @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) diff --git a/src/vm/plugin.class.ts b/src/vm/plugin.class.ts index 81ee7b0..aaee4b3 100644 --- a/src/vm/plugin.class.ts +++ b/src/vm/plugin.class.ts @@ -1,5 +1,10 @@ +import { TLog } from "src/query/logger/logger.types"; + export abstract class Plugin { protected name: string; + protected log: TLog; + protected cookies: Record; + protected headers: Record; constructor(name: string, protected methods: string[] = []) { this.name = name; @@ -13,6 +18,18 @@ export abstract class Plugin { return this.methods; } + setLog(log: TLog) { + this.log = log; + } + + setHeaders(headers: Record) { + this.headers = headers; + } + + setCookies(cookies: Record) { + this.cookies = cookies; + } + static init(...args: any[]): any { return args; } diff --git a/src/vm/plugins/database.plugin.ts b/src/vm/plugins/database.plugin.ts index 1589d51..63aaeea 100644 --- a/src/vm/plugins/database.plugin.ts +++ b/src/vm/plugins/database.plugin.ts @@ -35,7 +35,7 @@ export class DatabasePlugin extends Plugin { enableKeepAlive: true, }); - await dbConnection.query("SET SESSION MAX_EXECUTION_TIME=2000;"); + // await dbConnection.query("SET SESSION MAX_EXECUTION_TIME=2000;"); return new DatabasePlugin(name, dbConnection); } diff --git a/src/vm/plugins/query.plugin.ts b/src/vm/plugins/query.plugin.ts index e5dcde2..43547ad 100644 --- a/src/vm/plugins/query.plugin.ts +++ b/src/vm/plugins/query.plugin.ts @@ -15,11 +15,7 @@ export class QueryPlugin extends Plugin { return new QueryPlugin("query", query, queryExecuterService); } - async run(data: { - token: string; - queryData: any; - headers?: Record; - }): Promise { + async run(id: string, data: any): Promise { const query = await this.QueryExecuterService.queryRepository.findOne({ where: { id: this.query.id }, }); @@ -35,9 +31,11 @@ export class QueryPlugin extends Plugin { } return await this.QueryExecuterService.runQuery( - query.id, - data.queryData, - data.headers + id, + data, + this.headers, + this.cookies, + this.log ); } diff --git a/src/vm/vm.class.ts b/src/vm/vm.class.ts index d49b6b9..0ae56c7 100644 --- a/src/vm/vm.class.ts +++ b/src/vm/vm.class.ts @@ -16,7 +16,9 @@ export class Vm { private isolate: ivm.Isolate; private timeLimit?: bigint; private cpuTimeLimit?: bigint; - private sessionId: string | null; + private log: TLog; + private headers?: Record; + private cookies?: Record; constructor(configs: { memoryLimit: number; @@ -25,6 +27,9 @@ export class Vm { modules: VModule[]; plugins: Plugin[]; functions: string[]; + log: TLog; + headers?: Record; + cookies?: Record; }) { this.memoryLimit = configs.memoryLimit; this.modules = configs.modules; @@ -32,6 +37,9 @@ export class Vm { this.timeLimit = configs.timeLimit; this.cpuTimeLimit = configs.cpuTimeLimit; this.functions = configs.functions; + this.log = configs.log; + this.headers = configs.headers; + this.cookies = configs.cookies; } async init(): Promise { @@ -60,7 +68,17 @@ export class Vm { for (const method of plugin.getMethods()) { const fnRef = new ivm.Reference(async (...args) => { - return await plugin[method](...args); + plugin.setLog(this.log); + plugin.setHeaders(this.headers); + plugin.setCookies(this.cookies); + const result = await plugin[method](...args); + + if (result && result.log) { + this.log = result.log; + delete result.log; + } + + return result; }); await this.context.evalClosure( @@ -86,8 +104,7 @@ export class Vm { async runScript( script: string, args: Record, - headers: Record, - log: TLog + headers: Record ): Promise { let resolvePromise: (value: any) => void; let rejectPromise: (reason?: any) => void; @@ -98,13 +115,11 @@ export class Vm { }); this.setFunction("returnResult", (res) => { - resolvePromise({ ...res, log }); + resolvePromise({ ...res, log: this.log }); }); this.setFunction("log", (...args) => { - if (!log) { - return; - } + console.log(...args); const logType = args.find( (arg) => @@ -114,7 +129,7 @@ export class Vm { Object.values(TLogType).includes(arg.type) ); - log = LoggerService.log(log, { + this.log = LoggerService.log(this.log, { content: args .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))) .join(" "), @@ -124,7 +139,12 @@ export class Vm { }); this.setFunction("error", (error: any) => { - console.error("Script error:", error); + LoggerService.log(this.log, { + content: error?.stack || error?.toString() || "Unknown error", + type: TLogType.error, + timeStamp: new Date().getTime(), + }); + rejectPromise(error); }); diff --git a/tests/base/case1-payload.js b/tests/base/case1-payload.js index ce448a3..0289d47 100644 --- a/tests/base/case1-payload.js +++ b/tests/base/case1-payload.js @@ -5,6 +5,8 @@ import "module/squel"; import "plugin/db"; import "plugin/axios"; import "plugin/session"; +import "plugin/query"; +import { query } from "express"; function createSQL(id) { return squel.select().from("test").where("id = ?", id).toString(); @@ -15,6 +17,8 @@ async function main(input, headers) { await db.query("START TRANSACTION"); + await query.run("d079b709-eba3-49be-9f84-08ac6a64a8a8", input); + // log(await db.query('insert into test (name) values ("Test")')); log(new Date().toISOString()); @@ -28,9 +32,9 @@ async function main(input, headers) { await session.set("test", 1); - const res = await db.query(` - SELECT SLEEP(10000); -`); + // const res = await db.query(` + // SELECT SLEEP(10000); + // `); log(new Date().toISOString()); diff --git a/tests/base/case1.ts b/tests/base/case1.ts index ff1332c..ebd2563 100644 --- a/tests/base/case1.ts +++ b/tests/base/case1.ts @@ -2,11 +2,11 @@ // import createDatabase from "../functions/createDatabase"; // import createDatabaseNode from "../functions/createDatabaseNode"; // import createProject from "../functions/createProject"; -import createQuery from "../functions/createQuery"; +// import createQuery from "../functions/createQuery"; // import databaseMigrationUp from "../functions/databaseMigrationUp"; import runQuery from "../functions/runQuery"; -import * as fs from "fs"; -import * as path from "path"; +// import * as fs from "fs"; +// import * as path from "path"; (async () => { try { @@ -34,15 +34,17 @@ import * as path from "path"; // console.log("Migrations applied:", migrationResult); - const payloadPath = path.join(__dirname, "case1-payload.js"); - const query = await createQuery( - { token: "04c38f93-f2fb-4d2c-a8e2-791effa35239" }, - fs.readFileSync(payloadPath, { encoding: "utf-8" }) - ); + // const payloadPath = path.join(__dirname, "case1-payload.js"); + // const query = await createQuery( + // { token: "c69c2c75-9b30-4aa5-9641-0a931f5aad40" }, + // fs.readFileSync(payloadPath, { encoding: "utf-8" }) + // ); - console.log(query); + // console.log(query); - const result = await runQuery(query.id, { id: 1 }); + const result = await runQuery("66e651f0-261b-4ebd-9749-077abffaddc2", { + id: 1, + }); console.log("Query Result:", result.data); console.log("headers:", result.headers); diff --git a/tests/functions/createQuery.ts b/tests/functions/createQuery.ts index a1629ca..6f6c9da 100644 --- a/tests/functions/createQuery.ts +++ b/tests/functions/createQuery.ts @@ -9,7 +9,7 @@ export default async (project: { token: string }, source: string) => { source, projectToken: project.token, }, - { headers: { "x-api-token": "efbeccd6-dde1-47dc-b3aa-4fbd773d5429" } } + { headers: { "x-api-token": "43c2e96e-af25-4467-9103-1479daa6288d" } } ); return response.data; } catch (error) { diff --git a/tests/functions/runQuery.ts b/tests/functions/runQuery.ts index a81856f..437dbfe 100644 --- a/tests/functions/runQuery.ts +++ b/tests/functions/runQuery.ts @@ -8,8 +8,8 @@ export default async (token: string, queryData: Record) => { queryData, { headers: { - "x-api-token": "efbeccd6-dde1-47dc-b3aa-4fbd773d5429", - Cookie: `x-session-id=psaCHcYFMrt6RnUz_1760094020243`, + "x-api-token": "43c2e96e-af25-4467-9103-1479daa6288d", + Cookie: `x-session-id=gTEd90aRJFmLzJKu_1760193754588`, }, } );