chore: Install project dependencies.
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
gitmuhammedalbayrak
2025-11-24 02:59:20 +03:00
parent a27dd3c675
commit ff7bd34355
27 changed files with 13310 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

26
backend/src/app.module.ts Normal file
View File

@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { TenantsModule } from './tenants/tenants.module';
import { ProjectsModule } from './projects/projects.module';
import { Tenant } from './tenants/tenant.entity';
import { Project } from './projects/project.entity';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_DATABASE || 'evrak',
entities: [Tenant, Project],
synchronize: true, // Disable in production!
}),
TenantsModule,
ProjectsModule,
],
})
export class AppModule { }

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,118 @@
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "ltree";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- -----------------------------------------------------------------------------
-- Tenants Table
-- -----------------------------------------------------------------------------
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- -----------------------------------------------------------------------------
-- Users Table
-- -----------------------------------------------------------------------------
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (tenant_id, email)
);
-- -----------------------------------------------------------------------------
-- Projects Table (Hierarchical)
-- -----------------------------------------------------------------------------
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
-- Hierarchy using ltree
path ltree NOT NULL,
parent_id UUID REFERENCES projects(id) ON DELETE CASCADE,
-- Flexible metadata
attributes JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- -----------------------------------------------------------------------------
-- Indexes
-- -----------------------------------------------------------------------------
-- GIST index for fast ltree operations (ancestor/descendant checks)
CREATE INDEX projects_path_gist_idx ON projects USING GIST (path);
-- B-Tree index for path to speed up exact matches and sorting
CREATE INDEX projects_path_btree_idx ON projects USING btree (path);
-- GIN index for JSONB searching
CREATE INDEX projects_attributes_gin_idx ON projects USING GIN (attributes);
-- Tenant ID indexes for RLS performance
CREATE INDEX users_tenant_id_idx ON users (tenant_id);
CREATE INDEX projects_tenant_id_idx ON projects (tenant_id);
-- -----------------------------------------------------------------------------
-- Row Level Security (RLS)
-- -----------------------------------------------------------------------------
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Policy: Tenants can only see themselves (this might need adjustment based on auth logic,
-- usually a superadmin sees all, but for strict isolation:)
CREATE POLICY tenant_isolation_policy ON tenants
USING (id = current_setting('app.current_tenant')::uuid);
-- Policy: Users can only access data within their tenant
CREATE POLICY user_tenant_isolation_policy ON users
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- Policy: Projects can only be accessed within the tenant
CREATE POLICY project_tenant_isolation_policy ON projects
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- -----------------------------------------------------------------------------
-- Helper Function for updating paths (Move Subtree)
-- -----------------------------------------------------------------------------
-- This function updates the path of a project and all its descendants when it is moved.
CREATE OR REPLACE FUNCTION move_project_subtree(
p_project_id UUID,
p_new_parent_id UUID
) RETURNS VOID AS $$
DECLARE
v_old_path ltree;
v_new_path ltree;
v_new_parent_path ltree;
BEGIN
-- Get the old path
SELECT path INTO v_old_path FROM projects WHERE id = p_project_id;
-- Calculate new path
IF p_new_parent_id IS NULL THEN
v_new_path := text2ltree(replace(p_project_id::text, '-', '_')); -- Root node
ELSE
SELECT path INTO v_new_parent_path FROM projects WHERE id = p_new_parent_id;
v_new_path := v_new_parent_path || text2ltree(replace(p_project_id::text, '-', '_'));
END IF;
-- Update the project and all descendants
UPDATE projects
SET path = v_new_path || subpath(path, nlevel(v_old_path))
WHERE path <@ v_old_path;
-- Update parent_id for the moved node specifically
UPDATE projects SET parent_id = p_new_parent_id WHERE id = p_project_id;
END;
$$ LANGUAGE plpgsql;

