Attachments Domain
Attachments Domain (attachments_v1)
Section titled “Attachments Domain (attachments_v1)”The Attachments domain (attachments_v1) is a bounded context that handles comprehensive file and document management within the CO2 Target Asset Management system. It provides robust support for file uploads, folder hierarchies, metadata extraction, preview generation, and relationships between files and domain entities.
Overview
Section titled “Overview”The domain models two primary bounded concepts:
- Attachments (Files) - Individual files with metadata, versioning, and attachment relationships
- Folders - Hierarchical folder structures for organizing files
Key capabilities include:
- File storage with content-addressed blob references
- Multi-target attachment relationships (files can be attached to multiple entities)
- File versioning with history (up to 50 versions per file)
- Automatic metadata extraction (image dimensions, PDF page counts, etc.)
- Preview/thumbnail generation and management
- Voice note transcription support (with OpenAI Whisper integration)
- Tile definition for map overlay attachments
- Folder hierarchy and child management
Core Concepts
Section titled “Core Concepts”Files and Attachments
Section titled “Files and Attachments”An Attachment represents a file stored in the system. Key properties:
- AttachmentId - SHA256 hash-based identifier
- AttachmentFileName - Original file name
- AttachmentDisplayName - User-facing display name (optional, defaults to fileName)
- Size - File size in bytes
- ContentType - MIME type (e.g.,
image/png,application/pdf) - BlobRef - Content-addressed reference to stored blob (path, URI, SHA256 hash, byte length)
- Status - Either
activeordeleted - ParentFolderId - Optional folder hierarchy placement
Files can be attached to multiple entities (assets, locations, etc.) through the attachment system.
Folders
Section titled “Folders”A Folder provides hierarchical organization:
- FolderId - Unique identifier for the folder
- AttachmentFolderName - Human-readable folder name
- ParentFolderId - Parent folder reference (null for root folders)
- Status - Either
activeordeleted - ChildFolderIds - List of subfolder identifiers
- ChildFileIds - List of file identifiers in this folder
- FolderMetadata - Typed metadata including move history
File Metadata
Section titled “File Metadata”The domain supports typed, structured metadata extraction for various file types:
- ImageMetadata - Image dimensions, color space, DPI, orientation
- PdfMetadata - Page count, embedded fonts, encryption status, page dimensions
- VideoMetadata - Duration, codec, resolution, frame rate, bitrate
- AudioMetadata - Duration, codec, sample rate, bit rate, transcription (for voice notes)
Metadata is automatically extracted during file creation or version updates.
Previews and Thumbnails
Section titled “Previews and Thumbnails”Files can have associated preview references for efficient display:
- PreviewRef - Reference to a generated preview image
- PreviewType - Size type (thumbnail, small, medium, large)
- Blob Storage Reference - Download URI and blob metadata
- Generation Metadata - Generator version, generation timestamp
Key Aggregates
Section titled “Key Aggregates”AttachmentAggregate
Section titled “AttachmentAggregate”Manages individual file state and lifecycle.
class AttachmentAggregate extends Aggregate<AttachmentAggregate> { final AttachmentId attachmentId; final AttachmentFileName fileName; final AttachmentDisplayName? displayName; final int size; final ContentType mimeType; final UserId uploadedBy; final DateTime uploadedAt; final DateTime updatedAt; final AttachmentStatus status;
// Content-addressed blob reference final BlobRef? blobRef;
// Folder hierarchy final FolderId? parentFolderId;
// Multi-entity attachment tracking final Map<String, FileAttachment> _attachments; final Set<AttachmentTarget> attachedEntities;
// File versioning final int version; final List<AttachmentVersion> versionHistory; // Max 50 versions
// Structured metadata final ImageMetadata? imageMetadata; final PdfMetadata? pdfMetadata; final VideoMetadata? videoMetadata; final AudioMetadata? audioMetadata;
// Previews/thumbnails final List<PreviewRef> previews;
// Optional tile configuration for map overlays final FeatureMapTileEdgeDimensions? tileDimensions;}Key Business Logic:
effectiveDisplayName- Returns displayName if set, otherwise fileNameisActive/isDeleted- Status checksactiveAttachments- Get attachments that haven’t been detachedhasActiveAttachments- Check if file is attached to any entitycanBeDeleted()- Business rule: only deletable if no active attachmentsisImage,isAudio,isVideo,isDocument- Type checksisVoiceNote- Check if audio file is a voice note with transcriptionisInRoot/belongsToFolder()- Hierarchy checksgetPreview(PreviewType)- Retrieve specific previewgetVersion(int)- Retrieve specific version
FolderAggregate
Section titled “FolderAggregate”Manages folder state and hierarchy.
class FolderAggregate extends Aggregate<FolderAggregate> { final FolderId folderId; final AttachmentFolderName folderName; final FolderId? parentFolderId; // null for root folders final UserId createdBy; final DateTime createdAt; final DateTime updatedAt; final AttachmentStatus status;
// Typed folder metadata (move history, etc.) final FolderMetadata folderMetadata;
// Child references final List<String> childFolderIds; final List<String> childFileIds;}Key Business Logic:
isActive/isDeleted- Status checksisRoot- Check if this is a root-level foldercanBeDeleted()- Business rule: only deletable if empty (no children)isEmpty- Check if folder has no childrentotalChildren- Count of all child folders and filescontainsChild(childId, childType)- Check for specific childpathIds- Get parent folder IDs (for path reconstruction)
FileAttachment (Value Object)
Section titled “FileAttachment (Value Object)”Represents the attachment relationship between a file and a target entity:
class FileAttachment { final String attachmentId; final FileId fileId; final AttachmentTarget target; // Asset, location, etc. final AttachmentRole attachmentRole; final UserId attachedBy; final DateTime attachedAt; final List<AttachmentTag> tags; final String status; // 'active' or 'detached' final UserId? detachedBy; final DateTime? detachedAt; final AttachmentReason? detachmentAttachmentReason;}Tracks which entities a file is attached to and the status of those relationships.
Domain Events
Section titled “Domain Events”The domain publishes comprehensive events for file and folder operations:
Attachment Events
Section titled “Attachment Events”AttachmentCreatedEvent
Section titled “AttachmentCreatedEvent”Published when a new attachment is created with embedded file bytes.
class AttachmentCreatedEvent implements Event { final AttachmentId attachmentId; final AttachmentFileName fileName; final ContentType contentType; final int fileSize; final List<int> fileBytes; // Embedded bytes final FolderId? parentFolderId; final UserId createdBy; final FileMetadataBase fileMetadata; final BlobRef? blobRef; final DateTime createdAt;}AttachmentCreatedEventV2
Section titled “AttachmentCreatedEventV2”Improved version using pre-uploaded blob references (no embedded bytes).
class AttachmentCreatedEventV2 implements Event { final AttachmentId attachmentId; final AttachmentFileName fileName; final ContentType contentType; final int fileSize; final BlobRef blobRef; // Required - no embedded bytes final FolderId? parentFolderId; final UserId createdBy; final FileMetadataBase fileMetadata; final DateTime createdAt;}AttachmentDeletedEvent
Section titled “AttachmentDeletedEvent”Published when an attachment is permanently deleted.
AttachmentMovedEvent
Section titled “AttachmentMovedEvent”Published when an attachment is moved to a different folder.
class AttachmentMovedEvent implements Event { final AttachmentId attachmentId; final FolderId? previousParentFolderId; final FolderId? newParentFolderId; // null = root level final UserId movedBy; final DateTime movedAt; final AttachmentReason? reason;}AttachmentVersionCreatedEvent
Section titled “AttachmentVersionCreatedEvent”Published when a new version of a file is uploaded.
class AttachmentVersionCreatedEvent implements Event { final AttachmentId attachmentId; final int versionNumber; final List<int> fileBytes; final int fileSize; final ContentType contentType; final UserId createdBy; final DateTime createdAt; final String? changeDescription; final FileMetadataBase fileMetadata; final BlobRef? blobRef;}AttachmentRenamedEvent
Section titled “AttachmentRenamedEvent”Published when an attachment’s display name is changed.
class AttachmentRenamedEvent implements Event { final AttachmentId attachmentId; final AttachmentDisplayName? previousDisplayName; final AttachmentDisplayName newDisplayName; final UserId renamedBy; final DateTime renamedAt;}AttachmentAttachedEvent
Section titled “AttachmentAttachedEvent”Published when a file is attached to an entity (asset, location, etc.).
class AttachmentAttachedEvent implements Event { final AttachmentId attachmentId; final String targetKind; // 'asset', 'location', etc. final String targetId; final UserId attachedBy; final AttachmentRole role; // 'document', 'evidence', etc. final DateTime attachedAt; final AttachmentRelationMetadata relationMetadata;}AttachmentDetachedEvent
Section titled “AttachmentDetachedEvent”Published when a file is detached from an entity.
class AttachmentDetachedEvent implements Event { final AttachmentId attachmentId; final String targetKind; final String targetId; final UserId detachedBy; final DateTime detachedAt; final AttachmentRelationMetadata relationMetadata;}AttachmentPreviewsUpdatedEvent
Section titled “AttachmentPreviewsUpdatedEvent”Published when previews and metadata are updated for an attachment.
class AttachmentPreviewsUpdatedEvent implements Event { final AttachmentId attachmentId; final List<PreviewRef> previews; final PdfMetadata? pdfMetadata; final UserId updatedBy; final DateTime updatedAt;}VoiceNoteTranscriptionUpdatedEvent
Section titled “VoiceNoteTranscriptionUpdatedEvent”Published when a voice note’s transcription is updated (from server-side processing).
class VoiceNoteTranscriptionUpdatedEvent implements Event { final AttachmentId attachmentId; final String transcription; final UserId updatedBy; final DateTime updatedAt; final double? confidence; // 0.0 to 1.0 final String? languageCode; // e.g., 'en-US'}AttachmentTileDefinitionSetEvent
Section titled “AttachmentTileDefinitionSetEvent”Published when map tile configuration is set (dimensions, bearing, clipping).
class AttachmentTileDefinitionSetEvent implements Event { final AttachmentId attachmentId; final FeatureMapTileEdgeDimensions? dimensions; final double? bearingDeg; final GeoJSONPolygon? tileClip; final List<GeoJSONPolygon>? tileCutouts; final UserId setBy; final DateTime setAt;}Folder Events
Section titled “Folder Events”FolderCreatedEvent
Section titled “FolderCreatedEvent”Published when a new folder is created.
class FolderCreatedEvent implements Event { final FolderId folderId; final AttachmentFolderName folderName; final FolderId? parentFolderId; final UserId createdBy; final DateTime createdAt; final FolderMetadata folderMetadata;}FolderDeletedEvent
Section titled “FolderDeletedEvent”Published when a folder is permanently deleted.
FolderMovedEvent
Section titled “FolderMovedEvent”Published when a folder is moved to a different parent.
FolderChildAddedEvent
Section titled “FolderChildAddedEvent”Published when a file or subfolder is added to a folder.
class FolderChildAddedEvent implements Event { final FolderId folderId; final String childId; final ChildType childType; // 'file' or 'folder' final UserId addedBy; final DateTime addedAt;}FolderChildRemovedEvent
Section titled “FolderChildRemovedEvent”Published when a file or subfolder is removed from a folder.
Directives (Command Handlers)
Section titled “Directives (Command Handlers)”The domain implements directives that validate and execute commands:
Attachment Directives
Section titled “Attachment Directives”CreateAttachmentDirective
Section titled “CreateAttachmentDirective”Creates a new attachment from file bytes. Two versions exist:
- CreateAttachmentDirective - Uses embedded bytes (original)
- CreateAttachmentDirectiveV2 - Uses pre-uploaded BlobRef (recommended)
class CreateAttachmentPayloadV2 extends Payload { final String attachmentId; final AttachmentFileName fileName; final ContentType contentType; final int fileSize; final BlobRef blobRef; // Pre-uploaded blob reference final FolderId? parentFolderId; final UserId createdBy; final FileMetadataBase fileMetadata;}UpdateAttachmentPreviewsDirective
Section titled “UpdateAttachmentPreviewsDirective”Updates previews and metadata for an attachment.
DeleteAttachmentDirective
Section titled “DeleteAttachmentDirective”Permanently deletes an attachment.
MoveAttachmentDirective
Section titled “MoveAttachmentDirective”Moves an attachment to a different folder.
AttachAttachmentDirective
Section titled “AttachAttachmentDirective”Attaches a file to an entity (asset, location, etc.).
DetachAttachmentDirective
Section titled “DetachAttachmentDirective”Detaches a file from an entity.
Folder Directives
Section titled “Folder Directives”CreateFolderDirective
Section titled “CreateFolderDirective”Creates a new folder.
DeleteFolderDirective
Section titled “DeleteFolderDirective”Permanently deletes a folder.
MoveFolderDirective
Section titled “MoveFolderDirective”Moves a folder to a different parent.
AddChildToFolderDirective
Section titled “AddChildToFolderDirective”Adds a file or subfolder to a folder.
RemoveChildFromFolderDirective
Section titled “RemoveChildFromFolderDirective”Removes a file or subfolder from a folder.
Voice Note Directives
Section titled “Voice Note Directives”TranscribeVoiceNoteDirective
Section titled “TranscribeVoiceNoteDirective”Triggers server-side transcription of a voice note (OpenAI Whisper).
UpdateVoiceNoteTranscriptionDirective
Section titled “UpdateVoiceNoteTranscriptionDirective”Updates the transcription text (after server processing completes).
Tile Definition Directive
Section titled “Tile Definition Directive”SetAttachmentTileDefinitionDirective
Section titled “SetAttachmentTileDefinitionDirective”Configures map tile parameters (dimensions, bearing, clipping, cutouts).
Blob Storage Integration
Section titled “Blob Storage Integration”The domain uses content-addressed blob storage via BlobRef:
class BlobRef { final String path; // Storage path like 'blobs/sha256/abc123...' final Uri uri; // Download URI final String sha256Hex; // Content hash final int byteLength; // File size final String mediaType; // MIME type}Key Design Principles:
- Content is addressed by SHA256 hash, enabling deduplication
- Blob references are immutable and deterministic
- Files are never updated in place; new versions create new blobs
- Legacy
storagePathfields are no longer used; BlobRef is required for active files
Metadata Extraction
Section titled “Metadata Extraction”The domain provides pluggable metadata extractors:
abstract class MetadataExtractor { Future<FileMetadataBase?> extract(List<int> bytes, ContentType contentType); bool supports(ContentType contentType); int get priority => 0; // Higher priority runs first}Built-in Extractors:
- ImageMetadataExtractor - Extracts image dimensions, color space, orientation
- PdfMetadataExtractor - Extracts page count, encryption info, page sizes
The MetadataExtractionService coordinates extraction across registered extractors.
Preview Generation
Section titled “Preview Generation”The domain supports configurable preview generation:
abstract class PreviewGenerator { Future<List<PreviewRef>> generatePreviews( List<int> bytes, ContentType contentType, AttachmentId attachmentId, ); bool supports(ContentType contentType);}Built-in Generators:
- ImagePreviewGenerator - Creates resized thumbnails (TODO: implement actual resizing)
- PdfPreviewGenerator - Placeholder for PDF page thumbnails (domain-pure; infra handles PDF rendering)
Preview generation is coordinated by PreviewGenerationService.
Voice Note Transcription
Section titled “Voice Note Transcription”Voice note support includes:
- Automatic Detection - Audio files marked as voice notes via
AudioMetadata.isVoiceNote - Transcription Service - Interface for speech-to-text integration
- Confidence Tracking - Optional confidence scores from transcription services
- Language Detection - Optional language codes (e.g., ‘en-US’, ‘de-DE’)
The VoiceNoteTranscriptionUpdatedEvent carries the transcription result after server processing.
Usage Examples
Section titled “Usage Examples”Creating an Attachment
Section titled “Creating an Attachment”Use CreateAttachmentDirectiveV2 with a pre-uploaded blob:
final directive = CreateAttachmentDirectiveV2( payload: CreateAttachmentPayloadV2( attachmentId: AttachmentId.fromString('sha256hash'), fileName: AttachmentFileName.fromString('invoice.pdf'), contentType: ContentType('application/pdf'), fileSize: 2048000, blobRef: BlobRef( path: 'blobs/sha256/abc123...', uri: Uri.parse('https://storage.example.com/blobs/sha256/abc123...'), sha256Hex: 'abc123...', byteLength: 2048000, mediaType: 'application/pdf', ), parentFolderId: FolderId.fromString('folder-id'), createdBy: UserId.fromString('user-123'), fileMetadata: PdfMetadata(pageCount: 10), ),);Attaching a File to an Asset
Section titled “Attaching a File to an Asset”final directive = AttachAttachmentDirective( payload: AttachAttachmentPayload( attachmentId: AttachmentId.fromString('attachment-id'), targetKind: 'trackable_asset', targetId: 'asset-uuid', attachedBy: UserId.fromString('user-123'), role: AttachmentRole.evidence, relationMetadata: AttachmentRelationMetadata.empty(), ),);Creating a Folder Hierarchy
Section titled “Creating a Folder Hierarchy”// Create a root folderfinal rootDirective = CreateFolderDirective( payload: CreateFolderPayload( folderId: FolderId.fromString('root-folder-id').value, folderName: AttachmentFolderName.fromString('Project Documents'), parentFolderId: null, // Root level createdBy: UserId.fromString('user-123'), ),);
// Create a subfolderfinal subDirective = CreateFolderDirective( payload: CreateFolderPayload( folderId: FolderId.fromString('sub-folder-id').value, folderName: AttachmentFolderName.fromString('Reports'), parentFolderId: FolderId.fromString('root-folder-id'), createdBy: UserId.fromString('user-123'), ),);Adding Files to a Folder
Section titled “Adding Files to a Folder”final directive = AddChildToFolderDirective( payload: AddChildToFolderPayload( folderId: FolderId.fromString('folder-id').value, childId: 'attachment-id', childType: ChildType.file, addedBy: UserId.fromString('user-123'), ),);Dependencies
Section titled “Dependencies”The domain depends on:
- nomos_core - Core domain-driven design framework
- nomos_geo - Geographic types (for tile definitions with GeoJSON)
- contracts_v1 - Shared domain contracts (value objects, enums)
Backward Compatibility
Section titled “Backward Compatibility”The domain maintains backward compatibility with legacy data:
- Legacy Metadata Map - The untyped
metadatafield is retained for backwards compatibility but deprecated. New code should use typed fields (imageMetadata,pdfMetadata, etc.) - V1 vs V2 Events - Both
AttachmentCreatedEvent(with embedded bytes) andAttachmentCreatedEventV2(with BlobRef) are supported for deserialization - Robust Deserialization - The aggregate’s
fromJsonfactory includes defensive parsing to handle malformed or legacy documents
File Organization
Section titled “File Organization”dart_packages/co2/domains/attachments_v1/├── lib/│ ├── attachments_v1.dart # Library entry point│ └── src/│ ├── aggregates/│ │ ├── attachment_aggregate.dart # Attachment aggregate│ │ └── folder_aggregate.dart # Folder aggregate│ ├── events/│ │ └── attachment_events.dart # All domain events│ ├── directives/│ │ └── attachment_directives.dart # All directives│ └── services/│ ├── metadata_extractor.dart # Metadata extraction│ ├── image_metadata_extractor.dart # Image-specific│ ├── pdf_metadata_extractor.dart # PDF-specific│ ├── preview_generator.dart # Preview generation│ └── transcription_service.dart # Voice note transcription└── test/ └── (test files)Registration
Section titled “Registration”Initialize the domain by calling registerAttachmentsV1() or creating an AttachmentsV1 module instance:
// Register all types for serializationregisterAttachmentsV1();
// Or use module approachvoid registerDomainTypes() { AttachmentsV1().registerDomainTypes();}This registers:
- Aggregates:
AttachmentAggregate,FolderAggregate - All domain events
- All directives and payloads