first commit
This commit is contained in:
19
aws_sigma_service/api/nest-cli.json
Normal file
19
aws_sigma_service/api/nest-cli.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"monorepo": false,
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "main",
|
||||
"language": "ts",
|
||||
"generateOptions": {
|
||||
"spec": false
|
||||
},
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "./tsconfig.build.json",
|
||||
"webpack": false,
|
||||
"deleteOutDir": true,
|
||||
"assets": [],
|
||||
"watchAssets": false,
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
42
aws_sigma_service/api/package.json
Normal file
42
aws_sigma_service/api/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@sigma/api",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/main",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"start": "node .",
|
||||
"format": "prettier -w src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.1.9",
|
||||
"@nestjs/core": "^11.1.9",
|
||||
"@nestjs/jwt": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.9",
|
||||
"@sigma/common": "*",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"mongoose": "^8.20.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.12",
|
||||
"@nestjs/schematics": "^11.0.9",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/passport": "^0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
66
aws_sigma_service/api/src/app.module.ts
Normal file
66
aws_sigma_service/api/src/app.module.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ProjectsController } from "./controllers/projects.controller.js";
|
||||
import { FunctionsController } from "./controllers/functions.controller.js";
|
||||
import { MongooseModule } from "@nestjs/mongoose";
|
||||
import { AuthService } from "./services/auth.service.js";
|
||||
import { UsersService } from "./services/users.service.js";
|
||||
import { User, UserSchema } from "./schemas/user.schema.js";
|
||||
import { Project, ProjectSchema } from "./schemas/project.schema.js";
|
||||
import { Function, FunctionSchema } from "./schemas/function.schema.js";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { BcryptService } from "./services/bcrypt.service.js";
|
||||
import { JWT_SECRET } from "./auth/const.js";
|
||||
import { AuthController } from "./controllers/auth.controller.js";
|
||||
import { JwtStrategy } from "./auth/jwt.strategy.js";
|
||||
import { LocalStrategy } from "./auth/local.strategy.js";
|
||||
import { ProjectsService } from "./services/projects.service.js";
|
||||
import { ProjectFunctionsService } from "./services/project-functions.service.js";
|
||||
import { UsersController } from "./controllers/users.controller.js";
|
||||
import { FunctionsModule } from "./functions/functions.module.js";
|
||||
import { HealthService } from "./services/health.service.js";
|
||||
import { HealthController } from "./controllers/health.controller.js";
|
||||
|
||||
@Module({
|
||||
controllers: [
|
||||
AuthController,
|
||||
UsersController,
|
||||
ProjectsController,
|
||||
FunctionsController,
|
||||
HealthController,
|
||||
],
|
||||
imports: [
|
||||
JwtModule.register({
|
||||
secret: JWT_SECRET,
|
||||
signOptions: { expiresIn: "3600s" },
|
||||
}),
|
||||
MongooseModule.forRoot(
|
||||
process.env.MONGO_URL ?? "mongodb://sigma:supersecret@localhost:27017"
|
||||
),
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: User.name,
|
||||
schema: UserSchema,
|
||||
},
|
||||
{
|
||||
name: Project.name,
|
||||
schema: ProjectSchema,
|
||||
},
|
||||
{
|
||||
name: Function.name,
|
||||
schema: FunctionSchema,
|
||||
},
|
||||
]),
|
||||
FunctionsModule,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
UsersService,
|
||||
ProjectsService,
|
||||
ProjectFunctionsService,
|
||||
HealthService,
|
||||
BcryptService,
|
||||
LocalStrategy,
|
||||
JwtStrategy,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
1
aws_sigma_service/api/src/auth/const.ts
Normal file
1
aws_sigma_service/api/src/auth/const.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const JWT_SECRET = process.env["JWT_SECRET"] ?? "supersecret";
|
||||
5
aws_sigma_service/api/src/auth/jwt-auth.guard.ts
Normal file
5
aws_sigma_service/api/src/auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {}
|
||||
19
aws_sigma_service/api/src/auth/jwt.strategy.ts
Normal file
19
aws_sigma_service/api/src/auth/jwt.strategy.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { JWT_SECRET } from "./const.js";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: JWT_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
return { userId: payload.sub, username: payload.username };
|
||||
}
|
||||
}
|
||||
5
aws_sigma_service/api/src/auth/local-auth.guard.ts
Normal file
5
aws_sigma_service/api/src/auth/local-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||
21
aws_sigma_service/api/src/auth/local.strategy.ts
Normal file
21
aws_sigma_service/api/src/auth/local.strategy.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy } from "passport-local";
|
||||
import { AuthService } from "../services/auth.service.js";
|
||||
import type { UserDocument } from "../schemas/user.schema.js";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async validate(username: string, password: string): Promise<UserDocument> {
|
||||
const user = await this.authService.validateUser(username, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
16
aws_sigma_service/api/src/controllers/auth.controller.ts
Normal file
16
aws_sigma_service/api/src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Controller, HttpCode, Post, Request, UseGuards } from "@nestjs/common";
|
||||
import type { Request as ExpressRequest } from "express";
|
||||
import { LocalAuthGuard } from "../auth/local-auth.guard.js";
|
||||
import { AuthService } from "../services/auth.service.js";
|
||||
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@HttpCode(200)
|
||||
@Post("login")
|
||||
async login(@Request() req: ExpressRequest): Promise<{ auth_token: string }> {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
|
||||
import { CreateFunctionDto, UpdateFunctionDto } from "@sigma/common";
|
||||
import type { FunctionDocument } from "../schemas/function.schema.js";
|
||||
import { ProjectFunctionsService } from "../services/project-functions.service.js";
|
||||
|
||||
@Controller("projects/:projectId/functions")
|
||||
export class FunctionsController {
|
||||
constructor(
|
||||
private readonly projectFunctionsService: ProjectFunctionsService
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
create(
|
||||
@Req() request: Request,
|
||||
@Param("projectId") projectId: string,
|
||||
@Body() payload: CreateFunctionDto
|
||||
): Promise<FunctionDocument> {
|
||||
return this.projectFunctionsService.create(
|
||||
request.user.userId,
|
||||
projectId,
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put(":functionId")
|
||||
async update(
|
||||
@Req() request: Request,
|
||||
@Param("projectId") projectId: string,
|
||||
@Param("functionId") functionId: string,
|
||||
@Body() payload: UpdateFunctionDto
|
||||
): Promise<FunctionDocument> {
|
||||
const func = await this.projectFunctionsService.update(
|
||||
request.user.userId,
|
||||
projectId,
|
||||
functionId,
|
||||
payload
|
||||
);
|
||||
|
||||
if (!func) {
|
||||
throw new NotFoundException("Function not found");
|
||||
}
|
||||
|
||||
return func;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(":functionId")
|
||||
@HttpCode(204)
|
||||
async delete(
|
||||
@Req() request: Request,
|
||||
@Param("projectId") projectId: string,
|
||||
@Param("functionId") functionId: string
|
||||
): Promise<FunctionDocument | null> {
|
||||
const func = await this.projectFunctionsService.delete(
|
||||
request.user.userId,
|
||||
projectId,
|
||||
functionId
|
||||
);
|
||||
|
||||
if (!func) {
|
||||
throw new NotFoundException("Function not found");
|
||||
}
|
||||
|
||||
return func;
|
||||
}
|
||||
}
|
||||
13
aws_sigma_service/api/src/controllers/health.controller.ts
Normal file
13
aws_sigma_service/api/src/controllers/health.controller.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { HealthService } from "../services/health.service.js";
|
||||
import { CpuInfoDto } from "@sigma/common";
|
||||
|
||||
@Controller("health")
|
||||
export class HealthController {
|
||||
constructor(private healthService: HealthService) {}
|
||||
|
||||
@Get("/cpu")
|
||||
getCpuInfo(): CpuInfoDto {
|
||||
return { usage: this.healthService.getServerCpuUsage() };
|
||||
}
|
||||
}
|
||||
79
aws_sigma_service/api/src/controllers/projects.controller.ts
Normal file
79
aws_sigma_service/api/src/controllers/projects.controller.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
import { ProjectsService } from "../services/projects.service.js";
|
||||
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
|
||||
import {
|
||||
CreateProjectDto,
|
||||
ProjectDto,
|
||||
QueryProjectDto,
|
||||
UpdateProjectDto,
|
||||
} from "@sigma/common";
|
||||
import type { ProjectDocument } from "../schemas/project.schema.js";
|
||||
|
||||
@Controller("projects")
|
||||
export class ProjectsController {
|
||||
constructor(private readonly projectsService: ProjectsService) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
create(
|
||||
@Req() request: Request,
|
||||
@Body() createProjectDto: CreateProjectDto
|
||||
): Promise<ProjectDocument> {
|
||||
return this.projectsService.create(request.user.userId, createProjectDto);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
findAll(
|
||||
@Req() request: Request,
|
||||
@Query() query: QueryProjectDto
|
||||
): Promise<ProjectDocument[]> {
|
||||
return this.projectsService.findAll(request.user.userId, query);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(":id")
|
||||
findOne(
|
||||
@Req() request: Request,
|
||||
@Param("id") id: string
|
||||
): Promise<ProjectDto | null> {
|
||||
return this.projectsService.findOne(request.user.userId, id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Patch(":id")
|
||||
update(
|
||||
@Req() request: Request,
|
||||
@Param("id") id: string,
|
||||
@Body() updateProjectDto: UpdateProjectDto
|
||||
): Promise<ProjectDocument | null> {
|
||||
return this.projectsService.update(
|
||||
request.user.userId,
|
||||
id,
|
||||
updateProjectDto
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(":id")
|
||||
@HttpCode(204)
|
||||
remove(
|
||||
@Req() request: Request,
|
||||
@Param("id") id: string
|
||||
): Promise<ProjectDocument | null> {
|
||||
return this.projectsService.remove(request.user.userId, id);
|
||||
}
|
||||
}
|
||||
69
aws_sigma_service/api/src/controllers/users.controller.ts
Normal file
69
aws_sigma_service/api/src/controllers/users.controller.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards,
|
||||
Request,
|
||||
Get,
|
||||
ConflictException,
|
||||
} from "@nestjs/common";
|
||||
import type { Request as ExpressRequest } from "express";
|
||||
import { UsersService } from "../services/users.service.js";
|
||||
import { BcryptService } from "../services/bcrypt.service.js";
|
||||
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
|
||||
import { RegisterUserDto, UpdateUserDto, UserDto } from "@sigma/common";
|
||||
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private bcryptService: BcryptService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@HttpCode(201)
|
||||
async register(@Body() registerDto: RegisterUserDto): Promise<UserDto> {
|
||||
try {
|
||||
const user = await this.usersService.create({
|
||||
username: registerDto.username,
|
||||
passwordHash: await this.bcryptService.hashPassword(
|
||||
registerDto.password
|
||||
),
|
||||
projects: [],
|
||||
});
|
||||
|
||||
return {
|
||||
username: user.username,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if ("code" in error && error.code === 11000) {
|
||||
throw new ConflictException("Username already exists");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put()
|
||||
async update(
|
||||
@Request() req: ExpressRequest,
|
||||
@Body() updateDto: UpdateUserDto
|
||||
) {
|
||||
this.usersService.update(req.user?._id, {
|
||||
passwordHash: await this.bcryptService.hashPassword(updateDto.password),
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get("me")
|
||||
async getProfile(@Request() req: ExpressRequest): Promise<UserDto> {
|
||||
const user = await this.usersService.findOne(req.user?.username);
|
||||
|
||||
return {
|
||||
username: user!.username,
|
||||
};
|
||||
}
|
||||
}
|
||||
65
aws_sigma_service/api/src/functions/execution.controller.ts
Normal file
65
aws_sigma_service/api/src/functions/execution.controller.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { All, Controller, Param, Req, Res } from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { FunctionExecutionService } from "./execution.service.js";
|
||||
|
||||
@Controller("exec")
|
||||
export class ExecutionController {
|
||||
constructor(
|
||||
private readonly functionExecutionService: FunctionExecutionService
|
||||
) {}
|
||||
|
||||
@All(":projectSlug")
|
||||
async handleRoot(
|
||||
@Param("projectSlug") projectSlug: string,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response
|
||||
) {
|
||||
await this.dispatchExecution(projectSlug, request, response);
|
||||
}
|
||||
|
||||
@All(":projectSlug/*path")
|
||||
async handleNested(
|
||||
@Param("projectSlug") projectSlug: string,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response
|
||||
) {
|
||||
await this.dispatchExecution(projectSlug, request, response);
|
||||
}
|
||||
|
||||
private async dispatchExecution(
|
||||
projectSlug: string,
|
||||
request: Request,
|
||||
response: Response
|
||||
) {
|
||||
const executionResult = await this.functionExecutionService.execute(
|
||||
projectSlug,
|
||||
request
|
||||
);
|
||||
|
||||
if (executionResult.responsePayload) {
|
||||
const { statusCode, headers, body } = executionResult.responsePayload;
|
||||
this.applyHeaders(response, headers);
|
||||
this.sendBody(response, statusCode ?? 200, body ?? "");
|
||||
return;
|
||||
} else {
|
||||
this.sendBody(response, 500, executionResult.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
private applyHeaders(
|
||||
response: Response,
|
||||
headers: Record<string, string> | undefined
|
||||
) {
|
||||
if (!headers) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
response.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private sendBody(response: Response, statusCode: number, body: string) {
|
||||
response.status(statusCode).send(body);
|
||||
}
|
||||
}
|
||||
64
aws_sigma_service/api/src/functions/execution.queue.ts
Normal file
64
aws_sigma_service/api/src/functions/execution.queue.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export class ExecutionQueueOverflowError extends Error {
|
||||
constructor(message = "Execution queue is full") {
|
||||
super(message);
|
||||
this.name = "ExecutionQueueOverflowError";
|
||||
}
|
||||
}
|
||||
|
||||
type Task<T> = () => Promise<T>;
|
||||
|
||||
interface PendingTask {
|
||||
task: () => Promise<unknown>;
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}
|
||||
|
||||
export class ExecutionQueue {
|
||||
private running = 0;
|
||||
private readonly queue: PendingTask[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly concurrency: number,
|
||||
private readonly queueLimit: number
|
||||
) {}
|
||||
|
||||
run<T>(task: Task<T>): Promise<T> {
|
||||
if (this.running < this.concurrency) {
|
||||
return this.execute(task);
|
||||
}
|
||||
|
||||
const limit = this.queueLimit;
|
||||
if (Number.isFinite(limit) && limit >= 0 && this.queue.length >= limit) {
|
||||
throw new ExecutionQueueOverflowError();
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
this.queue.push({
|
||||
task: () => task(),
|
||||
resolve: (value) => resolve(value as T),
|
||||
reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async execute<T>(task: Task<T>): Promise<T> {
|
||||
this.running += 1;
|
||||
try {
|
||||
return await task();
|
||||
} finally {
|
||||
this.running -= 1;
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private processQueue(): void {
|
||||
while (this.running < this.concurrency && this.queue.length > 0) {
|
||||
const next = this.queue.shift();
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.execute(next.task).then(next.resolve).catch(next.reject);
|
||||
}
|
||||
}
|
||||
}
|
||||
310
aws_sigma_service/api/src/functions/execution.service.ts
Normal file
310
aws_sigma_service/api/src/functions/execution.service.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import {
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
RequestTimeoutException,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
import { FunctionsService } from "./functions.service.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import {
|
||||
ExecutionQueue,
|
||||
ExecutionQueueOverflowError,
|
||||
} from "./execution.queue.js";
|
||||
import {
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
validate,
|
||||
} from "class-validator";
|
||||
|
||||
export interface ExecutionResponse {
|
||||
project: string;
|
||||
function: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
durationMs: number;
|
||||
timedOut: boolean;
|
||||
responsePayload?: ExecutorHttpResponse | null;
|
||||
}
|
||||
|
||||
export class ExecutorHttpResponse {
|
||||
@IsNumber({})
|
||||
@Min(100)
|
||||
@Max(599)
|
||||
statusCode!: number;
|
||||
|
||||
@IsObject()
|
||||
headers!: Record<string, string>;
|
||||
|
||||
@IsString()
|
||||
body!: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FunctionExecutionService {
|
||||
private baseRuntimeDir = "/tmp/sigma";
|
||||
private executorBinary =
|
||||
process.env.EXECUTOR_BIN_PATH ??
|
||||
path.resolve(
|
||||
process.cwd(),
|
||||
"..",
|
||||
"executor",
|
||||
"target",
|
||||
"release",
|
||||
"executor"
|
||||
);
|
||||
|
||||
private timeoutMs = 1000;
|
||||
private maxConcurrency = 10;
|
||||
private maxQueueSize = 32;
|
||||
|
||||
private queue = new ExecutionQueue(this.maxConcurrency, this.maxQueueSize);
|
||||
|
||||
constructor(private functionsService: FunctionsService) {}
|
||||
|
||||
async execute(
|
||||
projectSlug: string,
|
||||
request: Request
|
||||
): Promise<ExecutionResponse> {
|
||||
const project = await this.functionsService.findProjectBySlug(projectSlug);
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project ${projectSlug} not found`);
|
||||
}
|
||||
|
||||
const functionPath = this.extractFunctionPath(
|
||||
request.path ?? request.url,
|
||||
projectSlug
|
||||
);
|
||||
|
||||
const func = await this.functionsService.findRunnableFunction(
|
||||
project._id,
|
||||
functionPath,
|
||||
request.method
|
||||
);
|
||||
|
||||
if (!func) {
|
||||
throw new NotFoundException(
|
||||
`No function for ${request.method.toUpperCase()} ${functionPath}`
|
||||
);
|
||||
}
|
||||
|
||||
await this.functionsService.ensureCpuQuota(project);
|
||||
|
||||
const eventPayload = this.buildEventPayload(request, functionPath);
|
||||
const executionResult = await this.runExecutor(
|
||||
projectSlug,
|
||||
func.code,
|
||||
eventPayload
|
||||
);
|
||||
|
||||
await this.functionsService.recordCpuUsage(
|
||||
project,
|
||||
executionResult.durationMs
|
||||
);
|
||||
|
||||
await this.functionsService.recordInvocation(
|
||||
func._id.toString(),
|
||||
executionResult.exitCode === 0,
|
||||
executionResult.stderr
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().length > 0)
|
||||
);
|
||||
|
||||
if (executionResult.timedOut) {
|
||||
throw new RequestTimeoutException("Function execution timed out");
|
||||
}
|
||||
|
||||
return {
|
||||
project: project.slug,
|
||||
function: func.name,
|
||||
...executionResult,
|
||||
};
|
||||
}
|
||||
|
||||
private extractFunctionPath(
|
||||
pathname: string | undefined,
|
||||
slug: string
|
||||
): string {
|
||||
if (!pathname) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
const needle = `/exec/${slug}`;
|
||||
const index = pathname.indexOf(needle);
|
||||
let relative =
|
||||
index === -1 ? pathname : pathname.slice(index + needle.length);
|
||||
if (!relative || relative.length === 0) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (!relative.startsWith("/")) {
|
||||
relative = `/${relative}`;
|
||||
}
|
||||
|
||||
if (relative.length > 1 && relative.endsWith("/")) {
|
||||
relative = relative.slice(0, -1);
|
||||
}
|
||||
|
||||
return relative || "/";
|
||||
}
|
||||
|
||||
private buildEventPayload(request: Request, functionPath: string) {
|
||||
const body = this.serializeBody(request.body);
|
||||
const pathValue = request.path ?? request.url ?? functionPath;
|
||||
|
||||
return {
|
||||
method: request.method,
|
||||
path: pathValue,
|
||||
originalUrl: request.originalUrl,
|
||||
functionPath,
|
||||
headers: request.headers,
|
||||
query: request.query,
|
||||
params: request.params,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
private serializeBody(body: unknown): unknown {
|
||||
if (Buffer.isBuffer(body)) {
|
||||
return body.toString("utf8");
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private async runExecutor(slug: string, code: string, eventPayload: unknown) {
|
||||
try {
|
||||
return await this.queue.run(() =>
|
||||
this.executeWithinRuntime(slug, code, eventPayload)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ExecutionQueueOverflowError) {
|
||||
throw new ServiceUnavailableException(
|
||||
"Execution queue is full, please retry later"
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeWithinRuntime(
|
||||
slug: string,
|
||||
code: string,
|
||||
eventPayload: unknown
|
||||
) {
|
||||
await fs.mkdir(this.baseRuntimeDir, { recursive: true });
|
||||
const tempDir = path.join(this.baseRuntimeDir, slug, randomUUID());
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
const scriptPath = path.join(tempDir, "function.js");
|
||||
const eventPath = path.join(tempDir, "event.json");
|
||||
|
||||
await fs.writeFile(scriptPath, `${code}\n`, "utf8");
|
||||
await fs.writeFile(
|
||||
eventPath,
|
||||
JSON.stringify(eventPayload, (_, value) => value ?? null, 2),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
const result = await this.spawnExecutor(slug, scriptPath, tempDir);
|
||||
const durationMs = performance.now() - start;
|
||||
|
||||
return {
|
||||
...result,
|
||||
durationMs,
|
||||
responsePayload: await this.extractResponsePayload(result.stdout),
|
||||
};
|
||||
} catch (e) {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: (e as Error).message,
|
||||
exitCode: null,
|
||||
durationMs: performance.now() - start,
|
||||
timedOut: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private spawnExecutor(slug: string, scriptPath: string, cwd: string) {
|
||||
return new Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
timedOut: boolean;
|
||||
}>((resolve, reject) => {
|
||||
const child = spawn(
|
||||
this.executorBinary,
|
||||
["--enable-fs", "--enable-sql", `scope_${slug}`, scriptPath],
|
||||
{
|
||||
cwd,
|
||||
}
|
||||
);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
let timedOut = false;
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill("SIGKILL");
|
||||
}, this.timeoutMs);
|
||||
|
||||
child.on("error", (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(new InternalServerErrorException(err.message));
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timer);
|
||||
resolve({
|
||||
stdout,
|
||||
stderr,
|
||||
exitCode: code,
|
||||
timedOut,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async extractResponsePayload(
|
||||
stdout: string
|
||||
): Promise<ExecutorHttpResponse | null> {
|
||||
try {
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
|
||||
const response = new ExecutorHttpResponse();
|
||||
response.statusCode = parsed.statusCode;
|
||||
response.headers = parsed.headers;
|
||||
response.body = parsed.body;
|
||||
|
||||
const errors = await validate(response);
|
||||
|
||||
if (errors.length === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
aws_sigma_service/api/src/functions/functions.module.ts
Normal file
19
aws_sigma_service/api/src/functions/functions.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MongooseModule } from "@nestjs/mongoose";
|
||||
import { ExecutionController } from "./execution.controller.js";
|
||||
import { FunctionExecutionService } from "./execution.service.js";
|
||||
import { FunctionsService } from "./functions.service.js";
|
||||
import { Project, ProjectSchema } from "../schemas/project.schema.js";
|
||||
import { Function, FunctionSchema } from "../schemas/function.schema.js";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
{ name: Project.name, schema: ProjectSchema },
|
||||
{ name: Function.name, schema: FunctionSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [ExecutionController],
|
||||
providers: [FunctionExecutionService, FunctionsService],
|
||||
})
|
||||
export class FunctionsModule {}
|
||||
162
aws_sigma_service/api/src/functions/functions.service.ts
Normal file
162
aws_sigma_service/api/src/functions/functions.service.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import type { Model } from "mongoose";
|
||||
import { Project, type ProjectDocument } from "../schemas/project.schema.js";
|
||||
import {
|
||||
Function as SigmaFunction,
|
||||
type FunctionDocument,
|
||||
} from "../schemas/function.schema.js";
|
||||
|
||||
@Injectable()
|
||||
export class FunctionsService {
|
||||
constructor(
|
||||
@InjectModel(Project.name)
|
||||
private readonly projectModel: Model<Project>,
|
||||
@InjectModel(SigmaFunction.name)
|
||||
private readonly functionModel: Model<SigmaFunction>
|
||||
) {}
|
||||
|
||||
async findProjectBySlug(slug: string): Promise<ProjectDocument | null> {
|
||||
return this.projectModel.findOne({ slug }).exec();
|
||||
}
|
||||
|
||||
async findRunnableFunction(
|
||||
projectId: ProjectDocument["_id"],
|
||||
requestPath: string,
|
||||
method: string
|
||||
): Promise<FunctionDocument | null> {
|
||||
const functions = await this.functionModel
|
||||
.find({ project: projectId })
|
||||
.exec();
|
||||
|
||||
const normalizedPath = this.normalizePath(requestPath);
|
||||
const upperMethod = method.toUpperCase();
|
||||
|
||||
for (const func of functions) {
|
||||
if (!this.supportsMethod(func, upperMethod)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.pathMatches(func.path, normalizedPath)) {
|
||||
return func;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async ensureCpuQuota(project: ProjectDocument): Promise<void> {
|
||||
const quota = project.cpuTimeQuotaMsPerMinute ?? 1000;
|
||||
const updated = this.resetUsageWindowIfNeeded(project);
|
||||
if (project.cpuTimeUsedMs >= quota) {
|
||||
throw new HttpException(
|
||||
`CPU time quota exceeded for project ${project.slug}`,
|
||||
HttpStatus.TOO_MANY_REQUESTS
|
||||
);
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await project.save();
|
||||
}
|
||||
}
|
||||
|
||||
async recordCpuUsage(
|
||||
project: ProjectDocument,
|
||||
elapsedMs: number
|
||||
): Promise<void> {
|
||||
const quota = project.cpuTimeQuotaMsPerMinute ?? 1000;
|
||||
this.resetUsageWindowIfNeeded(project);
|
||||
project.cpuTimeUsedMs = Math.min(
|
||||
quota,
|
||||
(project.cpuTimeUsedMs ?? 0) + Math.max(0, Math.round(elapsedMs))
|
||||
);
|
||||
|
||||
await project.save();
|
||||
}
|
||||
|
||||
async recordInvocation(
|
||||
functionId: string,
|
||||
successful: boolean,
|
||||
logs: string[]
|
||||
): Promise<void> {
|
||||
const func = await this.functionModel.findByIdAndUpdate(functionId, {
|
||||
$inc: { invocationCount: 1, errorCount: successful ? 0 : 1 },
|
||||
lastInvocation: new Date(),
|
||||
$push: { logs: { $each: logs, $slice: -100 } },
|
||||
});
|
||||
|
||||
if (func) {
|
||||
await this.projectModel.findByIdAndUpdate(func._id, {
|
||||
$inc: {
|
||||
functionsInvocationCount: 1,
|
||||
functionsErrorCount: successful ? 0 : 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private supportsMethod(func: FunctionDocument, method: string): boolean {
|
||||
if (!func.methods || func.methods.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return func.methods.some((allowed) => allowed.toUpperCase() === method);
|
||||
}
|
||||
|
||||
private pathMatches(pattern: string, requestPath: string): boolean {
|
||||
const normalizedPattern = this.normalizePath(pattern);
|
||||
if (normalizedPattern === "/*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const patternSegments = normalizedPattern.split("/").filter(Boolean);
|
||||
const requestSegments = requestPath.split("/").filter(Boolean);
|
||||
|
||||
if (patternSegments.length !== requestSegments.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return patternSegments.every((segment, index) => {
|
||||
const value = requestSegments[index];
|
||||
if (segment.startsWith(":")) {
|
||||
return typeof value === "string" && value.length > 0;
|
||||
}
|
||||
return segment === value;
|
||||
});
|
||||
}
|
||||
|
||||
private normalizePath(input: string): string {
|
||||
if (!input || input === "") {
|
||||
return "/";
|
||||
}
|
||||
|
||||
let normalized = input.startsWith("/") ? input : `/${input}`;
|
||||
if (normalized.length > 1 && normalized.endsWith("/")) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private resetUsageWindowIfNeeded(project: ProjectDocument): boolean {
|
||||
const now = Date.now();
|
||||
const startedAt = project.cpuTimeWindowStartedAt?.getTime() ?? 0;
|
||||
if (now - startedAt >= 60_000) {
|
||||
project.cpuTimeWindowStartedAt = new Date(now);
|
||||
project.cpuTimeUsedMs = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!project.cpuTimeWindowStartedAt) {
|
||||
project.cpuTimeWindowStartedAt = new Date(now);
|
||||
project.cpuTimeUsedMs = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (project.cpuTimeUsedMs == null) {
|
||||
project.cpuTimeUsedMs = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
24
aws_sigma_service/api/src/main.ts
Normal file
24
aws_sigma_service/api/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { AppModule } from "./app.module.js";
|
||||
import { ValidationPipe } from "@nestjs/common";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
cors: {
|
||||
credentials: true,
|
||||
origin: "http://localhost:1234",
|
||||
},
|
||||
// logger: ["debug", "log", "error", "warn", "fatal"],
|
||||
});
|
||||
app.setGlobalPrefix("/api/v1");
|
||||
app.set("query parser", "extended");
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: false,
|
||||
})
|
||||
);
|
||||
|
||||
await app.listen(process.env.PORT || 3000);
|
||||
}
|
||||
bootstrap();
|
||||
36
aws_sigma_service/api/src/schemas/function.schema.ts
Normal file
36
aws_sigma_service/api/src/schemas/function.schema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import type { HydratedDocument } from "mongoose";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
@Schema()
|
||||
export class Function {
|
||||
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: "Project" })
|
||||
project!: mongoose.Types.ObjectId;
|
||||
|
||||
@Prop()
|
||||
name!: string;
|
||||
|
||||
@Prop()
|
||||
path!: string;
|
||||
|
||||
@Prop()
|
||||
methods!: string[];
|
||||
|
||||
@Prop({ default: "// Write your function code here" })
|
||||
code!: string;
|
||||
|
||||
@Prop()
|
||||
invocationCount: number = 0;
|
||||
|
||||
@Prop()
|
||||
errorCount: number = 0;
|
||||
|
||||
@Prop()
|
||||
logs: string[] = [];
|
||||
|
||||
@Prop()
|
||||
lastInvocation?: Date;
|
||||
}
|
||||
|
||||
export type FunctionDocument = HydratedDocument<Function>;
|
||||
export const FunctionSchema = SchemaFactory.createForClass(Function);
|
||||
43
aws_sigma_service/api/src/schemas/project.schema.ts
Normal file
43
aws_sigma_service/api/src/schemas/project.schema.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import type { Function } from "./function.schema.js";
|
||||
import type { HydratedDocument } from "mongoose";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
@Schema()
|
||||
export class Project {
|
||||
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: "User" })
|
||||
owner!: mongoose.Types.ObjectId;
|
||||
|
||||
@Prop()
|
||||
name!: string;
|
||||
|
||||
@Prop({ unique: true })
|
||||
slug!: string;
|
||||
|
||||
@Prop()
|
||||
description!: string;
|
||||
|
||||
@Prop({
|
||||
type: [{ type: mongoose.Schema.Types.ObjectId, ref: "Function" }],
|
||||
default: [],
|
||||
})
|
||||
functions!: Function[];
|
||||
|
||||
@Prop({ default: 1000 })
|
||||
cpuTimeQuotaMsPerMinute!: number;
|
||||
|
||||
@Prop({ default: Date.now })
|
||||
cpuTimeWindowStartedAt!: Date;
|
||||
|
||||
@Prop({ default: 0 })
|
||||
cpuTimeUsedMs!: number;
|
||||
|
||||
@Prop()
|
||||
functionsInvocationCount: number = 0;
|
||||
|
||||
@Prop()
|
||||
functionsErrorCount: number = 0;
|
||||
}
|
||||
|
||||
export type ProjectDocument = HydratedDocument<Project>;
|
||||
export const ProjectSchema = SchemaFactory.createForClass(Project);
|
||||
22
aws_sigma_service/api/src/schemas/user.schema.ts
Normal file
22
aws_sigma_service/api/src/schemas/user.schema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import type { HydratedDocument } from "mongoose";
|
||||
import mongoose from "mongoose";
|
||||
import type { Project } from "./project.schema.js";
|
||||
|
||||
@Schema()
|
||||
export class User {
|
||||
@Prop({ unique: true })
|
||||
username!: string;
|
||||
|
||||
@Prop()
|
||||
passwordHash!: string;
|
||||
|
||||
@Prop({
|
||||
type: [{ type: mongoose.Schema.Types.ObjectId, ref: "Project" }],
|
||||
default: [],
|
||||
})
|
||||
projects!: Project[];
|
||||
}
|
||||
|
||||
export type UserDocument = HydratedDocument<User>;
|
||||
export const UserSchema = SchemaFactory.createForClass(User);
|
||||
41
aws_sigma_service/api/src/services/auth.service.ts
Normal file
41
aws_sigma_service/api/src/services/auth.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { UsersService } from "./users.service.js";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import type { UserDocument } from "../schemas/user.schema.js";
|
||||
import { BcryptService } from "./bcrypt.service.js";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private jwtService: JwtService,
|
||||
private bcryptService: BcryptService
|
||||
) {}
|
||||
|
||||
async validateUser(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<UserDocument | null> {
|
||||
const user = await this.usersService.findOne(username);
|
||||
|
||||
if (
|
||||
user &&
|
||||
(await this.bcryptService.comparePasswords(password, user.passwordHash))
|
||||
) {
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: UserDocument): Promise<{ auth_token: string }> {
|
||||
const payload = {
|
||||
sub: user._id,
|
||||
username: user.username,
|
||||
};
|
||||
|
||||
return {
|
||||
auth_token: await this.jwtService.signAsync(payload),
|
||||
};
|
||||
}
|
||||
}
|
||||
15
aws_sigma_service/api/src/services/bcrypt.service.ts
Normal file
15
aws_sigma_service/api/src/services/bcrypt.service.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class BcryptService {
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 10;
|
||||
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
async comparePasswords(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
}
|
||||
42
aws_sigma_service/api/src/services/health.service.ts
Normal file
42
aws_sigma_service/api/src/services/health.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as os from "os";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
private totalTime?: number;
|
||||
private usedTime?: number;
|
||||
private currentUsage?: number;
|
||||
|
||||
constructor() {
|
||||
setInterval(() => this.updateCpuUsage(), 1000);
|
||||
}
|
||||
|
||||
private updateCpuUsage() {
|
||||
const cpus = os.cpus();
|
||||
|
||||
const totalTime = cpus.reduce(
|
||||
(acc, cpu) =>
|
||||
acc + Object.values(cpu.times).reduce((acc, time) => acc + time, 0),
|
||||
0
|
||||
);
|
||||
|
||||
const usedTime = cpus.reduce(
|
||||
(acc, cpu) => acc + cpu.times.user + cpu.times.sys,
|
||||
0
|
||||
);
|
||||
|
||||
if (this.totalTime && this.usedTime) {
|
||||
const totalDiff = totalTime - this.totalTime;
|
||||
const usedDiff = usedTime - this.usedTime;
|
||||
|
||||
this.currentUsage = (usedDiff / totalDiff) * 100;
|
||||
}
|
||||
|
||||
this.totalTime = totalTime;
|
||||
this.usedTime = usedTime;
|
||||
}
|
||||
|
||||
getServerCpuUsage(): number {
|
||||
return this.currentUsage ?? 0;
|
||||
}
|
||||
}
|
||||
111
aws_sigma_service/api/src/services/project-functions.service.ts
Normal file
111
aws_sigma_service/api/src/services/project-functions.service.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import type { Model, ObjectId } from "mongoose";
|
||||
import { Project, type ProjectDocument } from "../schemas/project.schema.js";
|
||||
import {
|
||||
Function as SigmaFunction,
|
||||
type FunctionDocument,
|
||||
} from "../schemas/function.schema.js";
|
||||
import { CreateFunctionDto, UpdateFunctionDto } from "@sigma/common";
|
||||
|
||||
@Injectable()
|
||||
export class ProjectFunctionsService {
|
||||
constructor(
|
||||
@InjectModel(Project.name)
|
||||
private readonly projectModel: Model<Project>,
|
||||
@InjectModel(SigmaFunction.name)
|
||||
private readonly functionModel: Model<SigmaFunction>
|
||||
) {}
|
||||
|
||||
async create(
|
||||
owner: ObjectId,
|
||||
projectId: string,
|
||||
payload: CreateFunctionDto
|
||||
): Promise<FunctionDocument> {
|
||||
const project = await this.projectModel
|
||||
.findOne({
|
||||
_id: projectId,
|
||||
owner,
|
||||
})
|
||||
.exec();
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException("Project not found");
|
||||
}
|
||||
|
||||
const createdFunction = await this.functionModel.create({
|
||||
project: project._id,
|
||||
name: payload.name,
|
||||
path: payload.path,
|
||||
});
|
||||
|
||||
await this.appendFunctionReference(project, createdFunction._id);
|
||||
|
||||
return createdFunction;
|
||||
}
|
||||
|
||||
async delete(
|
||||
owner: ObjectId,
|
||||
projectId: string,
|
||||
functionId: string
|
||||
): Promise<FunctionDocument | null> {
|
||||
const project = await this.projectModel.findOne({
|
||||
_id: projectId,
|
||||
owner,
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException("Project not found");
|
||||
}
|
||||
|
||||
const func = await this.functionModel.findOneAndDelete({
|
||||
project: project._id,
|
||||
_id: functionId,
|
||||
});
|
||||
|
||||
await project.updateOne(
|
||||
{
|
||||
functions: project.functions.filter(
|
||||
(funcId) => funcId.toString() !== functionId
|
||||
),
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
return func;
|
||||
}
|
||||
|
||||
async update(
|
||||
owner: ObjectId,
|
||||
projectId: string,
|
||||
functionId: string,
|
||||
payload: UpdateFunctionDto
|
||||
): Promise<FunctionDocument | null> {
|
||||
const project = await this.projectModel.findOne({
|
||||
_id: projectId,
|
||||
owner,
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException("Project not found");
|
||||
}
|
||||
|
||||
return await this.functionModel.findOneAndUpdate(
|
||||
{
|
||||
project: project._id,
|
||||
_id: functionId,
|
||||
},
|
||||
payload,
|
||||
{ new: true }
|
||||
);
|
||||
}
|
||||
|
||||
private async appendFunctionReference(
|
||||
project: ProjectDocument,
|
||||
functionId: FunctionDocument["_id"]
|
||||
): Promise<void> {
|
||||
await this.projectModel
|
||||
.updateOne({ _id: project._id }, { $addToSet: { functions: functionId } })
|
||||
.exec();
|
||||
}
|
||||
}
|
||||
126
aws_sigma_service/api/src/services/projects.service.ts
Normal file
126
aws_sigma_service/api/src/services/projects.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import type { Model, ObjectId } from "mongoose";
|
||||
import { Project, type ProjectDocument } from "../schemas/project.schema.js";
|
||||
import {
|
||||
Function as SigmaFunction,
|
||||
type FunctionDocument,
|
||||
} from "../schemas/function.schema.js";
|
||||
import {
|
||||
CreateProjectDto,
|
||||
FunctionDto,
|
||||
ProjectDto,
|
||||
UpdateProjectDto,
|
||||
} from "@sigma/common";
|
||||
|
||||
@Injectable()
|
||||
export class ProjectsService {
|
||||
constructor(
|
||||
@InjectModel(Project.name) private projectModel: Model<Project>,
|
||||
@InjectModel(SigmaFunction.name)
|
||||
private functionModel: Model<SigmaFunction>
|
||||
) {}
|
||||
|
||||
async create(
|
||||
owner: ObjectId,
|
||||
createProjectDto: CreateProjectDto
|
||||
): Promise<ProjectDocument> {
|
||||
const createdProject = new this.projectModel({
|
||||
...createProjectDto,
|
||||
owner,
|
||||
});
|
||||
|
||||
return createdProject.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
owner: ObjectId,
|
||||
query: UpdateProjectDto
|
||||
): Promise<ProjectDocument[]> {
|
||||
return this.projectModel
|
||||
.find({
|
||||
owner,
|
||||
...query,
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
async findOne(owner: ObjectId, id: string): Promise<ProjectDto | null> {
|
||||
const project = await this.projectModel
|
||||
.findOne({
|
||||
_id: id,
|
||||
owner,
|
||||
})
|
||||
.exec();
|
||||
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functions = await this.functionModel
|
||||
.find({ project: project._id })
|
||||
.exec();
|
||||
|
||||
return this.toProjectDto(project, functions);
|
||||
}
|
||||
|
||||
async update(
|
||||
owner: ObjectId,
|
||||
id: string,
|
||||
updateProjectDto: UpdateProjectDto
|
||||
): Promise<ProjectDocument | null> {
|
||||
return this.projectModel
|
||||
.findOneAndUpdate(
|
||||
{
|
||||
_id: id,
|
||||
owner,
|
||||
},
|
||||
updateProjectDto,
|
||||
{ new: true }
|
||||
)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async remove(owner: ObjectId, id: string): Promise<ProjectDocument | null> {
|
||||
return this.projectModel
|
||||
.findOneAndDelete({
|
||||
_id: id,
|
||||
owner,
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
private toProjectDto(
|
||||
project: ProjectDocument,
|
||||
functions: FunctionDocument[]
|
||||
): ProjectDto {
|
||||
return {
|
||||
_id: project._id.toString(),
|
||||
name: project.name,
|
||||
slug: project.slug,
|
||||
description: project.description,
|
||||
cpuTimeQuotaMsPerMinute: project.cpuTimeQuotaMsPerMinute ?? 0,
|
||||
cpuTimeWindowStartedAt:
|
||||
project.cpuTimeWindowStartedAt?.toISOString() ??
|
||||
new Date(0).toISOString(),
|
||||
cpuTimeUsedMs: project.cpuTimeUsedMs ?? 0,
|
||||
functionsInvocationCount: project.functionsInvocationCount ?? 0,
|
||||
functionsErrorCount: project.functionsErrorCount ?? 0,
|
||||
functions: functions.map((func) => this.toFunctionDto(func)),
|
||||
};
|
||||
}
|
||||
|
||||
private toFunctionDto(func: FunctionDocument): FunctionDto {
|
||||
return {
|
||||
_id: func._id.toString(),
|
||||
name: func.name,
|
||||
path: func.path,
|
||||
methods: func.methods ?? [],
|
||||
code: func.code,
|
||||
invocations: func.invocationCount ?? 0,
|
||||
errors: func.errorCount ?? 0,
|
||||
logs: func.logs ?? [],
|
||||
lastInvocation: func.lastInvocation,
|
||||
};
|
||||
}
|
||||
}
|
||||
27
aws_sigma_service/api/src/services/users.service.ts
Normal file
27
aws_sigma_service/api/src/services/users.service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import { User, type UserDocument } from "../schemas/user.schema.js";
|
||||
import type { Model } from "mongoose";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(@InjectModel(User.name) private userModel: Model<User>) {}
|
||||
|
||||
async create(user: User): Promise<UserDocument> {
|
||||
const createdUser = new this.userModel(user);
|
||||
return createdUser.save();
|
||||
}
|
||||
|
||||
async findOne(username: string): Promise<UserDocument | null> {
|
||||
return this.userModel.findOne({
|
||||
username: username,
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
updateData: Partial<User>
|
||||
): Promise<UserDocument | null> {
|
||||
return this.userModel.findByIdAndUpdate(id, updateData, { new: true });
|
||||
}
|
||||
}
|
||||
4
aws_sigma_service/api/tsconfig.build.json
Normal file
4
aws_sigma_service/api/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
10
aws_sigma_service/api/tsconfig.json
Normal file
10
aws_sigma_service/api/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["node"],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user