20 Commits
develop ... sdk

Author SHA1 Message Date
8b77f7e42c chore: update subproject commit reference in sdk 2025-10-29 17:06:47 +02:00
9e79b44bdb feat: implement session management with SessionController and SessionService; enhance session update logic to handle non-existent sessions 2025-10-29 17:06:38 +02:00
7fad278d31 chore: update subproject commit reference in sdk 2025-10-28 21:00:22 +02:00
776d8a8187 feat: update deleteSetting endpoint to use path parameter for key 2025-10-28 20:43:00 +02:00
e4b29a918f Merge remote-tracking branch 'origin/projectDetails' into sdk 2025-10-28 20:40:58 +02:00
4acd59b482 chore: update subproject commit reference in sdk 2025-10-28 20:37:22 +02:00
84c48dd482 feat: add getProjectInfo method and corresponding endpoint in ProjectController; refactor logger methods to include projectId 2025-10-28 20:36:53 +02:00
bbc378dc95 feat: add isPublic field to Query entity and implement QueryPublicGuard for public query access control 2025-10-28 16:35:26 +02:00
5d596832d6 feat: add optional isTypescript parameter to create and update query methods 2025-10-28 15:40:03 +02:00
3a1249615e feat: add TypeScript compilation support in QueryExecuterService 2025-10-28 15:37:09 +02:00
0ac6b7db6f chore: update subproject commit reference in sdk 2025-10-27 20:07:34 +02:00
9080f193c1 fix: update revokeToken method to use @Param decorator for token retrieval 2025-10-27 20:07:26 +02:00
1a2d7b20c0 feat: add submodule configuration for few-line-sdk 2025-10-27 19:20:15 +02:00
e1fce6d11d chore: remove submodule configuration for few-line-sdk 2025-10-27 19:13:37 +02:00
1d5160e60e chore: remove submodule reference for lib 2025-10-27 19:11:41 +02:00
038f2f8605 fix: remove unnecessary blank line in ProjectService 2025-10-27 19:08:56 +02:00
91ad421b8d feat: add submodule for few-line-sdk library 2025-10-27 19:08:40 +02:00
ee5ad66759 feat: add meta field to project entity and corresponding migration, service, and controller updates 2025-10-15 20:47:11 +03:00
f7b775f87b Merge remote-tracking branch 'origin/develop' into projectDetails 2025-10-15 19:53:41 +03:00
6992041429 feat: add getProjectDetails method and corresponding endpoint 2025-10-15 19:52:36 +03:00
18 changed files with 325 additions and 47 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lib/sdk"]
path = lib/sdk
url = http://192.168.0.16:3000/lborv/few-line-sdk.git

1
lib/sdk Submodule

Submodule lib/sdk added at 4b8c6753ef

View File

@ -3,6 +3,7 @@ import {
Controller,
Delete,
Inject,
Param,
Post,
UseGuards,
} from "@nestjs/common";
@ -28,8 +29,8 @@ export class ApiController {
return this.apiService.generateToken(body.id);
}
@Delete("token/revoke")
revokeToken(@Body() body: { token: string }) {
return this.apiService.revokeToken(body.token);
@Delete("token/revoke/:token")
revokeToken(@Param("token") token: string) {
return this.apiService.revokeToken(token);
}
}

View File

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Projectmeta1760548310371 implements MigrationInterface {
name = "Projectmeta1760548310371";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`project\` ADD \`meta\` longtext NULL`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`project\` DROP COLUMN \`meta\``);
}
}

11
src/project/constants.ts Normal file
View File

@ -0,0 +1,11 @@
type item = {
id: string;
path: string;
};
export type TMeta = {
migrations: item[];
queries: item[];
functions: item[];
settings: item[];
};

View File

