feat: implement project settings management with CRUD operations and caching

This commit is contained in:
lborv
2025-10-13 20:40:01 +03:00
parent aa7920384c
commit 93f12cd1d8
14 changed files with 304 additions and 17 deletions

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ProjectSettings1760377023483 implements MigrationInterface {
name = "ProjectSettings1760377023483";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
`ALTER TABLE \`projectSetting\` DROP FOREIGN KEY \`FK_8dfaf9c1ebbadb7af024e72e871\``
);
await queryRunner.query(`DROP TABLE \`projectSetting\``);
}
}

View File

@ -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[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<number> {
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<void> {
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<void> {

View File

@ -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<any> {
const settings =
await this.queryExecuterService.projectSettingService.getAll(
this.query.project.id
);
return settings[property];
}
onFinish() {
// No resources to clean up
}
}

View File

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