This commit is contained in:
Boris D
2025-09-22 18:35:02 +03:00
parent 51f8b0d773
commit fbbbd61838
23 changed files with 282 additions and 67 deletions

View File

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Defaults1758550333691 implements MigrationInterface {
name = 'Defaults1758550333691'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`module\` DROP COLUMN \`isInjectable\``);
await queryRunner.query(`ALTER TABLE \`plugin\` ADD \`queryId\` uuid NULL`);
await queryRunner.query(`ALTER TABLE \`plugin\` ADD CONSTRAINT \`FK_5162d18c3653d35ff4d104dd940\` FOREIGN KEY (\`queryId\`) REFERENCES \`query\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`plugin\` DROP FOREIGN KEY \`FK_5162d18c3653d35ff4d104dd940\``);
await queryRunner.query(`ALTER TABLE \`plugin\` DROP COLUMN \`queryId\``);
await queryRunner.query(`ALTER TABLE \`module\` ADD \`isInjectable\` tinyint NOT NULL DEFAULT 1`);
}
}

View File

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class QueryModulesJoin1758551707113 implements MigrationInterface {
name = 'QueryModulesJoin1758551707113'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`query_modules_module\` (\`queryId\` uuid NOT NULL, \`moduleId\` uuid NOT NULL, INDEX \`IDX_12121324c524e12538de4948ce\` (\`queryId\`), INDEX \`IDX_7fb42dbc85874aafbd4bfb1c10\` (\`moduleId\`), PRIMARY KEY (\`queryId\`, \`moduleId\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`query_modules_module\` ADD CONSTRAINT \`FK_12121324c524e12538de4948cee\` FOREIGN KEY (\`queryId\`) REFERENCES \`query\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE \`query_modules_module\` ADD CONSTRAINT \`FK_7fb42dbc85874aafbd4bfb1c101\` FOREIGN KEY (\`moduleId\`) REFERENCES \`module\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`query_modules_module\` DROP FOREIGN KEY \`FK_7fb42dbc85874aafbd4bfb1c101\``);
await queryRunner.query(`ALTER TABLE \`query_modules_module\` DROP FOREIGN KEY \`FK_12121324c524e12538de4948cee\``);
await queryRunner.query(`DROP INDEX \`IDX_7fb42dbc85874aafbd4bfb1c10\` ON \`query_modules_module\``);
await queryRunner.query(`DROP INDEX \`IDX_12121324c524e12538de4948ce\` ON \`query_modules_module\``);
await queryRunner.query(`DROP TABLE \`query_modules_module\``);
}
}

View File