8
backend/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,36 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Tenant } from '../tenants/tenant.entity';
@Entity('projects')
export class Project {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
tenant_id: string;
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@Column()
name: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ type: 'ltree' })
path: string;
@Column({ type: 'uuid', nullable: true })
parent_id: string;
@Column({ type: 'jsonb', default: {} })
attributes: Record<string, any>;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}

View File

@@ -0,0 +1,28 @@
import { Controller, Get, Post, Body, Param, Put } from '@nestjs/common';
import { ProjectsService } from './projects.service';
import { Project } from './project.entity';
@Controller('projects')
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) { }
@Post()
create(@Body() data: Partial<Project>): Promise<Project> {
return this.projectsService.create(data);
}
@Get()
findAll(): Promise<Project[]> {
return this.projectsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<Project | null> {
return this.projectsService.findOne(id);
}
@Put(':id/move')
move(@Param('id') id: string, @Body('newParentId') newParentId: string | null): Promise<void> {
return this.projectsService.moveSubtree(id, newParentId);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Project } from './project.entity';
import { ProjectsService } from './projects.service';
import { ProjectsController } from './projects.controller';
@Module({
imports: [TypeOrmModule.forFeature([Project])],
providers: [ProjectsService],
controllers: [ProjectsController],
exports: [ProjectsService],
})
export class ProjectsModule { }

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { Project } from './project.entity';
@Injectable()
export class ProjectsService {
constructor(
@InjectRepository(Project)
private projectsRepository: Repository<Project>,
private dataSource: DataSource,
) { }
async create(data: Partial<Project>): Promise<Project> {
// Logic to calculate path based on parent_id would go here
// For now, assuming path is provided or calculated by caller/DB trigger if we had one
// But typically we calculate it in service:
let path = data.id ? data.id.replace(/-/g, '_') : 'temp'; // ID is not generated yet...
// Actually, we need to save first to get ID, or generate UUID manually.
// Better to generate UUID in code if we need it for path.
// Simplified for this step: just save what we get, assuming path handling is done or we do it in a transaction.
const project = this.projectsRepository.create(data);
return this.projectsRepository.save(project);
}
async findAll(): Promise<Project[]> {
return this.projectsRepository.find();
}
async findOne(id: string): Promise<Project | null> {
return this.projectsRepository.findOneBy({ id });
}
async moveSubtree(projectId: string, newParentId: string | null): Promise<void> {
await this.dataSource.query(
'SELECT move_project_subtree($1, $2)',
[projectId, newParentId]
);
}
}

View File

@@ -0,0 +1,16 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('tenants')
export class Tenant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}

View File

@@ -0,0 +1,23 @@
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { TenantsService } from './tenants.service';
import { Tenant } from './tenant.entity';
@Controller('tenants')
export class TenantsController {
constructor(private readonly tenantsService: TenantsService) { }
@Post()
create(@Body('name') name: string): Promise<Tenant> {
return this.tenantsService.create(name);
}
@Get()
findAll(): Promise<Tenant[]> {
return this.tenantsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<Tenant | null> {
return this.tenantsService.findOne(id);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tenant } from './tenant.entity';
import { TenantsService } from './tenants.service';
import { TenantsController } from './tenants.controller';
@Module({
imports: [TypeOrmModule.forFeature([Tenant])],
providers: [TenantsService],
controllers: [TenantsController],
exports: [TenantsService],
})
export class TenantsModule { }

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tenant } from './tenant.entity';
@Injectable()
export class TenantsService {
constructor(
@InjectRepository(Tenant)
private tenantsRepository: Repository<Tenant>,
) { }
async create(name: string): Promise<Tenant> {
const tenant = this.tenantsRepository.create({ name });
return this.tenantsRepository.save(tenant);
}
async findAll(): Promise<Tenant[]> {
return this.tenantsRepository.find();
}
async findOne(id: string): Promise<Tenant | null> {
return this.tenantsRepository.findOneBy({ id });
}
}