import { Body, Controller, Delete, Get, HttpCode, Param, ParseUUIDPipe, Post, UseGuards, } from '@nestjs/common'; import { AuthGuard } from '../auth/auth.guard.js'; import { CurrentUser } from '../auth/current-user.decorator.js'; import { ChangeCompanyVisibilityDto, ChangeGrantDto, CreateCompanyDto, CreateEstateDto, CreateGrantDto, CreatePlatformProjectDto, DeleteNodeDto, RenameNodeDto, TransferEstateDto, TransferPlatformProjectDto, } from './hierarchy.dto.js'; import { HierarchyRepository } from './hierarchy.repository.js'; import { HierarchyService } from './hierarchy.service.js'; /** * The hierarchy command family (contract 1 §5, §6.3). This controller is the * closed HTTP surface over the hierarchy class tables: the route-inventory * witness asserts these routes and no others exist. Delete commands take an * optional body (idempotency key) via POST-style DTOs; every mutation is * audited on its own transaction by the repository. */ @Controller('api/hierarchy') @UseGuards(AuthGuard) export class HierarchyController { constructor( private readonly repository: HierarchyRepository, private readonly service: HierarchyService, ) {} // ── companies ──────────────────────────────────────────────────────────── @Post('companies') async createCompany(@CurrentUser() user: { id: string }, @Body() dto: CreateCompanyDto) { return this.service.unwrap( await this.repository.createCompany({ actorId: user.id, name: dto.name, slug: dto.slug, idempotencyKey: dto.idempotencyKey, }), ); } /** Companies the caller holds a grant on (directly or via a descendant). */ @Get('companies') listGrantedCompanies(@CurrentUser() user: { id: string }) { return this.repository.listGrantedCompanies(user.id); } /** Directory-class companies, closed-field (§2.8). */ @Get('companies/directory') listDirectory() { return this.repository.listDirectory(); } @Post('companies/:id/rename') @HttpCode(200) async renameCompany( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: RenameNodeDto, ) { return this.service.unwrap( await this.repository.renameCompany({ actorId: user.id, companyId: id, name: dto.name, idempotencyKey: dto.idempotencyKey, }), ); } @Post('companies/:id/visibility') @HttpCode(200) async changeCompanyVisibility( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: ChangeCompanyVisibilityDto, ) { return this.service.unwrap( await this.repository.changeCompanyVisibility({ actorId: user.id, companyId: id, visibility: dto.visibility, idempotencyKey: dto.idempotencyKey, }), ); } @Delete('companies/:id') async deleteCompany( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: DeleteNodeDto, ) { return this.service.unwrap( await this.repository.deleteCompany({ actorId: user.id, companyId: id, idempotencyKey: dto?.idempotencyKey, }), ); } // ── estates ────────────────────────────────────────────────────────────── @Post('estates') async createEstate(@CurrentUser() user: { id: string }, @Body() dto: CreateEstateDto) { return this.service.unwrap( await this.repository.createEstate({ actorId: user.id, companyId: dto.companyId, name: dto.name, slug: dto.slug, idempotencyKey: dto.idempotencyKey, }), ); } @Post('estates/:id/rename') @HttpCode(200) async renameEstate( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: RenameNodeDto, ) { return this.service.unwrap( await this.repository.renameEstate({ actorId: user.id, estateId: id, name: dto.name, idempotencyKey: dto.idempotencyKey, }), ); } @Post('estates/:id/transfer') @HttpCode(200) async transferEstate( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: TransferEstateDto, ) { return this.service.unwrap( await this.repository.transferEstate({ actorId: user.id, estateId: id, destinationCompanyId: dto.destinationCompanyId, idempotencyKey: dto.idempotencyKey, }), ); } @Delete('estates/:id') async deleteEstate( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: DeleteNodeDto, ) { return this.service.unwrap( await this.repository.deleteEstate({ actorId: user.id, estateId: id, idempotencyKey: dto?.idempotencyKey, }), ); } // ── platform projects ──────────────────────────────────────────────────── @Post('platform-projects') async createPlatformProject( @CurrentUser() user: { id: string }, @Body() dto: CreatePlatformProjectDto, ) { return this.service.unwrap( await this.repository.createPlatformProject({ actorId: user.id, estateId: dto.estateId, name: dto.name, slug: dto.slug, idempotencyKey: dto.idempotencyKey, }), ); } @Post('platform-projects/:id/rename') @HttpCode(200) async renamePlatformProject( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: RenameNodeDto, ) { return this.service.unwrap( await this.repository.renamePlatformProject({ actorId: user.id, platformProjectId: id, name: dto.name, idempotencyKey: dto.idempotencyKey, }), ); } @Post('platform-projects/:id/transfer') @HttpCode(200) async transferPlatformProject( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: TransferPlatformProjectDto, ) { return this.service.unwrap( await this.repository.transferPlatformProject({ actorId: user.id, platformProjectId: id, destinationEstateId: dto.destinationEstateId, idempotencyKey: dto.idempotencyKey, }), ); } @Delete('platform-projects/:id') async deletePlatformProject( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: DeleteNodeDto, ) { return this.service.unwrap( await this.repository.deletePlatformProject({ actorId: user.id, platformProjectId: id, idempotencyKey: dto?.idempotencyKey, }), ); } // ── grants ─────────────────────────────────────────────────────────────── @Post('grants') async createGrant(@CurrentUser() user: { id: string }, @Body() dto: CreateGrantDto) { return this.service.unwrap( await this.repository.createGrant({ actorId: user.id, userId: dto.userId, targetKind: dto.targetKind, targetId: dto.targetId, role: dto.role, idempotencyKey: dto.idempotencyKey, }), ); } @Post('grants/:id/change') @HttpCode(200) async changeGrant( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: ChangeGrantDto, ) { return this.service.unwrap( await this.repository.changeGrant({ actorId: user.id, grantId: id, role: dto.role, idempotencyKey: dto.idempotencyKey, }), ); } @Delete('grants/:id') async revokeGrant( @CurrentUser() user: { id: string }, @Param('id', ParseUUIDPipe) id: string, @Body() dto: DeleteNodeDto, ) { return this.service.unwrap( await this.repository.revokeGrant({ actorId: user.id, grantId: id, idempotencyKey: dto?.idempotencyKey, }), ); } }