298 lines
8.2 KiB
TypeScript
298 lines
8.2 KiB
TypeScript
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,
|
|
}),
|
|
);
|
|
}
|
|
}
|