import { COMPANY_VISIBILITY, HIERARCHY_GRANT_ROLES } from '@mosaicstack/db'; import { IsIn, IsOptional, IsString, IsUUID, Matches, MaxLength, MinLength } from 'class-validator'; /** * Hierarchy command DTOs (contract 1 §5, contract 2 §4/§7). * * The global ValidationPipe runs with whitelist + forbidNonWhitelisted, so a * payload field absent from these classes is a 400. That closure is itself * contract surface: * - CreateCompanyDto declares NO visibility field — creation is always * private (contract 1 §5.5); a visibility argument is refused by the pipe. * - CreateGrantDto declares NO teamId field — team grant subjects are * suspended (contract 2 §1.4/§7.5); a team subject is refused by the pipe. * Every class here must be registered in PIPE_GUARDED_DTOS so the boot-time * assertion proves the pipe sees the decorators. */ const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; const SLUG_MESSAGE = 'slug must be lowercase alphanumeric with interior hyphens'; export class CreateCompanyDto { @IsString() @MinLength(1) @MaxLength(255) name!: string; @IsString() @MaxLength(100) @Matches(SLUG_PATTERN, { message: SLUG_MESSAGE }) slug!: string; /** Client-supplied idempotency key (REQ-AUD-001 replay); server-generated when absent. */ @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class RenameNodeDto { @IsString() @MinLength(1) @MaxLength(255) name!: string; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class ChangeCompanyVisibilityDto { @IsIn(COMPANY_VISIBILITY) visibility!: (typeof COMPANY_VISIBILITY)[number]; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class DeleteNodeDto { @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class CreateEstateDto { @IsUUID() companyId!: string; @IsString() @MinLength(1) @MaxLength(255) name!: string; @IsString() @MaxLength(100) @Matches(SLUG_PATTERN, { message: SLUG_MESSAGE }) slug!: string; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class CreatePlatformProjectDto { @IsUUID() estateId!: string; @IsString() @MinLength(1) @MaxLength(255) name!: string; @IsString() @MaxLength(100) @Matches(SLUG_PATTERN, { message: SLUG_MESSAGE }) slug!: string; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class TransferEstateDto { @IsUUID() destinationCompanyId!: string; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class TransferPlatformProjectDto { @IsUUID() destinationEstateId!: string; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class CreateGrantDto { /** Subject user (better-auth text id). No teamId field — see module doc. */ @IsString() @MinLength(1) @MaxLength(255) userId!: string; @IsIn(['company', 'estate', 'platform_project']) targetKind!: 'company' | 'estate' | 'platform_project'; @IsUUID() targetId!: string; /** Bare vocabulary on requests; responses and audit events are namespaced (§4.5). */ @IsIn(HIERARCHY_GRANT_ROLES) role!: (typeof HIERARCHY_GRANT_ROLES)[number]; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; } export class ChangeGrantDto { @IsIn(HIERARCHY_GRANT_ROLES) role!: (typeof HIERARCHY_GRANT_ROLES)[number]; @IsOptional() @IsString() @MinLength(1) @MaxLength(255) idempotencyKey?: string; }