@ -1,14 +1,14 @@
import { Body, Controller, Inject, Put } from "@nestjs/common";
import { ProjectService } from "./project.service";
@Controller("")
@Controller("project")
export class ProjectController {
constructor(
@Inject(ProjectService)
private readonly projectService: ProjectService
) {}
@Put("project/create")
@Put("create")
createProject(@Body() body: { name: string }) {
return this.projectService.create(body.name);
}

View File

@ -12,9 +12,6 @@ export class VMModule {
@Column({ type: "varchar", length: 255 })
name: string;
@Column({ type: "tinyint", default: 1 })
isInjectable: number;
@ManyToMany(() => Query, (query) => query.modules)
queries: Query[];
}

View File

@ -1,4 +1,4 @@
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Query } from "./query.entity";
@Entity("plugin")
@ -12,8 +12,8 @@ export class VmPlugin {
@Column({ type: "varchar", length: 255, nullable: false })
name: string;
@ManyToMany(() => Query, (query) => query.plugins)
queries: Query[];
@ManyToOne(() => Query, (query) => query.plugins)
query: Query;
@Column({ type: "varchar", length: 255 })
config: string;

View File

@ -4,7 +4,9 @@ import {
Entity,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
JoinTable,
} from "typeorm";
import { VMModule } from "./module.entity";
import { VmPlugin } from "./plugin.entity";
@ -24,8 +26,13 @@ export class Query {
isActive: number;
@ManyToMany(() => VMModule, (module) => module.queries)
@JoinTable({
name: "query_modules_module",
joinColumn: { name: "queryId", referencedColumnName: "id" },
inverseJoinColumn: { name: "moduleId", referencedColumnName: "id" },
})
modules: VMModule[];
@ManyToMany(() => VmPlugin, (plugin) => plugin.queries)
@OneToMany(() => VmPlugin, (plugin) => plugin.query)
plugins: VmPlugin[];
}

View File

@ -16,6 +16,7 @@ export class QueryExecuterService {
async runQuery(token: string, queryData: any) {
const query = await this.queryRepository.findOne({
where: { id: token },
relations: ["modules", "plugins"],
});
if (!query) {
@ -42,16 +43,18 @@ export class QueryExecuterService {
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);
plugins: await Promise.all(
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}`);
}
default:
throw new Error(`Unknown plugin class: ${plugin.class}`);
}
}),
})
),
});
return await vm.init();

View File

@ -19,6 +19,84 @@ export class QueryHandlerService {
private readonly projectService: ProjectService
) {}
private async createOrGetVmModule(
name: string,
sourcePath: string
): Promise<VMModule> {
const vmModule = await this.moduleRepository.findOne({
where: {
name,
},
});
if (vmModule) {
return vmModule;
}
const newModule = this.moduleRepository.create({
sourcePath,
name,
});
return this.moduleRepository.save(newModule);
}
private async createOrGetPlugin(
name: string,
className: string,
query: Query,
config: Record<string, any>
): Promise<VmPlugin> {
const plugin = await this.pluginRepository.findOne({
where: {
name,
query: {
id: query.id,
},
},
});
if (plugin) {
return plugin;
}
const newPlugin = this.pluginRepository.create({
name,
class: className,
config: JSON.stringify(config),
query,
});
return this.pluginRepository.save(newPlugin);
}
// TODO: make it nice
async createDefaults(query: Query): Promise<void> {
const asyncCallModule = await this.createOrGetVmModule(
"asyncCall",
"dist/vm/modules/async.js"
);
const squelModule = await this.createOrGetVmModule(
"query",
"node_modules/squel/dist/squel.min.js"
);
query.modules = [asyncCallModule, squelModule];
const dbPlugin = await this.createOrGetPlugin("db", "DATABASE", query, {
// TODO: take it from database handler
host: process.env.DB_HOST,
user: process.env.DB_USERNAME,
port: process.env.DB_PORT,
password: process.env.DB_PASSWORD,
database: "test",
});
query.plugins = [dbPlugin];
await this.queryRepository.save(query);
}
async createQuery(queryData: { projectToken: string; source: string }) {
const project = await this.projectService.findByToken(
queryData.projectToken
@ -32,7 +110,11 @@ export class QueryHandlerService {
delete queryData.projectToken;
const query = this.queryRepository.create(queryData);
return this.queryRepository.save(query);
await this.queryRepository.save(query);
await this.createDefaults(query);
return query;
}
async updateQuery(id: string, updateData: Partial<Query>) {

View File

@ -1,4 +1,12 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function asyncCall(reference, args) {
return await reference.apply(undefined, args, { result: { promise: true } });
if (!Array.isArray(args)) {
args = [args];
}
const resJson = await reference.apply(undefined, args, {
result: { promise: true },
});
return JSON.parse(resJson);
}

View File

@ -12,31 +12,39 @@ export class DatabasePlugin extends Plugin {
super(name);
}
static init(
static async init(
name: string,
config: {
host: string;
port: number;
user: string;
password: string;
database: string;
}
): DatabasePlugin {
const dbConnection = mysql.createConnection({
): Promise<DatabasePlugin> {
const dbConnection = await mysql.createConnection({
host: config.host,
user: config.user,
port: config.port,
password: config.password,
database: config.database,
enableKeepAlive: true,
});
return new DatabasePlugin(name, dbConnection);
}
async run(query): Promise<{
rows: any[];
fields: any[];
}> {
const [rows, fields] = await this.dbConnection.execute(query);
return { rows, fields };
async run(query): Promise<any> {
try {
const [rows, fields] = await this.dbConnection.execute(query);
return JSON.stringify({
rows: rows,
fields: fields ?? [],
});
} catch (error) {
console.log("error", error);
}
}
onFinish() {

View File

@ -28,13 +28,17 @@ export class Vm {
this.jail.set("global", this.jail.derefInto());
for (const mod of this.modules) {
this.jail.setSync(mod.getName(), mod.getSource());
await this.context.eval(mod.getSource());
}
for (const plugin of this.plugins) {
this.jail.setSync(
plugin.getName(),
new ivm.Reference(plugin.run.bind(plugin))
new ivm.Reference(async (...args) => {
const res = await plugin.run(...args);
return res;
})
);
}
@ -61,6 +65,9 @@ export class Vm {
});
// TODO: log
this.setFunction("log", (...args) => {
console.log("vm log:", args);
});
this.setFunction("error", (error: any) => {
console.error("Script error:", error);
@ -82,9 +89,12 @@ export class Vm {
const compiledScript = await this.isolate.compileScript(scriptWithResult);
compiledScript.run(this.context);
this.onFinish();
return await resultPromise;
try {
return await resultPromise;
} finally {
this.onFinish();
}
}
onFinish() {