diff --git a/src/migrations/1760377023483-projectSettings.ts b/src/migrations/1760377023483-projectSettings.ts new file mode 100644 index 0000000..2211087 --- /dev/null +++ b/src/migrations/1760377023483-projectSettings.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ProjectSettings1760377023483 implements MigrationInterface { + name = "ProjectSettings1760377023483"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`projectSetting\` (\`id\` varchar(36) NOT NULL, \`key\` varchar(255) NOT NULL, \`value\` text NOT NULL, \`projectId\` varchar(36) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`projectSetting\` ADD CONSTRAINT \`FK_8dfaf9c1ebbadb7af024e72e871\` FOREIGN KEY (\`projectId\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`projectSetting\` DROP FOREIGN KEY \`FK_8dfaf9c1ebbadb7af024e72e871\`` + ); + await queryRunner.query(`DROP TABLE \`projectSetting\``); + } +} diff --git a/src/project/entities/project.entity.ts b/src/project/entities/project.entity.ts index 0d094a5..4d445bf 100644 --- a/src/project/entities/project.entity.ts +++ b/src/project/entities/project.entity.ts @@ -14,6 +14,7 @@ import { Database } from "../../databaseManager/entities/database.entity"; import { FunctionEntity } from "../../query/entities/function.entity"; import { RedisNode } from "../../redisManager/entities/redis.node.entity"; import { Log } from "../../query/logger/entities/log.entity"; +import { ProjectSetting } from "../settings/entities/project.setting.entity"; @Entity("project") export class Project { @@ -39,6 +40,9 @@ export class Project { @OneToMany(() => FunctionEntity, (functionEntity) => functionEntity.project) functions: FunctionEntity[]; + @OneToMany(() => ProjectSetting, (setting) => setting.project) + settings: ProjectSetting[]; + @ManyToMany(() => RedisNode, (redisNode) => redisNode.projects) @JoinTable() redisNodes: RedisNode[]; diff --git a/src/project/project.controller.ts b/src/project/project.controller.ts index ebaaaef..9e79344 100644 --- a/src/project/project.controller.ts +++ b/src/project/project.controller.ts @@ -1,14 +1,26 @@ -import { Body, Controller, Inject, Put, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Delete, + Get, + Inject, + Put, + Req, + UseGuards, +} from "@nestjs/common"; import { ProjectService } from "./project.service"; import { ApiTokenGuard } from "src/api/guards/api-token.guard"; import { AdminGuard } from "src/api/guards/admin.guard"; +import { ProjectSettingService } from "./settings/project.setting.service"; @Controller("project") @UseGuards(ApiTokenGuard) export class ProjectController { constructor( @Inject(ProjectService) - private readonly projectService: ProjectService + private readonly projectService: ProjectService, + @Inject(ProjectSettingService) + private readonly projectSettingService: ProjectSettingService ) {} @Put("create") @@ -21,4 +33,29 @@ export class ProjectController { createProjectWithoutDB(@Body() body: { name: string }) { return this.projectService.create(body.name, false); } + + @Put("settings/create") + createSetting( + @Body() body: { key: string; value: string }, + @Req() req: Request & { apiToken: { id: string } } + ) { + return this.projectSettingService.create( + req.apiToken.id, + body.key, + body.value + ); + } + + @Delete("settings/delete") + deleteSetting( + @Body() body: { key: string }, + @Req() req: Request & { apiToken: { id: string } } + ) { + return this.projectSettingService.delete(req.apiToken.id, body.key); + } + + @Get("settings") + getAllSettings(@Req() req: Request & { apiToken: { id: string } }) { + return this.projectSettingService.getAll(req.apiToken.id); + } } diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 2b9e302..deed0c2 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -6,16 +6,18 @@ import { ProjectController } from "./project.controller"; import { ApiModule } from "src/api/api.module"; import { RedisModule } from "src/redis/redis.module"; import { DatabaseManagerModule } from "src/databaseManager/database.manager.module"; +import { ProjectSetting } from "./settings/entities/project.setting.entity"; +import { ProjectSettingService } from "./settings/project.setting.service"; @Module({ imports: [ forwardRef(() => ApiModule), forwardRef(() => RedisModule), forwardRef(() => DatabaseManagerModule), - TypeOrmModule.forFeature([Project]), + TypeOrmModule.forFeature([Project, ProjectSetting]), ], controllers: [ProjectController], - providers: [ProjectService], - exports: [ProjectService], + providers: [ProjectService, ProjectSettingService], + exports: [ProjectService, ProjectSettingService], }) export class ProjectModule {} diff --git a/src/project/project.service.ts b/src/project/project.service.ts index 7dd8249..3ae6e75 100644 --- a/src/project/project.service.ts +++ b/src/project/project.service.ts @@ -4,6 +4,7 @@ import { Repository } from "typeorm"; import { Project } from "./entities/project.entity"; import { RedisClient } from "src/redis/redis.service"; import { DatabaseManagerService } from "src/databaseManager/database/database.manager.service"; +import { ProjectSettingService } from "./settings/project.setting.service"; @Injectable() export class ProjectService { @@ -13,9 +14,23 @@ export class ProjectService { @Inject(RedisClient) private readonly redisClient: RedisClient, @Inject(forwardRef(() => DatabaseManagerService)) - private readonly databaseManagerService: DatabaseManagerService + private readonly databaseManagerService: DatabaseManagerService, + @Inject(ProjectSettingService) + private readonly projectSettingService: ProjectSettingService ) {} + async createDefaultSettings(projectId: string) { + const defaultSettings = [{ key: "sessionTTL", value: "3600" }]; + + await Promise.all( + defaultSettings.map((setting) => + this.projectSettingService.create(projectId, setting.key, setting.value) + ) + ); + + return true; + } + async create(name: string, createDatabase: boolean = true) { const project = this.projectRepository.create({ name }); const projectSaved = await this.projectRepository.save(project); @@ -24,6 +39,9 @@ export class ProjectService { await this.databaseManagerService.createDatabase(projectSaved.id); } + await this.createDefaultSettings(projectSaved.id); + await this.redisClient.set(`project_${projectSaved.id}`, projectSaved, 300); + return projectSaved; } diff --git a/src/project/settings/entities/project.setting.entity.ts b/src/project/settings/entities/project.setting.entity.ts new file mode 100644 index 0000000..bac70aa --- /dev/null +++ b/src/project/settings/entities/project.setting.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Project } from "../../entities/project.entity"; + +@Entity("projectSetting") +export class ProjectSetting { + @PrimaryGeneratedColumn("uuid") + id: string; + + @ManyToOne(() => Project, (project) => project.settings, { + onDelete: "CASCADE", + }) + project: Project; + + @Column({ type: "varchar", length: 255, nullable: false }) + key: string; + + @Column({ type: "text", nullable: false }) + value: string; +} diff --git a/src/project/settings/project.setting.service.ts b/src/project/settings/project.setting.service.ts new file mode 100644 index 0000000..457047f --- /dev/null +++ b/src/project/settings/project.setting.service.ts @@ -0,0 +1,112 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { ProjectSetting } from "./entities/project.setting.entity"; +import { RedisClient } from "src/redis/redis.service"; + +@Injectable() +export class ProjectSettingService { + constructor( + @InjectRepository(ProjectSetting) + private readonly projectSettingRepository: Repository, + @Inject(RedisClient) + private readonly redisClient: RedisClient + ) {} + + async updateCache(projectId: string) { + const settings = await this.projectSettingRepository.find({ + where: { project: { id: projectId } }, + }); + + const settingsObject = settings.reduce((obj, setting) => { + obj[setting.key] = setting.value; + return obj; + }, {}); + + await this.redisClient.set( + `project_settings_${projectId}`, + settingsObject, + 300 + ); + + return settingsObject; + } + + async get(key: string, projectId: string) { + const cached = await this.redisClient.get(`project_settings_${projectId}`); + + if (cached && key in cached) { + return cached[key]; + } + + const setting = await this.projectSettingRepository.findOne({ + where: { project: { id: projectId }, key }, + }); + + if (setting) { + return setting.value; + } + + return null; + } + + async getAll(projectId: string) { + const cached = await this.redisClient.get(`project_settings_${projectId}`); + + if (cached) { + return cached; + } + + const settings = await this.projectSettingRepository.find({ + where: { project: { id: projectId } }, + }); + + const settingsObject = settings.reduce((obj, setting) => { + obj[setting.key] = setting.value; + return obj; + }, {}); + + await this.redisClient.set( + `project_settings_${projectId}`, + settingsObject, + 300 + ); + + return settingsObject; + } + + async create(projectId: string, key: string, value: string) { + const existingSetting = await this.projectSettingRepository.findOne({ + where: { project: { id: projectId }, key }, + }); + + if (existingSetting) { + existingSetting.value = value; + await this.projectSettingRepository.save(existingSetting); + + return await this.updateCache(projectId); + } + + const newSetting = this.projectSettingRepository.create({ + key, + value, + project: { id: projectId }, + }); + + await this.projectSettingRepository.save(newSetting); + + return await this.updateCache(projectId); + } + + async delete(projectId: string, key: string) { + const setting = await this.projectSettingRepository.findOne({ + where: { project: { id: projectId }, key }, + }); + + if (setting) { + await this.projectSettingRepository.remove(setting); + } + + return await this.updateCache(projectId); + } +} diff --git a/src/query/base/base-query.controller.ts b/src/query/base/base-query.controller.ts index 031da44..23c2296 100644 --- a/src/query/base/base-query.controller.ts +++ b/src/query/base/base-query.controller.ts @@ -18,6 +18,7 @@ import { LoggerService } from "../logger/logger.service"; import { TLogType } from "../logger/logger.types"; import { QueryResponse } from "src/vm/vm.constants"; import { Query } from "../entities/query.entity"; +import { Token } from "src/api/entities/token.entity"; @UseGuards(ApiTokenGuard) export abstract class BaseQueryController { @@ -34,9 +35,16 @@ export abstract class BaseQueryController { @Post("create") async createQuery( - @Body() queryData: { projectToken: string; source: string } + @Req() req: Request & { apiToken: Token }, + @Body() queryData: { source: string } ) { - return this.queryHandlerService.createQuery(queryData, this.getIsCommand()); + return this.queryHandlerService.createQuery( + { + ...queryData, + projectToken: req?.apiToken.project.id, + }, + this.getIsCommand() + ); } @Post("update/:id") diff --git a/src/query/executer/query.executer.service.ts b/src/query/executer/query.executer.service.ts index e3cfc9e..46a0929 100644 --- a/src/query/executer/query.executer.service.ts +++ b/src/query/executer/query.executer.service.ts @@ -18,6 +18,7 @@ import { FunctionService } from "src/query/function/function.service"; import { SessionService } from "../session/session.service"; import { TLog, TLogType } from "../logger/logger.types"; import { LoggerService } from "../logger/logger.service"; +import { ProjectSettingService } from "src/project/settings/project.setting.service"; @Injectable() export class QueryExecuterService { @@ -31,6 +32,7 @@ export class QueryExecuterService { readonly databaseManagerService: DatabaseManagerService, readonly redisNodeService: RedisNodeService, readonly sessionService: SessionService, + readonly projectSettingService: ProjectSettingService, @InjectQueue(QUEUE_NAMES.QUERY) private queryQueue: Queue ) { this.queueEvents = new QueueEvents(this.queryQueue.name); diff --git a/src/query/function/function.controller.ts b/src/query/function/function.controller.ts index 1eb93fe..fce239d 100644 --- a/src/query/function/function.controller.ts +++ b/src/query/function/function.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Inject, Post, UseGuards } from "@nestjs/common"; +import { Controller, Inject, Post, Req, UseGuards } from "@nestjs/common"; import { ApiTokenGuard } from "src/api/guards/api-token.guard"; import { FunctionService } from "./function.service"; +import { Token } from "src/api/entities/token.entity"; @Controller("functions") @UseGuards(ApiTokenGuard) @@ -11,12 +12,19 @@ export class FunctionController { ) {} @Post("create") - async createFunction(projectId: string, name: string, source: string) { - return this.functionService.create(projectId, name, source); + async createFunction( + @Req() req: Request & { apiToken: Token }, + name: string, + source: string + ) { + return this.functionService.create(req.apiToken.project.id, name, source); } @Post("delete") - async deleteFunction(projectId: string, name: string) { - return this.functionService.deleteFunction(projectId, name); + async deleteFunction( + @Req() req: Request & { apiToken: Token }, + name: string + ) { + return this.functionService.deleteFunction(req.apiToken.project.id, name); } } diff --git a/src/query/query.module.ts b/src/query/query.module.ts index 53c19cd..88ca4c4 100644 --- a/src/query/query.module.ts +++ b/src/query/query.module.ts @@ -18,6 +18,8 @@ import { SessionService } from "./session/session.service"; import { Log } from "./logger/entities/log.entity"; import { LoggerService } from "./logger/logger.service"; import { LoggerController } from "./logger/logger.controller"; +import { ProjectSettingService } from "src/project/settings/project.setting.service"; +import { ProjectSetting } from "src/project/settings/entities/project.setting.entity"; @Module({ imports: [ @@ -27,7 +29,7 @@ import { LoggerController } from "./logger/logger.controller"; forwardRef(() => QueueModule), forwardRef(() => RedisModule), forwardRef(() => RedisManagerModule), - TypeOrmModule.forFeature([Query, FunctionEntity, Log]), + TypeOrmModule.forFeature([Query, FunctionEntity, Log, ProjectSetting]), ], controllers: [ QueryController, @@ -41,12 +43,14 @@ import { LoggerController } from "./logger/logger.controller"; LoggerService, QueryHandlerService, FunctionService, + ProjectSettingService, ], exports: [ QueryExecuterService, TypeOrmModule, QueryHandlerService, LoggerService, + ProjectSettingService, ], }) export class QueryModule {} diff --git a/src/query/session/session.service.ts b/src/query/session/session.service.ts index f680182..1f3674a 100644 --- a/src/query/session/session.service.ts +++ b/src/query/session/session.service.ts @@ -1,9 +1,15 @@ import { Inject, Injectable } from "@nestjs/common"; +import { ProjectSettingService } from "src/project/settings/project.setting.service"; import { RedisClient } from "src/redis/redis.service"; @Injectable() export class SessionService { - constructor(@Inject(RedisClient) private readonly redisClient: RedisClient) {} + constructor( + @Inject(RedisClient) + private readonly redisClient: RedisClient, + @Inject(ProjectSettingService) + private readonly projectSettingService: ProjectSettingService + ) {} private generateSessionId(length: number = 16): string { const characters = @@ -16,6 +22,11 @@ export class SessionService { return `${result}_${new Date().getTime()}`; } + async getSessionTTL(projectId: string): Promise { + const ttl = await this.projectSettingService.get("sessionTTL", projectId); + return ttl ? parseInt(ttl) : 3600; + } + async create(prefix: string): Promise<{ sessionId: string }> { const sessionId = this.generateSessionId(); await this.set(sessionId, prefix, { @@ -33,7 +44,11 @@ export class SessionService { const data = await this.redisClient.get(`${prefix}:${sessionId}`); if (data) { - await this.redisClient.set(`${prefix}:${sessionId}`, data, 3600); + await this.redisClient.set( + `${prefix}:${sessionId}`, + data, + await this.getSessionTTL(prefix) + ); return data; } @@ -41,7 +56,11 @@ export class SessionService { } async set(sessionId: string, prefix: string, data: any): Promise { - await this.redisClient.set(`${prefix}:${sessionId}`, data, 3600); + await this.redisClient.set( + `${prefix}:${sessionId}`, + data, + await this.getSessionTTL(prefix) + ); } async delete(sessionId: string, prefix: string): Promise { diff --git a/src/vm/plugins/settings.plugin.ts b/src/vm/plugins/settings.plugin.ts new file mode 100644 index 0000000..57e6e0a --- /dev/null +++ b/src/vm/plugins/settings.plugin.ts @@ -0,0 +1,29 @@ +import { Query } from "src/query/entities/query.entity"; +import { Plugin } from "../plugin.class"; +import { QueryExecuterService } from "src/query/executer/query.executer.service"; + +export class SettingsPlugin extends Plugin { + constructor( + name: string, + private query: Query, + private queryExecuterService: QueryExecuterService + ) { + super(name, ["get"]); + } + + static async init(query: Query, queryExecuterService: QueryExecuterService) { + return new SettingsPlugin("settings", query, queryExecuterService); + } + + async get(property: string): Promise { + const settings = + await this.queryExecuterService.projectSettingService.getAll( + this.query.project.id + ); + return settings[property]; + } + + onFinish() { + // No resources to clean up + } +} diff --git a/src/vm/vm.constants.ts b/src/vm/vm.constants.ts index 8d2c2c9..f75590e 100644 --- a/src/vm/vm.constants.ts +++ b/src/vm/vm.constants.ts @@ -6,6 +6,7 @@ import { AxiosPlugin } from "./plugins/axios.plugin"; import { RedisPlugin } from "./plugins/redis.plugin"; import { SessionPlugin } from "./plugins/session.plugin"; import { TLog } from "src/query/logger/logger.types"; +import { SettingsPlugin } from "./plugins/settings.plugin"; export const registeredPlugins = { db: async (service: QueryExecuterService, query: Query) => { @@ -32,6 +33,9 @@ export const registeredPlugins = { return RedisPlugin.init("redis", redisConnection, query.project.id); }, + settings: async (service: QueryExecuterService, query: Query) => { + return SettingsPlugin.init(query, service); + }, session: async ( service: QueryExecuterService, query: Query,