feat: enhance query handling with modules and plugins
- Added ManyToMany relationship for plugins in Query entity. - Updated QueryExecuterController to accept structured query data. - Enhanced QueryExecuterService to support plugin initialization and execution. - Implemented QueryHandlerService methods for creating and updating queries, modules, and plugins. - Introduced new endpoints for creating and adding modules and plugins to queries. - Created Plugin base class and DatabasePlugin implementation for database interactions. - Updated VM class to integrate plugin functionality during script execution. - Added test cases for project, query, module, and plugin operations.
This commit is contained in:
@ -3,6 +3,8 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { TestModule } from "../test/test.module";
|
||||
import { ApiModule } from "../api/api.module";
|
||||
import { QueryModule } from "src/query/query.module";
|
||||
import { ProjectModule } from "src/project/project.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -24,6 +26,8 @@ import { ApiModule } from "../api/api.module";
|
||||
autoLoadEntities: true,
|
||||
}),
|
||||
ApiModule,
|
||||
ProjectModule,
|
||||
QueryModule,
|
||||
TestModule,
|
||||
],
|
||||
controllers: [],
|
||||
|
||||
14
src/migrations/1758401762034-ModulesAndPluigns.ts
Normal file
14
src/migrations/1758401762034-ModulesAndPluigns.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class ModulesAndPluigns1758401762034 implements MigrationInterface {
|
||||
name = 'ModulesAndPluigns1758401762034'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE \`plugin\` (\`id\` varchar(36) NOT NULL, \`class\` varchar(255) NOT NULL, \`name\` varchar(255) NOT NULL, \`config\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE \`plugin\``);
|
||||
}
|
||||
|
||||
}
|
||||
@ -2,10 +2,12 @@ import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { Project } from "./entities/project.entity";
|
||||
import { ProjectService } from "./project.service";
|
||||
import { ProjectController } from "./project.controller";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Project])],
|
||||
controllers: [],
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
exports: [ProjectService],
|
||||
})
|
||||
export class ProjectModule {}
|
||||
|
||||
@ -14,4 +14,8 @@ export class ProjectService {
|
||||
const project = this.projectRepository.create({ name });
|
||||
return this.projectRepository.save(project);
|
||||
}
|
||||
|
||||
findByToken(token: string) {
|
||||
return this.projectRepository.findOne({ where: { token } });
|
||||
}
|
||||
}
|
||||
|
||||
20
src/query/entities/plugin.entity.ts
Normal file
20
src/query/entities/plugin.entity.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { Query } from "./query.entity";
|
||||
|
||||
@Entity("plugin")
|
||||
export class VmPlugin {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: false })
|
||||
class: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@ManyToMany(() => Query, (query) => query.plugins)
|
||||
queries: Query[];
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
config: string;
|
||||
}
|
||||
@ -7,6 +7,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { VMModule } from "./module.entity";
|
||||
import { VmPlugin } from "./plugin.entity";
|
||||
|
||||
@Entity("query")
|
||||
export class Query {
|
||||
@ -24,4 +25,7 @@ export class Query {
|
||||
|
||||
@ManyToMany(() => VMModule, (module) => module.queries)
|
||||
modules: VMModule[];
|
||||
|
||||
@ManyToMany(() => VmPlugin, (plugin) => plugin.queries)
|
||||
plugins: VmPlugin[];
|
||||
}
|
||||
|
||||
@ -9,7 +9,10 @@ export class QueryExecuterController {
|
||||
) {}
|
||||
|
||||
@Post("/run/:token")
|
||||
async runQuery(@Param("token") token: string, @Body() query: any) {
|
||||
async runQuery(
|
||||
@Param("token") token: string,
|
||||
@Body() query: Record<string, any>
|
||||
) {
|
||||
return this.queryExecuterService.runQuery(token, query);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { Query } from "../entities/query.entity";
|
||||
import { Repository } from "typeorm";
|
||||
import { Vm } from "../../vm/vm.class";
|
||||
import { VModule } from "../../vm/module.class";
|
||||
import { DatabasePlugin } from "src/vm/plugins/database.plugin";
|
||||
|
||||
@Injectable()
|
||||
export class QueryExecuterService {
|
||||
@ -21,18 +22,38 @@ export class QueryExecuterService {
|
||||
throw new Error("Query not found");
|
||||
}
|
||||
|
||||
const vm = this.createVm(query);
|
||||
const result = await vm.runScript(query.source);
|
||||
const vm = await this.createVm(query);
|
||||
const result = await vm.runScript(query.source, queryData);
|
||||
|
||||
return { message: "Query executed", result, query: queryData };
|
||||
}
|
||||
|
||||
private createVm(query: Query) {
|
||||
return new Vm({
|
||||
memoryLimit: 5,
|
||||
private async createVm(query: Query) {
|
||||
if (query.modules === undefined) {
|
||||
query.modules = [];
|
||||
}
|
||||
|
||||
if (query.plugins === undefined) {
|
||||
query.plugins = [];
|
||||
}
|
||||
|
||||
const vm = new Vm({
|
||||
memoryLimit: 128,
|
||||
modules: query.modules.map((module) => {
|
||||
return new VModule(module.name, module.sourcePath);
|
||||
}),
|
||||
plugins: query.plugins.map((plugin) => {
|
||||
switch (plugin.class) {
|
||||
case "DATABASE": {
|
||||
const config = JSON.parse(plugin.config);
|
||||
return DatabasePlugin.init(plugin.name, config);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown plugin class: ${plugin.class}`);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
return await vm.init();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Controller, Inject } from "@nestjs/common";
|
||||
import { Body, Controller, Inject, Post } from "@nestjs/common";
|
||||
import { QueryHandlerService } from "./query.handler.service";
|
||||
|
||||
@Controller("query")
|
||||
@ -7,4 +7,47 @@ export class QueryHandlerController {
|
||||
@Inject(QueryHandlerService)
|
||||
private readonly queryHandlerService: QueryHandlerService
|
||||
) {}
|
||||
|
||||
@Post("create")
|
||||
async createQuery(
|
||||
@Body() queryData: { projectToken: string; source: string }
|
||||
) {
|
||||
return this.queryHandlerService.createQuery(queryData);
|
||||
}
|
||||
|
||||
@Post("update/:id")
|
||||
async updateQuery(
|
||||
@Body() updateData: Partial<{ source: string }>,
|
||||
@Inject("id") id: string
|
||||
) {
|
||||
return this.queryHandlerService.updateQuery(id, updateData);
|
||||
}
|
||||
|
||||
@Post("module/create")
|
||||
async createModule(@Body() moduleData: { name: string; sourcePath: string }) {
|
||||
return this.queryHandlerService.createModule(moduleData);
|
||||
}
|
||||
|
||||
@Post("plugin/create")
|
||||
async createPlugin(
|
||||
@Body() pluginData: { name: string; class: string; config: string }
|
||||
) {
|
||||
return this.queryHandlerService.createPlugin(pluginData);
|
||||
}
|
||||
|
||||
@Post("module/add")
|
||||
async addModuleToQuery(@Body() data: { queryId: string; moduleId: string }) {
|
||||
return this.queryHandlerService.addModuleToQuery(
|
||||
data.queryId,
|
||||
data.moduleId
|
||||
);
|
||||
}
|
||||
|
||||
@Post("plugin/add")
|
||||
async addPluginToQuery(@Body() data: { queryId: string; pluginId: string }) {
|
||||
return this.queryHandlerService.addPluginToQuery(
|
||||
data.queryId,
|
||||
data.pluginId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,106 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { Repository } from "typeorm";
|
||||
import { Query } from "../entities/query.entity";
|
||||
import { VMModule } from "../entities/module.entity";
|
||||
import { VmPlugin } from "../entities/plugin.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { ProjectService } from "src/project/project.service";
|
||||
|
||||
@Injectable()
|
||||
export class QueryHandlerService {}
|
||||
export class QueryHandlerService {
|
||||
constructor(
|
||||
@InjectRepository(Query)
|
||||
private readonly queryRepository: Repository<Query>,
|
||||
@InjectRepository(VMModule)
|
||||
private readonly moduleRepository: Repository<VMModule>,
|
||||
@InjectRepository(VmPlugin)
|
||||
private readonly pluginRepository: Repository<VmPlugin>,
|
||||
@Inject(ProjectService)
|
||||
private readonly projectService: ProjectService
|
||||
) {}
|
||||
|
||||
async createQuery(queryData: { projectToken: string; source: string }) {
|
||||
const project = await this.projectService.findByToken(
|
||||
queryData.projectToken
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
queryData["project"] = project;
|
||||
delete queryData.projectToken;
|
||||
|
||||
const query = this.queryRepository.create(queryData);
|
||||
return this.queryRepository.save(query);
|
||||
}
|
||||
|
||||
async updateQuery(id: string, updateData: Partial<Query>) {
|
||||
const query = await this.queryRepository.findOne({ where: { id } });
|
||||
|
||||
if (!query) {
|
||||
throw new Error("Query not found");
|
||||
}
|
||||
|
||||
Object.assign(query, updateData);
|
||||
return this.queryRepository.save(query);
|
||||
}
|
||||
|
||||
async createModule(moduleData: { name: string; sourcePath: string }) {
|
||||
const module = this.moduleRepository.create(moduleData);
|
||||
return this.moduleRepository.save(module);
|
||||
}
|
||||
|
||||
async createPlugin(pluginData: {
|
||||
name: string;
|
||||
class: string;
|
||||
config: string;
|
||||
}) {
|
||||
const plugin = this.pluginRepository.create(pluginData);
|
||||
return this.pluginRepository.save(plugin);
|
||||
}
|
||||
|
||||
async addModuleToQuery(queryId: string, moduleId: string) {
|
||||
const query = await this.queryRepository.findOne({
|
||||
where: { id: queryId },
|
||||
relations: ["modules"],
|
||||
});
|
||||
|
||||
if (!query) {
|
||||
throw new Error("Query not found");
|
||||
}
|
||||
|
||||
const module = await this.moduleRepository.findOne({
|
||||
where: { id: moduleId },
|
||||
});
|
||||
|
||||
if (!module) {
|
||||
throw new Error("Module not found");
|
||||
}
|
||||
|
||||
query.modules.push(module);
|
||||
return this.queryRepository.save(query);
|
||||
}
|
||||
|
||||
async addPluginToQuery(queryId: string, pluginId: string) {
|
||||
const query = await this.queryRepository.findOne({
|
||||
where: { id: queryId },
|
||||
relations: ["plugins"],
|
||||
});
|
||||
|
||||
if (!query) {
|
||||
throw new Error("Query not found");
|
||||
}
|
||||
|
||||
const plugin = await this.pluginRepository.findOne({
|
||||
where: { id: pluginId },
|
||||
});
|
||||
|
||||
if (!plugin) {
|
||||
throw new Error("Plugin not found");
|
||||
}
|
||||
|
||||
query.plugins.push(plugin);
|
||||
return this.queryRepository.save(query);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { Query } from "./entities/query.entity";
|
||||
import { VMModule } from "./entities/module.entity";
|
||||
@ -6,9 +6,14 @@ import { QueryExecuterController } from "./executer/query.executer.controller";
|
||||
import { QueryHandlerController } from "./handler/query.handler.controller";
|
||||
import { QueryExecuterService } from "./executer/query.executer.service";
|
||||
import { QueryHandlerService } from "./handler/query.handler.service";
|
||||
import { VmPlugin } from "./entities/plugin.entity";
|
||||
import { ProjectModule } from "src/project/project.module";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Query, VMModule])],
|
||||
imports: [
|
||||
forwardRef(() => ProjectModule),
|
||||
TypeOrmModule.forFeature([Query, VMModule, VmPlugin]),
|
||||
],
|
||||
controllers: [QueryExecuterController, QueryHandlerController],
|
||||
providers: [QueryExecuterService, QueryHandlerService],
|
||||
})
|
||||
|
||||
18
src/vm/plugin.class.ts
Normal file
18
src/vm/plugin.class.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export abstract class Plugin {
|
||||
protected name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
static init(...args: any[]): any {
|
||||
return args;
|
||||
}
|
||||
|
||||
abstract run(...args: any[]): any;
|
||||
abstract onFinish(...args: any[]): void;
|
||||
}
|
||||
5
src/vm/plugins.constants.ts
Normal file
5
src/vm/plugins.constants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { DatabasePlugin } from "./plugins/database.plugin";
|
||||
|
||||
export const PluginClass = {
|
||||
DATABASE: DatabasePlugin,
|
||||
};
|
||||
45
src/vm/plugins/database.plugin.ts
Normal file
45
src/vm/plugins/database.plugin.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Plugin } from "../plugin.class";
|
||||
import * as mysql from "mysql2/promise";
|
||||
|
||||
export class DatabasePlugin extends Plugin {
|
||||
constructor(name: string, private dbConnection: any) {
|
||||
if (!dbConnection || typeof dbConnection.execute !== "function") {
|
||||
throw new Error(
|
||||
"Invalid database connection: must be a mysql2 connection with execute method"
|
||||
);
|
||||
}
|
||||
|
||||
super(name);
|
||||
}
|
||||
|
||||
static init(
|
||||
name: string,
|
||||
config: {
|
||||
host: string;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
): DatabasePlugin {
|
||||
const dbConnection = mysql.createConnection({
|
||||
host: config.host,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
});
|
||||
|
||||
return new DatabasePlugin(name, dbConnection);
|
||||
}
|
||||
|
||||
async run(query): Promise<{
|
||||
rows: any[];
|
||||
fields: any[];
|
||||
}> {
|
||||
const [rows, fields] = await this.dbConnection.execute(query);
|
||||
return { rows, fields };
|
||||
}
|
||||
|
||||
onFinish() {
|
||||
this.dbConnection.end();
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,28 @@
|
||||
import * as ivm from "isolated-vm";
|
||||
import { VModule } from "./module.class";
|
||||
import { Plugin } from "./plugin.class";
|
||||
|
||||
export class Vm {
|
||||
private memoryLimit: number;
|
||||
private modules: VModule[];
|
||||
private context: any;
|
||||
private jail: any;
|
||||
private plugins: Plugin[];
|
||||
private isolate: ivm.Isolate;
|
||||
|
||||
constructor(configs: { memoryLimit: number; modules: VModule[] }) {
|
||||
constructor(configs: {
|
||||
memoryLimit: number;
|
||||
modules: VModule[];
|
||||
plugins: Plugin[];
|
||||
}) {
|
||||
this.memoryLimit = configs.memoryLimit;
|
||||
this.modules = configs.modules;
|
||||
this.plugins = configs.plugins;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const isolate = new ivm.Isolate({ memoryLimit: this.memoryLimit });
|
||||
this.context = isolate.createContext();
|
||||
async init(): Promise<Vm> {
|
||||
this.isolate = new ivm.Isolate({ memoryLimit: this.memoryLimit });
|
||||
this.context = await this.isolate.createContext();
|
||||
this.jail = this.context.global;
|
||||
|
||||
this.jail.set("global", this.jail.derefInto());
|
||||
@ -22,13 +30,22 @@ export class Vm {
|
||||
for (const mod of this.modules) {
|
||||
this.jail.setSync(mod.getName(), mod.getSource());
|
||||
}
|
||||
|
||||
for (const plugin of this.plugins) {
|
||||
this.jail.setSync(
|
||||
plugin.getName(),
|
||||
new ivm.Reference(plugin.run.bind(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setFunction(name: string, func: (...args) => any) {
|
||||
this.jail.setSync(name, func);
|
||||
}
|
||||
|
||||
async runScript(script: string) {
|
||||
async runScript(script: string, args: Record<string, any>): Promise<any> {
|
||||
let resolvePromise: (value: any) => void;
|
||||
let rejectPromise: (reason?: any) => void;
|
||||
|
||||
@ -37,11 +54,14 @@ export class Vm {
|
||||
rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.setFunction("result", (...args) => {
|
||||
this.setFunction("returnResult", (...args) => {
|
||||
console.log("Script result:", args);
|
||||
|
||||
resolvePromise(args);
|
||||
});
|
||||
|
||||
// TODO: log
|
||||
|
||||
this.setFunction("error", (error: any) => {
|
||||
console.error("Script error:", error);
|
||||
rejectPromise(error);
|
||||
@ -51,19 +71,25 @@ export class Vm {
|
||||
(async () => {
|
||||
${script}
|
||||
try {
|
||||
const result = await main()
|
||||
result(result)
|
||||
const result = await main(${JSON.stringify(args)});
|
||||
returnResult(result)
|
||||
} catch (e) {
|
||||
error(e)
|
||||
}
|
||||
})()
|
||||
`;
|
||||
|
||||
const compiledScript = await this.context.isolate.compileScript(
|
||||
scriptWithResult
|
||||
);
|
||||
const compiledScript = await this.isolate.compileScript(scriptWithResult);
|
||||
|
||||
compiledScript.run(this.context);
|
||||
this.onFinish();
|
||||
|
||||
return await resultPromise;
|
||||
}
|
||||
|
||||
onFinish() {
|
||||
this.plugins.forEach((plugin) => {
|
||||
plugin.onFinish();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user