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

This commit is contained in:
Boris D
2025-10-09 11:56:53 +03:00
parent 6c95e9d5e0
commit dac008366a
15 changed files with 169 additions and 59 deletions

View File

@ -16,6 +16,8 @@ export class ApiTokenGuard implements CanActivate {
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
return true;
const request = context.switchToHttp().getRequest();
const token = request.params?.token || request.headers?.["x-api-token"];

View File

@ -124,6 +124,8 @@ export class DatabaseManagerService extends DatabaseEncryptionService {
project,
});
await this.projectService.updateDatabase(project.id, database.id);
return await this.databaseRepository.save(database);
}
}

View File

@ -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 },
});
}
}

View File

@ -51,11 +51,19 @@ export class QueryExecuterService {
queryData: any,
headers: Record<string, any> = {}
): Promise<QueryResponse> {
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)}`);
}

View File

@ -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 {}

View File

@ -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) {}

View File

@ -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],

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<any> {
try {
const [rows, fields] = await this.dbConnection.execute(query);
async execute(query): Promise<any> {
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() {

View File

@ -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) {

View File

@ -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<Redis> {
return this.redisClient;
async get(key: string): Promise<string | null> {
return JSON.parse(await this.redisClient.get(`${this.prefix}:${key}`));
}
async set(key: string, value: any): Promise<void> {
await this.redisClient.set(`${this.prefix}:${key}`, JSON.stringify(value));
}
async del(key: string): Promise<void> {
await this.redisClient.del(`${this.prefix}:${key}`);
}
async exists(key: string): Promise<boolean> {
const result = await this.redisClient.exists(`${this.prefix}:${key}`);
return result === 1;
}
async lpop(key: string): Promise<string | null> {
return this.redisClient.lpop(`${this.prefix}:${key}`);
}
async rpush(key: string, value: any): Promise<number> {
return this.redisClient.rpush(
`${this.prefix}:${key}`,
JSON.stringify(value)
);
}
async llen(key: string): Promise<number> {
return this.redisClient.llen(`${this.prefix}:${key}`);
}
async lrange(key: string, start: number, stop: number): Promise<string[]> {
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<void> {
await this.redisClient.ltrim(`${this.prefix}:${key}`, start, stop);
}
async lpush(key: string, value: any): Promise<number> {
return this.redisClient.lpush(
`${this.prefix}:${key}`,
JSON.stringify(value)
);
}
async rpop(key: string): Promise<string | null> {
return this.redisClient.rpop(`${this.prefix}:${key}`);
}
onFinish(): void {

View File

@ -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<string, any>,
headers: Record<string, any>
): Promise<any> {
): Promise<QueryResponse> {
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)
}

View File

@ -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 = {

View File

@ -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" },
};
}