Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
35 KiB
35 KiB
Backend Specification: Documents Module (DMS)
Version: 1.0.0 Fecha: 2025-12-05 Modulos: MAE-016 (Gestion Documental)
Resumen
| Metrica | Valor |
|---|---|
| Controllers | 5 |
| Services | 6 |
| Entities | 10 |
| Endpoints | 40+ |
| Tests Requeridos | 45+ |
Estructura del Modulo
modules/documents/
├── documents.module.ts
├── controllers/
│ ├── document.controller.ts
│ ├── folder.controller.ts
│ ├── plan.controller.ts
│ ├── approval.controller.ts
│ └── search.controller.ts
├── services/
│ ├── document.service.ts
│ ├── folder.service.ts
│ ├── plan.service.ts
│ ├── approval.service.ts
│ ├── version.service.ts
│ └── storage.service.ts
├── entities/
│ ├── document.entity.ts
│ ├── document-version.entity.ts
│ ├── folder.entity.ts
│ ├── folder-permission.entity.ts
│ ├── construction-plan.entity.ts
│ ├── plan-revision.entity.ts
│ ├── document-approval.entity.ts
│ ├── approval-step.entity.ts
│ ├── document-tag.entity.ts
│ └── document-link.entity.ts
├── dto/
├── events/
└── jobs/
└── cleanup.job.ts
Controllers
1. DocumentController
@Controller('api/v1/documents')
@UseGuards(AuthGuard, TenantGuard)
@ApiTags('documents')
export class DocumentController {
// GET /api/v1/documents
@Get()
@ApiOperation({ summary: 'List documents' })
async findAll(
@Query() query: DocumentQueryDto,
@CurrentTenant() tenantId: UUID
): Promise<PaginatedResponse<DocumentDto>>;
// GET /api/v1/documents/:id
@Get(':id')
async findOne(
@Param('id') id: UUID,
@CurrentTenant() tenantId: UUID
): Promise<DocumentDetailDto>;
// POST /api/v1/documents
@Post()
@UseInterceptors(FileInterceptor('file'))
async create(
@UploadedFile() file: Express.Multer.File,
@Body() dto: CreateDocumentDto,
@CurrentTenant() tenantId: UUID,
@CurrentUser() userId: UUID
): Promise<DocumentDto>;
// PUT /api/v1/documents/:id
@Put(':id')
async update(
@Param('id') id: UUID,
@Body() dto: UpdateDocumentDto
): Promise<DocumentDto>;
// DELETE /api/v1/documents/:id
@Delete(':id')
async remove(@Param('id') id: UUID): Promise<void>;
// GET /api/v1/documents/:id/download
@Get(':id/download')
async download(@Param('id') id: UUID): Promise<StreamableFile>;
// GET /api/v1/documents/:id/preview
@Get(':id/preview')
async preview(@Param('id') id: UUID): Promise<PreviewDto>;
// POST /api/v1/documents/:id/versions
@Post(':id/versions')
@UseInterceptors(FileInterceptor('file'))
async uploadNewVersion(
@Param('id') id: UUID,
@UploadedFile() file: Express.Multer.File,
@Body() dto: UploadVersionDto,
@CurrentUser() userId: UUID
): Promise<DocumentVersionDto>;
// GET /api/v1/documents/:id/versions
@Get(':id/versions')
async getVersions(@Param('id') id: UUID): Promise<DocumentVersionDto[]>;
// GET /api/v1/documents/:id/versions/:versionId/download
@Get(':id/versions/:versionId/download')
async downloadVersion(
@Param('id') id: UUID,
@Param('versionId') versionId: UUID
): Promise<StreamableFile>;
// POST /api/v1/documents/:id/versions/:versionId/restore
@Post(':id/versions/:versionId/restore')
async restoreVersion(
@Param('id') id: UUID,
@Param('versionId') versionId: UUID,
@CurrentUser() userId: UUID
): Promise<DocumentDto>;
// POST /api/v1/documents/:id/copy
@Post(':id/copy')
async copyDocument(
@Param('id') id: UUID,
@Body() dto: CopyDocumentDto,
@CurrentUser() userId: UUID
): Promise<DocumentDto>;
// POST /api/v1/documents/:id/move
@Post(':id/move')
async moveDocument(
@Param('id') id: UUID,
@Body() dto: MoveDocumentDto,
@CurrentUser() userId: UUID
): Promise<DocumentDto>;
// POST /api/v1/documents/:id/tags
@Post(':id/tags')
async addTags(
@Param('id') id: UUID,
@Body() dto: AddTagsDto
): Promise<DocumentDto>;
// DELETE /api/v1/documents/:id/tags/:tagId
@Delete(':id/tags/:tagId')
async removeTag(
@Param('id') id: UUID,
@Param('tagId') tagId: UUID
): Promise<DocumentDto>;
// POST /api/v1/documents/:id/link
@Post(':id/link')
async linkToEntity(
@Param('id') id: UUID,
@Body() dto: LinkDocumentDto
): Promise<DocumentLinkDto>;
// DELETE /api/v1/documents/:id/link/:linkId
@Delete(':id/link/:linkId')
async unlinkFromEntity(
@Param('id') id: UUID,
@Param('linkId') linkId: UUID
): Promise<void>;
// POST /api/v1/documents/bulk-upload
@Post('bulk-upload')
@UseInterceptors(FilesInterceptor('files', 20))
async bulkUpload(
@UploadedFiles() files: Express.Multer.File[],
@Body() dto: BulkUploadDto,
@CurrentTenant() tenantId: UUID,
@CurrentUser() userId: UUID
): Promise<BulkUploadResultDto>;
}
2. FolderController
@Controller('api/v1/documents/folders')
@ApiTags('document-folders')
export class FolderController {
// GET /api/v1/documents/folders
@Get()
async findAll(@CurrentTenant() tenantId: UUID): Promise<FolderTreeDto[]>;
// GET /api/v1/documents/folders/:id
@Get(':id')
async findOne(@Param('id') id: UUID): Promise<FolderDetailDto>;
// POST /api/v1/documents/folders
@Post()
async create(
@Body() dto: CreateFolderDto,
@CurrentUser() userId: UUID
): Promise<FolderDto>;
// PUT /api/v1/documents/folders/:id
@Put(':id')
async update(
@Param('id') id: UUID,
@Body() dto: UpdateFolderDto
): Promise<FolderDto>;
// DELETE /api/v1/documents/folders/:id
@Delete(':id')
async remove(@Param('id') id: UUID): Promise<void>;
// GET /api/v1/documents/folders/:id/contents
@Get(':id/contents')
async getContents(
@Param('id') id: UUID,
@Query() query: FolderContentsQueryDto
): Promise<FolderContentsDto>;
// POST /api/v1/documents/folders/:id/permissions
@Post(':id/permissions')
async setPermissions(
@Param('id') id: UUID,
@Body() dto: SetPermissionsDto
): Promise<FolderPermissionDto[]>;
// GET /api/v1/documents/folders/:id/permissions
@Get(':id/permissions')
async getPermissions(@Param('id') id: UUID): Promise<FolderPermissionDto[]>;
// DELETE /api/v1/documents/folders/:id/permissions/:permissionId
@Delete(':id/permissions/:permissionId')
async removePermission(
@Param('id') id: UUID,
@Param('permissionId') permissionId: UUID
): Promise<void>;
// GET /api/v1/documents/folders/:id/path
@Get(':id/path')
async getPath(@Param('id') id: UUID): Promise<FolderPathDto[]>;
// POST /api/v1/documents/folders/:id/move
@Post(':id/move')
async moveFolder(
@Param('id') id: UUID,
@Body() dto: MoveFolderDto
): Promise<FolderDto>;
}
3. PlanController
@Controller('api/v1/documents/plans')
@ApiTags('construction-plans')
export class PlanController {
// GET /api/v1/documents/plans
@Get()
async findAll(@Query() query: PlanQueryDto): Promise<PaginatedResponse<PlanDto>>;
// GET /api/v1/documents/plans/:id
@Get(':id')
async findOne(@Param('id') id: UUID): Promise<PlanDetailDto>;
// POST /api/v1/documents/plans
@Post()
@UseInterceptors(FileInterceptor('file'))
async create(
@UploadedFile() file: Express.Multer.File,
@Body() dto: CreatePlanDto,
@CurrentUser() userId: UUID
): Promise<PlanDto>;
// PUT /api/v1/documents/plans/:id
@Put(':id')
async update(
@Param('id') id: UUID,
@Body() dto: UpdatePlanDto
): Promise<PlanDto>;
// DELETE /api/v1/documents/plans/:id
@Delete(':id')
async remove(@Param('id') id: UUID): Promise<void>;
// GET /api/v1/documents/plans/:id/download
@Get(':id/download')
async download(@Param('id') id: UUID): Promise<StreamableFile>;
// GET /api/v1/documents/plans/:id/revisions
@Get(':id/revisions')
async getRevisions(@Param('id') id: UUID): Promise<PlanRevisionDto[]>;
// POST /api/v1/documents/plans/:id/revisions
@Post(':id/revisions')
@UseInterceptors(FileInterceptor('file'))
async createRevision(
@Param('id') id: UUID,
@UploadedFile() file: Express.Multer.File,
@Body() dto: CreateRevisionDto,
@CurrentUser() userId: UUID
): Promise<PlanRevisionDto>;
// GET /api/v1/documents/plans/:id/revisions/:revisionId/download
@Get(':id/revisions/:revisionId/download')
async downloadRevision(
@Param('id') id: UUID,
@Param('revisionId') revisionId: UUID
): Promise<StreamableFile>;
// POST /api/v1/documents/plans/:id/revisions/:revisionId/approve
@Post(':id/revisions/:revisionId/approve')
async approveRevision(
@Param('id') id: UUID,
@Param('revisionId') revisionId: UUID,
@Body() dto: ApproveRevisionDto,
@CurrentUser() userId: UUID
): Promise<PlanRevisionDto>;
// POST /api/v1/documents/plans/:id/revisions/:revisionId/reject
@Post(':id/revisions/:revisionId/reject')
async rejectRevision(
@Param('id') id: UUID,
@Param('revisionId') revisionId: UUID,
@Body() dto: RejectRevisionDto,
@CurrentUser() userId: UUID
): Promise<PlanRevisionDto>;
// GET /api/v1/documents/plans/by-project/:projectId
@Get('by-project/:projectId')
async getByProject(
@Param('projectId') projectId: UUID,
@Query() query: PlanQueryDto
): Promise<PaginatedResponse<PlanDto>>;
// POST /api/v1/documents/plans/:id/compare
@Post(':id/compare')
async compareRevisions(
@Param('id') id: UUID,
@Body() dto: CompareRevisionsDto
): Promise<ComparisonResultDto>;
}
4. ApprovalController
@Controller('api/v1/documents/approvals')
@ApiTags('document-approvals')
export class ApprovalController {
// GET /api/v1/documents/approvals/pending
@Get('pending')
async getPendingApprovals(
@CurrentUser() userId: UUID
): Promise<PendingApprovalDto[]>;
// GET /api/v1/documents/approvals/:id
@Get(':id')
async findOne(@Param('id') id: UUID): Promise<ApprovalDetailDto>;
// POST /api/v1/documents/approvals
@Post()
async createApprovalRequest(
@Body() dto: CreateApprovalRequestDto,
@CurrentUser() userId: UUID
): Promise<ApprovalDto>;
// POST /api/v1/documents/approvals/:id/approve
@Post(':id/approve')
async approve(
@Param('id') id: UUID,
@Body() dto: ApproveDto,
@CurrentUser() userId: UUID
): Promise<ApprovalDto>;
// POST /api/v1/documents/approvals/:id/reject
@Post(':id/reject')
async reject(
@Param('id') id: UUID,
@Body() dto: RejectDto,
@CurrentUser() userId: UUID
): Promise<ApprovalDto>;
// POST /api/v1/documents/approvals/:id/request-changes
@Post(':id/request-changes')
async requestChanges(
@Param('id') id: UUID,
@Body() dto: RequestChangesDto,
@CurrentUser() userId: UUID
): Promise<ApprovalDto>;
// POST /api/v1/documents/approvals/:id/cancel
@Post(':id/cancel')
async cancel(
@Param('id') id: UUID,
@CurrentUser() userId: UUID
): Promise<ApprovalDto>;
// GET /api/v1/documents/approvals/:id/history
@Get(':id/history')
async getHistory(@Param('id') id: UUID): Promise<ApprovalHistoryDto[]>;
// GET /api/v1/documents/approvals/workflows
@Get('workflows')
async getWorkflows(@CurrentTenant() tenantId: UUID): Promise<ApprovalWorkflowDto[]>;
// POST /api/v1/documents/approvals/workflows
@Post('workflows')
async createWorkflow(
@Body() dto: CreateWorkflowDto,
@CurrentUser() userId: UUID
): Promise<ApprovalWorkflowDto>;
}
5. SearchController
@Controller('api/v1/documents/search')
@ApiTags('document-search')
export class SearchController {
// GET /api/v1/documents/search
@Get()
async search(@Query() query: SearchQueryDto): Promise<SearchResultDto>;
// GET /api/v1/documents/search/advanced
@Get('advanced')
async advancedSearch(@Query() query: AdvancedSearchQueryDto): Promise<SearchResultDto>;
// GET /api/v1/documents/search/suggestions
@Get('suggestions')
async getSuggestions(@Query('q') query: string): Promise<SuggestionDto[]>;
// GET /api/v1/documents/search/recent
@Get('recent')
async getRecent(@CurrentUser() userId: UUID): Promise<DocumentDto[]>;
// GET /api/v1/documents/search/by-entity
@Get('by-entity')
async searchByEntity(@Query() query: EntitySearchQueryDto): Promise<DocumentDto[]>;
// GET /api/v1/documents/search/tags
@Get('tags')
async searchTags(@Query('q') query: string): Promise<TagDto[]>;
// POST /api/v1/documents/search/index/:id
@Post('index/:id')
async reindexDocument(@Param('id') id: UUID): Promise<void>;
}
Services
DocumentService
@Injectable()
export class DocumentService {
constructor(
@InjectRepository(Document)
private readonly documentRepo: Repository<Document>,
@InjectRepository(DocumentVersion)
private readonly versionRepo: Repository<DocumentVersion>,
private readonly storageService: StorageService,
private readonly eventEmitter: EventEmitter2
) {}
async findAll(tenantId: UUID, query: DocumentQueryDto): Promise<PaginatedResponse<DocumentDto>>;
async findOne(tenantId: UUID, id: UUID): Promise<DocumentDetailDto>;
async create(tenantId: UUID, userId: UUID, file: Express.Multer.File, dto: CreateDocumentDto): Promise<DocumentDto>;
async update(id: UUID, dto: UpdateDocumentDto): Promise<DocumentDto>;
async remove(id: UUID): Promise<void>;
async getDownloadStream(id: UUID): Promise<{ stream: Readable; filename: string; mimeType: string }>;
async generatePreview(id: UUID): Promise<PreviewDto>;
async copyDocument(id: UUID, userId: UUID, dto: CopyDocumentDto): Promise<DocumentDto>;
async moveDocument(id: UUID, userId: UUID, dto: MoveDocumentDto): Promise<DocumentDto>;
async addTags(id: UUID, tagIds: string[]): Promise<DocumentDto>;
async removeTag(id: UUID, tagId: UUID): Promise<DocumentDto>;
async linkToEntity(id: UUID, dto: LinkDocumentDto): Promise<DocumentLinkDto>;
async unlinkFromEntity(id: UUID, linkId: UUID): Promise<void>;
async bulkUpload(tenantId: UUID, userId: UUID, files: Express.Multer.File[], dto: BulkUploadDto): Promise<BulkUploadResultDto>;
}
VersionService
@Injectable()
export class VersionService {
constructor(
@InjectRepository(DocumentVersion)
private readonly versionRepo: Repository<DocumentVersion>,
private readonly storageService: StorageService,
private readonly eventEmitter: EventEmitter2
) {}
async createVersion(documentId: UUID, userId: UUID, file: Express.Multer.File, dto: UploadVersionDto): Promise<DocumentVersionDto>;
async getVersions(documentId: UUID): Promise<DocumentVersionDto[]>;
async getVersion(versionId: UUID): Promise<DocumentVersionDto>;
async downloadVersion(versionId: UUID): Promise<{ stream: Readable; filename: string }>;
async restoreVersion(documentId: UUID, versionId: UUID, userId: UUID): Promise<DocumentDto>;
async deleteOldVersions(documentId: UUID, keepCount: number): Promise<number>;
async calculateVersionNumber(documentId: UUID): Promise<number>;
}
FolderService
@Injectable()
export class FolderService {
constructor(
@InjectRepository(Folder)
private readonly folderRepo: Repository<Folder>,
@InjectRepository(FolderPermission)
private readonly permissionRepo: Repository<FolderPermission>
) {}
async findAll(tenantId: UUID): Promise<FolderTreeDto[]>;
async findOne(id: UUID): Promise<FolderDetailDto>;
async create(tenantId: UUID, userId: UUID, dto: CreateFolderDto): Promise<FolderDto>;
async update(id: UUID, dto: UpdateFolderDto): Promise<FolderDto>;
async remove(id: UUID): Promise<void>;
async getContents(id: UUID, query: FolderContentsQueryDto): Promise<FolderContentsDto>;
async setPermissions(id: UUID, dto: SetPermissionsDto): Promise<FolderPermissionDto[]>;
async getPermissions(id: UUID): Promise<FolderPermissionDto[]>;
async removePermission(id: UUID, permissionId: UUID): Promise<void>;
async getPath(id: UUID): Promise<FolderPathDto[]>;
async moveFolder(id: UUID, dto: MoveFolderDto): Promise<FolderDto>;
async checkPermission(folderId: UUID, userId: UUID, permission: PermissionType): Promise<boolean>;
async getEffectivePermissions(folderId: UUID, userId: UUID): Promise<PermissionType[]>;
}
PlanService
@Injectable()
export class PlanService {
constructor(
@InjectRepository(ConstructionPlan)
private readonly planRepo: Repository<ConstructionPlan>,
@InjectRepository(PlanRevision)
private readonly revisionRepo: Repository<PlanRevision>,
private readonly storageService: StorageService,
private readonly eventEmitter: EventEmitter2
) {}
async findAll(tenantId: UUID, query: PlanQueryDto): Promise<PaginatedResponse<PlanDto>>;
async findOne(id: UUID): Promise<PlanDetailDto>;
async create(tenantId: UUID, userId: UUID, file: Express.Multer.File, dto: CreatePlanDto): Promise<PlanDto>;
async update(id: UUID, dto: UpdatePlanDto): Promise<PlanDto>;
async remove(id: UUID): Promise<void>;
async getDownloadStream(id: UUID): Promise<{ stream: Readable; filename: string }>;
async getRevisions(planId: UUID): Promise<PlanRevisionDto[]>;
async createRevision(planId: UUID, userId: UUID, file: Express.Multer.File, dto: CreateRevisionDto): Promise<PlanRevisionDto>;
async downloadRevision(revisionId: UUID): Promise<{ stream: Readable; filename: string }>;
async approveRevision(planId: UUID, revisionId: UUID, userId: UUID, dto: ApproveRevisionDto): Promise<PlanRevisionDto>;
async rejectRevision(planId: UUID, revisionId: UUID, userId: UUID, dto: RejectRevisionDto): Promise<PlanRevisionDto>;
async getByProject(projectId: UUID, query: PlanQueryDto): Promise<PaginatedResponse<PlanDto>>;
async compareRevisions(planId: UUID, revisionId1: UUID, revisionId2: UUID): Promise<ComparisonResultDto>;
async calculateNextRevisionCode(planId: UUID): Promise<string>;
}
ApprovalService
@Injectable()
export class ApprovalService {
constructor(
@InjectRepository(DocumentApproval)
private readonly approvalRepo: Repository<DocumentApproval>,
@InjectRepository(ApprovalStep)
private readonly stepRepo: Repository<ApprovalStep>,
private readonly notificationService: NotificationService,
private readonly eventEmitter: EventEmitter2
) {}
async getPendingApprovals(userId: UUID): Promise<PendingApprovalDto[]>;
async findOne(id: UUID): Promise<ApprovalDetailDto>;
async createApprovalRequest(userId: UUID, dto: CreateApprovalRequestDto): Promise<ApprovalDto>;
async approve(id: UUID, userId: UUID, dto: ApproveDto): Promise<ApprovalDto>;
async reject(id: UUID, userId: UUID, dto: RejectDto): Promise<ApprovalDto>;
async requestChanges(id: UUID, userId: UUID, dto: RequestChangesDto): Promise<ApprovalDto>;
async cancel(id: UUID, userId: UUID): Promise<ApprovalDto>;
async getHistory(id: UUID): Promise<ApprovalHistoryDto[]>;
async getWorkflows(tenantId: UUID): Promise<ApprovalWorkflowDto[]>;
async createWorkflow(tenantId: UUID, userId: UUID, dto: CreateWorkflowDto): Promise<ApprovalWorkflowDto>;
async advanceWorkflow(approvalId: UUID): Promise<void>;
async notifyNextApprover(approvalId: UUID): Promise<void>;
}
StorageService
@Injectable()
export class StorageService {
constructor(
private readonly configService: ConfigService
) {}
async upload(file: Express.Multer.File, path: string): Promise<StorageResult>;
async download(storagePath: string): Promise<Readable>;
async delete(storagePath: string): Promise<void>;
async copy(sourcePath: string, destinationPath: string): Promise<StorageResult>;
async move(sourcePath: string, destinationPath: string): Promise<StorageResult>;
async getMetadata(storagePath: string): Promise<StorageMetadata>;
async generatePresignedUrl(storagePath: string, expiresIn: number): Promise<string>;
async getStorageUsage(tenantId: UUID): Promise<StorageUsageDto>;
// Storage backend abstraction
private getStorageBackend(): StorageBackend {
const backend = this.configService.get('STORAGE_BACKEND');
switch (backend) {
case 's3': return new S3StorageBackend(this.configService);
case 'azure': return new AzureBlobStorageBackend(this.configService);
case 'local': return new LocalStorageBackend(this.configService);
default: return new LocalStorageBackend(this.configService);
}
}
}
DTOs
Document DTOs
export class CreateDocumentDto {
@IsString() @MaxLength(300)
name: string;
@IsOptional() @IsString()
description?: string;
@IsOptional() @IsUUID()
folderId?: string;
@IsEnum(DocumentType)
documentType: DocumentType;
@IsOptional() @IsArray() @IsUUID('4', { each: true })
tagIds?: string[];
@IsOptional() @IsObject()
metadata?: Record<string, any>;
@IsOptional() @IsUUID()
projectId?: string;
}
export class DocumentQueryDto extends PaginationDto {
@IsOptional() @IsUUID()
folderId?: string;
@IsOptional() @IsEnum(DocumentType)
documentType?: DocumentType;
@IsOptional() @IsString()
search?: string;
@IsOptional() @IsArray() @IsUUID('4', { each: true })
tagIds?: string[];
@IsOptional() @IsUUID()
projectId?: string;
@IsOptional() @IsDateString()
fromDate?: string;
@IsOptional() @IsDateString()
toDate?: string;
@IsOptional() @IsString()
mimeType?: string;
}
export class UploadVersionDto {
@IsOptional() @IsString()
changeNotes?: string;
@IsOptional() @IsBoolean()
isMajorVersion?: boolean;
}
export class CopyDocumentDto {
@IsUUID()
targetFolderId: string;
@IsOptional() @IsString()
newName?: string;
}
export class MoveDocumentDto {
@IsUUID()
targetFolderId: string;
}
export class LinkDocumentDto {
@IsEnum(EntityType)
entityType: EntityType;
@IsUUID()
entityId: string;
@IsOptional() @IsString()
linkType?: string;
}
Folder DTOs
export class CreateFolderDto {
@IsString() @MaxLength(200)
name: string;
@IsOptional() @IsString()
description?: string;
@IsOptional() @IsUUID()
parentId?: string;
@IsOptional() @IsUUID()
projectId?: string;
}
export class FolderContentsQueryDto extends PaginationDto {
@IsOptional() @IsBoolean()
includeFolders?: boolean;
@IsOptional() @IsBoolean()
includeDocuments?: boolean;
@IsOptional() @IsString()
sortBy?: 'name' | 'createdAt' | 'updatedAt' | 'size';
@IsOptional() @IsString()
sortOrder?: 'asc' | 'desc';
}
export class SetPermissionsDto {
@IsArray()
@ValidateNested({ each: true })
permissions: {
principalType: 'user' | 'role';
principalId: string;
permissionLevel: PermissionLevel;
}[];
@IsOptional() @IsBoolean()
applyToChildren?: boolean;
}
Plan DTOs
export class CreatePlanDto {
@IsString() @MaxLength(30)
planNumber: string;
@IsString() @MaxLength(300)
name: string;
@IsOptional() @IsString()
description?: string;
@IsEnum(PlanType)
planType: PlanType;
@IsOptional() @IsEnum(Discipline)
discipline?: Discipline;
@IsUUID()
projectId: string;
@IsOptional() @IsString()
scale?: string;
@IsOptional() @IsDateString()
planDate?: string;
}
export class CreateRevisionDto {
@IsString() @MaxLength(10)
revisionCode: string;
@IsOptional() @IsString()
changeDescription?: string;
@IsOptional() @IsArray() @IsString({ each: true })
changesFromPrevious?: string[];
}
export class ApproveRevisionDto {
@IsOptional() @IsString()
comments?: string;
@IsOptional() @IsString()
digitalSignature?: string;
}
export class RejectRevisionDto {
@IsString()
reason: string;
@IsOptional() @IsArray() @IsString({ each: true })
requiredChanges?: string[];
}
export class CompareRevisionsDto {
@IsUUID()
revisionId1: string;
@IsUUID()
revisionId2: string;
}
Approval DTOs
export class CreateApprovalRequestDto {
@IsUUID()
documentId: string;
@IsOptional() @IsUUID()
workflowId?: string;
@IsOptional() @IsArray() @IsUUID('4', { each: true })
approverIds?: string[];
@IsOptional() @IsString()
notes?: string;
@IsOptional() @IsDateString()
dueDate?: string;
}
export class ApproveDto {
@IsOptional() @IsString()
comments?: string;
@IsOptional() @IsString()
digitalSignature?: string;
}
export class RejectDto {
@IsString()
reason: string;
}
export class RequestChangesDto {
@IsString()
requestedChanges: string;
@IsOptional() @IsDateString()
reviseDueDate?: string;
}
export class CreateWorkflowDto {
@IsString() @MaxLength(100)
name: string;
@IsOptional() @IsString()
description?: string;
@IsArray()
@ValidateNested({ each: true })
steps: {
stepOrder: number;
approverType: 'user' | 'role';
approverId: string;
isOptional?: boolean;
}[];
@IsOptional() @IsBoolean()
requireAllApprovers?: boolean;
}
Search DTOs
export class SearchQueryDto {
@IsString() @MinLength(2)
q: string;
@IsOptional() @IsNumber()
limit?: number;
@IsOptional() @IsNumber()
offset?: number;
@IsOptional() @IsEnum(DocumentType)
documentType?: DocumentType;
}
export class AdvancedSearchQueryDto extends PaginationDto {
@IsOptional() @IsString()
name?: string;
@IsOptional() @IsString()
content?: string;
@IsOptional() @IsArray() @IsUUID('4', { each: true })
folderIds?: string[];
@IsOptional() @IsArray() @IsUUID('4', { each: true })
tagIds?: string[];
@IsOptional() @IsEnum(DocumentType)
documentType?: DocumentType;
@IsOptional() @IsDateString()
createdAfter?: string;
@IsOptional() @IsDateString()
createdBefore?: string;
@IsOptional() @IsDateString()
modifiedAfter?: string;
@IsOptional() @IsDateString()
modifiedBefore?: string;
@IsOptional() @IsNumber()
minSize?: number;
@IsOptional() @IsNumber()
maxSize?: number;
@IsOptional() @IsString()
mimeType?: string;
@IsOptional() @IsUUID()
createdBy?: string;
}
export class EntitySearchQueryDto {
@IsEnum(EntityType)
entityType: EntityType;
@IsUUID()
entityId: string;
@IsOptional() @IsString()
linkType?: string;
}
Scheduled Jobs
CleanupJob
@Injectable()
export class CleanupJob {
constructor(
private readonly documentService: DocumentService,
private readonly versionService: VersionService,
private readonly storageService: StorageService,
@InjectRepository(Document)
private readonly documentRepo: Repository<Document>
) {}
@Cron('0 2 * * 0') // Weekly on Sunday at 2 AM
async cleanupDeletedDocuments() {
// Find documents deleted more than 30 days ago
const deletedDocs = await this.documentRepo.find({
where: {
deletedAt: LessThan(subDays(new Date(), 30))
}
});
for (const doc of deletedDocs) {
// Delete all versions from storage
const versions = await this.versionService.getVersions(doc.id);
for (const version of versions) {
await this.storageService.delete(version.storagePath);
}
// Hard delete document
await this.documentRepo.delete(doc.id);
}
}
@Cron('0 3 * * *') // Daily at 3 AM
async cleanupOrphanedFiles() {
// Find orphaned files in storage
const orphanedFiles = await this.storageService.findOrphanedFiles();
for (const file of orphanedFiles) {
await this.storageService.delete(file.path);
}
}
@Cron('0 4 1 * *') // Monthly on 1st at 4 AM
async archiveOldVersions() {
// Archive versions older than 1 year (keep only latest 5)
const documents = await this.documentRepo.find({
where: {
createdAt: LessThan(subYears(new Date(), 1))
}
});
for (const doc of documents) {
await this.versionService.deleteOldVersions(doc.id, 5);
}
}
}
Events
export class DocumentCreatedEvent {
constructor(
public readonly documentId: string,
public readonly tenantId: string,
public readonly name: string,
public readonly folderId: string | null,
public readonly uploadedBy: string,
public readonly fileSize: number,
public readonly mimeType: string,
public readonly timestamp: Date = new Date()
) {}
}
export class DocumentVersionCreatedEvent {
constructor(
public readonly documentId: string,
public readonly versionId: string,
public readonly versionNumber: number,
public readonly uploadedBy: string,
public readonly changeNotes: string | null,
public readonly timestamp: Date = new Date()
) {}
}
export class DocumentApprovedEvent {
constructor(
public readonly documentId: string,
public readonly approvalId: string,
public readonly approvedBy: string,
public readonly allApproversCompleted: boolean,
public readonly timestamp: Date = new Date()
) {}
}
export class DocumentRejectedEvent {
constructor(
public readonly documentId: string,
public readonly approvalId: string,
public readonly rejectedBy: string,
public readonly reason: string,
public readonly timestamp: Date = new Date()
) {}
}
export class PlanRevisionApprovedEvent {
constructor(
public readonly planId: string,
public readonly revisionId: string,
public readonly revisionCode: string,
public readonly approvedBy: string,
public readonly timestamp: Date = new Date()
) {}
}
export class FolderPermissionsChangedEvent {
constructor(
public readonly folderId: string,
public readonly changedBy: string,
public readonly permissionsAdded: number,
public readonly permissionsRemoved: number,
public readonly timestamp: Date = new Date()
) {}
}
Integraciones
Full-Text Search Integration
@Injectable()
export class SearchIndexService {
constructor(
private readonly configService: ConfigService,
private readonly httpService: HttpService
) {}
private readonly elasticsearchUrl = this.configService.get('ELASTICSEARCH_URL');
async indexDocument(document: Document, content: string): Promise<void> {
await this.httpService.put(
`${this.elasticsearchUrl}/documents/_doc/${document.id}`,
{
tenantId: document.tenantId,
name: document.name,
description: document.description,
content: content,
documentType: document.documentType,
folderId: document.folderId,
tags: document.tags.map(t => t.name),
mimeType: document.mimeType,
fileSize: document.fileSize,
createdAt: document.createdAt,
updatedAt: document.updatedAt,
createdBy: document.createdBy
}
).toPromise();
}
async removeFromIndex(documentId: string): Promise<void> {
await this.httpService.delete(
`${this.elasticsearchUrl}/documents/_doc/${documentId}`
).toPromise();
}
async search(tenantId: UUID, query: SearchQueryDto): Promise<SearchResultDto> {
const response = await this.httpService.post(
`${this.elasticsearchUrl}/documents/_search`,
{
query: {
bool: {
must: [
{ term: { tenantId } },
{
multi_match: {
query: query.q,
fields: ['name^3', 'description^2', 'content', 'tags']
}
}
]
}
},
from: query.offset || 0,
size: query.limit || 20,
highlight: {
fields: {
content: {},
name: {},
description: {}
}
}
}
).toPromise();
return {
hits: response.data.hits.hits.map(hit => ({
id: hit._id,
score: hit._score,
...hit._source,
highlights: hit.highlight
})),
total: response.data.hits.total.value
};
}
}
PDF/Image Processing Integration
@Injectable()
export class DocumentProcessingService {
constructor(
private readonly storageService: StorageService
) {}
async extractText(storagePath: string, mimeType: string): Promise<string> {
const stream = await this.storageService.download(storagePath);
switch (mimeType) {
case 'application/pdf':
return this.extractPdfText(stream);
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return this.extractDocxText(stream);
case 'image/png':
case 'image/jpeg':
return this.extractImageText(stream); // OCR
default:
return '';
}
}
async generateThumbnail(storagePath: string, mimeType: string): Promise<Buffer> {
const stream = await this.storageService.download(storagePath);
switch (mimeType) {
case 'application/pdf':
return this.generatePdfThumbnail(stream);
case 'image/png':
case 'image/jpeg':
return this.generateImageThumbnail(stream);
default:
return this.getDefaultThumbnail(mimeType);
}
}
async generatePreview(storagePath: string, mimeType: string): Promise<PreviewDto> {
// Generate preview based on file type
// Returns HTML or image URL for preview
}
}
Compliance Integration
@Injectable()
export class DocumentComplianceIntegration {
@OnEvent('document.approved')
async handleDocumentApproved(event: DocumentApprovedEvent): Promise<void> {
if (event.allApproversCompleted) {
// Check if document is linked to compliance item
const links = await this.documentLinkRepo.find({
where: {
documentId: event.documentId,
entityType: 'compliance_item'
}
});
for (const link of links) {
// Update compliance item status
await this.complianceService.updateItemStatus(link.entityId, 'compliant', {
evidenceDocumentId: event.documentId,
approvalDate: event.timestamp
});
}
}
}
@OnEvent('plan_revision.approved')
async handlePlanRevisionApproved(event: PlanRevisionApprovedEvent): Promise<void> {
// Update project with latest approved plan
const plan = await this.planRepo.findOne(event.planId);
if (plan.projectId) {
await this.projectService.updatePlanStatus(plan.projectId, {
planId: event.planId,
revisionCode: event.revisionCode,
approvedAt: event.timestamp
});
}
}
}
Referencias
Ultima actualizacion: 2025-12-05