@ -15,6 +15,7 @@ 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";
import { TMeta } from "../constants";
@Entity("project")
export class Project {
@ -43,6 +44,16 @@ export class Project {
@OneToMany(() => ProjectSetting, (setting) => setting.project)
settings: ProjectSetting[];
@Column({
type: "longtext",
nullable: true,
transformer: {
to: (value: TMeta) => JSON.stringify(value),
from: (value: string) => JSON.parse(value) as TMeta,
},
})
meta: TMeta;
@ManyToMany(() => RedisNode, (redisNode) => redisNode.projects)
@JoinTable()
redisNodes: RedisNode[];

View File

@ -4,6 +4,8 @@ import {
Delete,
Get,
Inject,
Param,
Post,
Put,
Req,
UseGuards,
@ -11,7 +13,9 @@ import {
import { ProjectService } from "./project.service";
import { ApiTokenGuard } from "src/api/guards/api-token.guard";
import { AdminGuard } from "src/api/guards/admin.guard";
import { Request } from "express";
import { ProjectSettingService } from "./settings/project.setting.service";
import { TMeta } from "./constants";
@Controller("project")
@UseGuards(ApiTokenGuard)
@ -24,10 +28,26 @@ export class ProjectController {
) {}
@Put("create")
@UseGuards(AdminGuard)
createProject(@Body() body: { name: string }) {
return this.projectService.create(body.name);
}
@Get("details")
getProjectDetails(
@Req() req: Request & { apiToken: { project: { id: string } } }
) {
return this.projectService.getProjectDetails(req.apiToken.project.id);
}
@Post("update/meta")
updateProjectMeta(
@Body() body: { meta: TMeta },
@Req() req: Request & { apiToken: { project: { id: string } } }
) {
return this.projectService.updateMeta(req.apiToken.project.id, body.meta);
}
@Put("create-without-db")
@UseGuards(AdminGuard)
createProjectWithoutDB(@Body() body: { name: string }) {
@ -46,12 +66,12 @@ export class ProjectController {
);
}
@Delete("settings/delete")
@Delete("settings/delete/:key")
deleteSetting(
@Body() body: { key: string },
@Param("key") key: string,
@Req() req: Request & { apiToken: { id: string } }
) {
return this.projectSettingService.delete(req.apiToken.id, body.key);
return this.projectSettingService.delete(req.apiToken.id, key);
}
@Get("settings")
@ -63,6 +83,5 @@ export class ProjectController {
@UseGuards(AdminGuard)
getAllApiTokens(@Req() req: Request & { apiToken: { id: string } }) {
return this.projectService.getAllApiTokens(req.apiToken.id);
}
}
}
}

View File

@ -5,6 +5,7 @@ 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";
import { TMeta } from "./constants";
@Injectable()
export class ProjectService {
@ -45,6 +46,33 @@ export class ProjectService {
return projectSaved;
}
async updateMeta(projectId: string, meta: TMeta) {
return this.projectRepository.update({ id: projectId }, { meta });
}
async getProjectDetails(projectId: string) {
const project = await this.projectRepository.findOne({
where: { id: projectId },
relations: [
"database",
"database.migrations",
"queries",
"functions",
"settings",
],
});
return {
migrations: project?.database?.migrations || [],
queries: project?.queries || [],
functions: project?.functions || [],
settings: project?.settings || [],
meta: project?.meta || null,
name: project?.name || "",
id: project?.id || "",
};
}
async findById(id: string) {
const cached = await this.redisClient.get(`project_${id}`);
@ -76,7 +104,7 @@ export class ProjectService {
redisNodes: redisNodeId,
});
}
async getAllApiTokens(projectId: string) {
const project = await this.projectRepository.findOne({
where: { id: projectId },
@ -84,4 +112,13 @@ export class ProjectService {
});
return project?.apiTokens || [];
}
async getProjectInfo(projectId: string) {
const project = await this.projectRepository.findOne({
where: { id: projectId },
relations: ["queries", "apiTokens", "functions", "settings"],
});
return project;
}
}

View File

@ -19,8 +19,8 @@ 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";
import { QueryPublicGuard } from "../guards/query-public";
@UseGuards(ApiTokenGuard)
export abstract class BaseQueryController {
constructor(
@Inject(QueryHandlerService)
@ -34,9 +34,11 @@ export abstract class BaseQueryController {
protected abstract getIsCommand(): boolean;
@Post("create")
@UseGuards(ApiTokenGuard)
async createQuery(
@Req() req: Request & { apiToken: Token },
@Body() queryData: { source: string }
@Body()
queryData: { source: string; isTypescript?: number; isPublic?: number }
) {
return this.queryHandlerService.createQuery(
{
@ -48,23 +50,26 @@ export abstract class BaseQueryController {
}
@Post("update/:id")
@UseGuards(QueryGuard)
@UseGuards(ApiTokenGuard, QueryGuard)
async updateQuery(
@Body() updateData: Partial<{ source: string }>,
@Body()
updateData: Partial<{
source: string;
isTypescript?: number;
isPublic?: number;
}>,
@Param("id") id: string
) {
return this.queryHandlerService.updateQuery(id, updateData);
}
@Post("/run/:id")
@UseGuards(QueryGuard)
async runQuery(
@Param("id") id: string,
@Body() query: Record<string, any>,
@Headers() headers: Record<string, any>,
@Res() res: Response,
@Req() req: Request & { query: Query }
) {
private async run(
id: string,
query: Record<string, any>,
headers: Record<string, any>,
res: Response,
req: Request & { query: Query }
): Promise<QueryResponse> {
let queryResult: QueryResponse;
const loggerTraceId =
headers["x-trace-id"] || LoggerService.generateTraceId();
@ -143,8 +148,32 @@ export abstract class BaseQueryController {
res.send(queryResult?.response || null);
}
@Post("/run-public/:id")
@UseGuards(QueryPublicGuard)
async runPublicQuery(
@Param("id") id: string,
@Body() query: Record<string, any>,
@Headers() headers: Record<string, any>,
@Res() res: Response,
@Req() req: Request & { query: Query }
) {
return this.run(id, query, headers, res, req);
}
@Post("/run/:id")
@UseGuards(ApiTokenGuard, QueryGuard)
async runQuery(
@Param("id") id: string,
@Body() query: Record<string, any>,
@Headers() headers: Record<string, any>,
@Res() res: Response,
@Req() req: Request & { query: Query }
) {
return this.run(id, query, headers, res, req);
}
@Delete("/delete/:id")
@UseGuards(QueryGuard)
@UseGuards(ApiTokenGuard, QueryGuard)
async deleteQuery(@Param("id") id: string) {
return this.queryHandlerService.deleteQuery(id);
}

View File

@ -27,4 +27,10 @@ export class Query {
@Column({ type: "tinyint", default: 0 })
isCommand: number;
@Column({ type: "tinyint", default: 0 })
isTypescript: number;
@Column({ type: "tinyint", default: 0 })
isPublic: number;
}

View File

@ -19,6 +19,7 @@ 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";
import ts from "typescript";
@Injectable()
export class QueryExecuterService {
@ -89,6 +90,14 @@ export class QueryExecuterService {
return result;
}
private compileTypeScript(tsCode: string) {
const jsCode = ts.transpileModule(tsCode, {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText;
return jsCode;
}
async runQuery(
token: string,
queryData: any,
@ -122,17 +131,19 @@ export class QueryExecuterService {
type: TLogType.info,
});
let script = this.clearImports(query.source);
if (query.isTypescript) {
script = this.compileTypeScript(script);
}
const vm = await this.createVm(
query,
log,
callStack,
cookies["x-session-id"]
);
const result = await vm.runScript(
this.clearImports(query.source),
queryData,
headers
);
const result = await vm.runScript(script, queryData, headers);
if (!this.checkResponse(result)) {
throw new Error(`Error initializing VM: ${JSON.stringify(result)}`);

View File

@ -1,4 +1,12 @@
import { Controller, Inject, Post, Req, UseGuards } from "@nestjs/common";
import {
Controller,
Delete,
Inject,
Param,
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";
@ -20,10 +28,10 @@ export class FunctionController {
return this.functionService.create(req.apiToken.project.id, name, source);
}
@Post("delete")
@Delete("delete/:name")
async deleteFunction(
@Req() req: Request & { apiToken: Token },
name: string
@Param("name") name: string
) {
return this.functionService.deleteFunction(req.apiToken.project.id, name);
}

View File

@ -0,0 +1,44 @@
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { QueryHandlerService } from "src/query/handler/query.handler.service";
@Injectable()
export class QueryPublicGuard implements CanActivate {
constructor(
@Inject(QueryHandlerService)
private readonly queryHandlerService: QueryHandlerService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const queryId = request.params?.id;
if (!queryId) {
throw new UnauthorizedException("Query ID is required");
}
const query = await this.queryHandlerService.getQueryById(queryId);
if (!query) {
throw new UnauthorizedException("Query not found");
}
if (!query.isActive) {
throw new UnauthorizedException("Query is inactive");
}
if (query.isPublic !== 1) {
throw new UnauthorizedException("Query is not public");
}
request.query = query;
return true;
}
}

View File

@ -31,7 +31,6 @@ export class QueryHandlerService {
delete queryData.projectToken;
const query = this.queryRepository.create(queryData);
await this.queryRepository.save(query);
return query;
@ -64,9 +63,9 @@ export class QueryHandlerService {
}
await this.redisClient.del(`query_${id}`);
Object.assign(query, updateData);
return this.queryRepository.save(query);
return await this.queryRepository.save(query);
}
async deleteQuery(id: string) {

View File

@ -5,11 +5,14 @@ import {
Inject,
Param,
Post,
Req,
UseGuards,
} from "@nestjs/common";
import { LoggerService } from "./logger.service";
import { ApiTokenGuard } from "src/api/guards/api-token.guard";
import { QueryGuard } from "../guards/query.guard";
import { Token } from "src/api/entities/token.entity";
import { Query } from "../entities/query.entity";
@Controller("logger")
@UseGuards(ApiTokenGuard)
@ -19,14 +22,17 @@ export class LoggerController {
private readonly loggerService: LoggerService
) {}
@Get("/:id/:traceId")
getByTraceId(@Param("traceId") traceId: string) {
return this.loggerService.findByTraceId(traceId);
@Get("/:traceId")
getByTraceId(
@Req() req: Request & { apiToken: Token },
@Param("traceId") traceId: string
) {
return this.loggerService.findByTraceId(req.apiToken.project.id, traceId);
}
@Post("/:id/findAll")
@Post("/findAll")
findAll(
@Param("id") projectId: string,
@Req() req: Request & { apiToken: Token },
@Body()
body: {
traceId?: string;
@ -37,13 +43,13 @@ export class LoggerController {
offset: number;
}
) {
return this.loggerService.findByProjectId(projectId, body);
return this.loggerService.findByProjectId(req.apiToken.project.id, body);
}
@Post("/:id/find")
@Post("/find")
@UseGuards(QueryGuard)
find(
@Param("id") _id: string,
@Req() req: Request & { query: Query },
@Body()
body: {
traceId?: string;
@ -54,6 +60,6 @@ export class LoggerController {
offset: number;
}
) {
return this.loggerService.find(_id, body);
return this.loggerService.find(req.query.id, body);
}
}

View File

@ -22,8 +22,15 @@ export class LoggerService {
return await this.logRepository.save(log);
}
async findByTraceId(traceId: string): Promise<Log[]> {
return await this.logRepository.find({ where: { traceId } });
async findByTraceId(projectId: string, traceId: string): Promise<Log[]> {
return await this.logRepository.find({
where: {
traceId,
project: {
id: projectId,
},
},
});
}
private prepareQuery(data: {

View File

@ -0,0 +1,59 @@
import {
Body,
Controller,
Delete,
Get,
Inject,
Param,
Post,
Put,
Req,
UseGuards,
} from "@nestjs/common";
import { ApiTokenGuard } from "src/api/guards/api-token.guard";
import { SessionService } from "./session.service";
@Controller("session")
@UseGuards(ApiTokenGuard)
export class SessionController {
constructor(
@Inject(SessionService)
private readonly sessionService: SessionService
) {}
@Get("get/:sessionId")
getSession(
@Param("sessionId") sessionId: string,
@Req() req: Request & { apiToken: { project: { id: string } } }
) {
return this.sessionService.get(sessionId, req.apiToken.project.id);
}
@Delete("delete/:sessionId")
deleteSession(
@Param("sessionId") sessionId: string,
@Req() req: Request & { apiToken: { project: { id: string } } }
) {
return this.sessionService.delete(sessionId, req.apiToken.project.id);
}
@Post("create")
createSession(
@Req() req: Request & { apiToken: { project: { id: string } } }
) {
return this.sessionService.create(req.apiToken.project.id);
}
@Put("set/:sessionId")
setSession(
@Param("sessionId") sessionId: string,
@Req() req: Request & { apiToken: { project: { id: string } } },
@Body() body: { data: any }
) {
return this.sessionService.set(
sessionId,
req.apiToken.project.id,
body.data
);
}
}

View File

@ -56,9 +56,20 @@ export class SessionService {
}
async set(sessionId: string, prefix: string, data: any): Promise<void> {
const existingSession = await this.redisClient.get(
`${prefix}:${sessionId}`
);
if (!existingSession) {
throw new Error("Session not found");
}
await this.redisClient.set(
`${prefix}:${sessionId}`,
data,
{
...existingSession,
...data,
},
await this.getSessionTTL(prefix)
);
}