Skip to content

Commit

Permalink
Merge branch 'main' into feat-backend-porject-relations
Browse files Browse the repository at this point in the history
  • Loading branch information
NarwhalChen authored Feb 5, 2025
2 parents 1d886a6 + 00f2df6 commit f741eb4
Show file tree
Hide file tree
Showing 23 changed files with 761 additions and 401 deletions.
4 changes: 2 additions & 2 deletions backend/src/build-system/__tests__/mock/MockBuilderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export class MockBuilderContext extends BuilderContext {
return this.virtualDirectory.parseJsonStructure(jsonContent);
}

execute(): Promise<void> {
return Promise.resolve(); // Mock a resolved promise for execution
execute(): Promise<string> {
return Promise.resolve(''); // Mock a resolved promise for execution
}

getNodeData(handler: any): any {
Expand Down
16 changes: 10 additions & 6 deletions backend/src/build-system/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,21 @@ export class BuilderContext {
this.handlerManager = BuildHandlerManager.getInstance();
this.model = OpenAIModelProvider.getInstance();
this.monitor = BuildMonitor.getInstance();
this.logger = new Logger(`builder-context-${id}`);
this.logger = new Logger(`builder-context-${id ?? sequence.id}`);
this.virtualDirectory = new VirtualDirectory();

// Initialize global context with default project values
this.globalContext.set('projectName', sequence.name);
this.globalContext.set('description', sequence.description || '');
this.globalContext.set('platform', 'web'); // Default platform is 'web'
this.globalContext.set('databaseType', sequence.databaseType || 'SQLite');
this.globalContext.set(
'projectUUID',
new Date().toISOString().slice(0, 10).replace(/:/g, '-') + '-' + uuidv4(),
);

const projectUUIDPath =
new Date().toISOString().slice(0, 18).replaceAll(/:/g, '-') +
'-' +
uuidv4();
this.globalContext.set('projectUUID', projectUUIDPath);


if (process.env.DEBUG) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
Expand Down Expand Up @@ -311,7 +314,7 @@ export class BuilderContext {
* @returns A promise that resolves when the entire build sequence is complete.
*/

async execute(): Promise<void> {
async execute(): Promise<string> {
try {
const nodes = this.sequence.nodes;
let currentIndex = 0;
Expand Down Expand Up @@ -368,6 +371,7 @@ export class BuilderContext {
await Promise.all(Array.from(runningPromises));
await new Promise((resolve) => setTimeout(resolve, this.POLL_INTERVAL));
}
return this.getGlobalContext('projectUUID');
} catch (error) {
this.writeLog('execution-error.json', {
error: error.message,
Expand Down
5 changes: 5 additions & 0 deletions backend/src/build-system/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export interface BuildSequence {
description?: string;
databaseType?: string;
nodes: BuildNode[];
packages: BuildProjectPackage[];
}
export interface BuildProjectPackage {
name: string;
version: string;
}

/**
Expand Down
6 changes: 5 additions & 1 deletion backend/src/common/model-provider/openai-model-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class OpenAIModelProvider implements IModelProvider {
private readonly logger = new Logger('OpenAIModelProvider');
private queues: Map<string, PQueue> = new Map();
private configLoader: ConfigLoader;
private defaultModel: string;

private constructor() {
this.openai = new OpenAI({
Expand All @@ -35,6 +36,9 @@ export class OpenAIModelProvider implements IModelProvider {
const chatModels = this.configLoader.getAllChatModelConfigs();

for (const model of chatModels) {
if (model.default) {
this.defaultModel = model.model;
}
if (!model.endpoint || !model.token) continue;

const key = this.getQueueKey(model);
Expand Down Expand Up @@ -87,7 +91,7 @@ export class OpenAIModelProvider implements IModelProvider {

async chatSync(input: ChatInput): Promise<string> {
try {
const queue = this.getQueueForModel(input.model);
const queue = this.getQueueForModel(input.model ?? this.defaultModel);
const completion = await queue.add(async () => {
const result = await this.openai.chat.completions.create({
messages: input.messages,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/common/model-provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export interface MessageInterface {
}

export interface ChatInput {
model: string;
// if model not provided, default model will be used
model?: string;
messages: MessageInterface[];
}

Expand Down
73 changes: 58 additions & 15 deletions backend/src/guard/project.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
CanActivate,
ExecutionContext,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { JwtService } from '@nestjs/jwt';
Expand All @@ -11,6 +12,8 @@ import { ProjectService } from '../project/project.service';

@Injectable()
export class ProjectGuard implements CanActivate {
private readonly logger = new Logger('ProjectGuard');

constructor(
private readonly projectsService: ProjectService,
private readonly jwtService: JwtService,
Expand All @@ -19,38 +22,78 @@ export class ProjectGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const args = gqlContext.getArgs();

// Verify and decode JWT token
const user = await this.validateToken(request);

// Extract project identifier from arguments
const projectIdentifier = this.extractProjectIdentifier(args);

if (!projectIdentifier) {
this.logger.debug('No project identifier found in request');
return true; // Skip check if no project identifier is found
}

// Extract the authorization header
// Validate project ownership
await this.validateProjectOwnership(projectIdentifier, user.userId);

// Store user in request context for later use
request.user = user;
return true;
}

private async validateToken(request: any): Promise<any> {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Authorization token is missing');
}

// Decode the token to get user information
const token = authHeader.split(' ')[1];
let user: any;
try {
user = this.jwtService.verify(token);
return this.jwtService.verify(token);
} catch (error) {
this.logger.error(`Token validation failed: ${error.message}`);
throw new UnauthorizedException('Invalid token');
}
}

// Extract projectId from the request arguments
const args = gqlContext.getArgs();
const { projectId } = args;
private extractProjectIdentifier(args: any): string | undefined {
// Handle different input formats
if (args.projectId) return args.projectId;
if (args.input?.projectId) return args.input.projectId;
if (args.isValidProject?.projectId) return args.isValidProject.projectId;
if (args.projectPath) return args.projectPath;
if (args.input?.projectPath) return args.input.projectPath;
if (args.isValidProject?.projectPath)
return args.isValidProject.projectPath;

// Fetch the project and check if the userId matches the project's userId
const project = await this.projectsService.getProjectById(projectId);
if (!project) {
return undefined;
}

private async validateProjectOwnership(
projectIdentifier: string,
userId: number,
): Promise<void> {
let project;
try {
project = await this.projectsService.getProjectById(projectIdentifier);
} catch (error) {
this.logger.error(`Failed to fetch project: ${error.message}`);
throw new UnauthorizedException('Project not found');
}

//To do: In the feature when we need allow teams add check here

if (project.userId !== user.userId) {
throw new UnauthorizedException('User is not the owner of the project');
if (!project) {
throw new UnauthorizedException('Project not found');
}

return true;
if (project.userId !== userId) {
this.logger.warn(
`Unauthorized access attempt: User ${userId} tried to access project ${projectIdentifier}`,
);
throw new UnauthorizedException(
'User is not authorized to access this project',
);
}
}
}
123 changes: 123 additions & 0 deletions backend/src/project/build-system-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { BackendCodeHandler } from 'src/build-system/handlers/backend/code-generate';
import { BackendFileReviewHandler } from 'src/build-system/handlers/backend/file-review/file-review';
import { BackendRequirementHandler } from 'src/build-system/handlers/backend/requirements-document';
import { DBRequirementHandler } from 'src/build-system/handlers/database/requirements-document';
import { DBSchemaHandler } from 'src/build-system/handlers/database/schemas/schemas';
import { FileFAHandler } from 'src/build-system/handlers/file-manager/file-arch';
import { FileStructureHandler } from 'src/build-system/handlers/file-manager/file-structure';
import { FrontendCodeHandler } from 'src/build-system/handlers/frontend-code-generate';
import { PRDHandler } from 'src/build-system/handlers/product-manager/product-requirements-document/prd';
import { ProjectInitHandler } from 'src/build-system/handlers/project-init';
import { UXDMDHandler } from 'src/build-system/handlers/ux/datamap';
import { UXSMDHandler } from 'src/build-system/handlers/ux/sitemap-document';
import { UXSMSHandler } from 'src/build-system/handlers/ux/sitemap-structure';
import { UXSMSPageByPageHandler } from 'src/build-system/handlers/ux/sitemap-structure/sms-page';
import { BuildSequence } from 'src/build-system/types';
import { v4 as uuidv4 } from 'uuid';
import { CreateProjectInput } from './dto/project.input';

export function buildProjectSequenceByProject(
input: CreateProjectInput,
): BuildSequence {
const sequence: BuildSequence = {
id: uuidv4(),
version: '1.0.0',
name: input.projectName,
description: input.description,
databaseType: input.databaseType,
packages: input.packages,
nodes: [
{
handler: ProjectInitHandler,
name: 'Project Folders Setup',
},
{
handler: PRDHandler,
name: 'Project Requirements Document Node',
},
{
handler: UXSMDHandler,
name: 'UX Sitemap Document Node',
},
{
handler: UXSMSHandler,
name: 'UX Sitemap Structure Node',
},
{
handler: UXDMDHandler,
name: 'UX DataMap Document Node',
},
{
handler: DBRequirementHandler,
name: 'Database Requirements Node',
},
{
handler: FileStructureHandler,
name: 'File Structure Generation',
options: {
projectPart: 'frontend',
},
},
{
handler: UXSMSPageByPageHandler,
name: 'Level 2 UX Sitemap Structure Node details',
},
{
handler: DBSchemaHandler,
name: 'Database Schemas Node',
},
{
handler: FileFAHandler,
name: 'File Arch',
},
{
handler: BackendRequirementHandler,
name: 'Backend Requirements Node',
},
{
handler: BackendCodeHandler,
name: 'Backend Code Generator Node',
},
{
handler: BackendFileReviewHandler,
name: 'Backend File Review Node',
},
{
handler: FrontendCodeHandler,
name: 'Frontend Code Generator Node',
},
],
};
return sequence;
}

/**
* Generates a project name prompt based on the provided description.
*/
export function generateProjectNamePrompt(description: string): string {
return `You are a project name generator. Based on the following project description, generate a concise, memorable, and meaningful project name.
Input Description: ${description}
Requirements for the project name:
1. Must be 1-3 words maximum
2. Should be clear and professional
3. Avoid generic terms like "project" or "system"
4. Use camelCase or kebab-case format
5. Should reflect the core functionality or purpose
6. Must be unique and memorable
7. Should be easy to pronounce
8. Avoid acronyms unless they're very intuitive
Please respond ONLY with the project name, without any explanation or additional text.
Example inputs and outputs:
Description: "A task management system with real-time collaboration features"
Output: taskFlow
Description: "An AI-powered document analysis and extraction system"
Output: docMind
Description: "A microservice-based e-commerce platform with advanced inventory management"
Output: tradeCore`;
}
36 changes: 36 additions & 0 deletions backend/src/project/dto/project.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import { InputType, Field, ID } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsUUID, IsOptional } from 'class-validator';

/**
* @deprecated We don't need project upsert
*/
@InputType()
export class UpsertProjectInput {
@Field()
Expand All @@ -24,3 +27,36 @@ export class UpsertProjectInput {
@IsString({ each: true })
projectPackages?: string[];
}

@InputType()
export class CreateProjectInput {
@Field(() => String, { nullable: true })
projectName?: string;

@Field()
description: string;

@Field(() => [ProjectPackage])
packages: ProjectPackage[];

@Field(() => String, { nullable: true })
databaseType?: string;
}

@InputType()
export class ProjectPackage {
@Field()
name: string;

@Field()
version: string;
}

@InputType()
export class IsValidProjectInput {
@Field(() => ID)
projectId: string;

@Field(() => String, { nullable: true })
projectPath: string;
}
Loading

0 comments on commit f741eb4

Please sign in to comment.