Skip to content

Attachments Domain

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.

The domain models two primary bounded concepts:

  1. Attachments (Files) - Individual files with metadata, versioning, and attachment relationships
  2. 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

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 active or deleted
  • ParentFolderId - Optional folder hierarchy placement

Files can be attached to multiple entities (assets, locations, etc.) through the attachment system.

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 active or deleted
  • ChildFolderIds - List of subfolder identifiers
  • ChildFileIds - List of file identifiers in this folder
  • FolderMetadata - Typed metadata including move history

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.

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

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 fileName
  • isActive / isDeleted - Status checks
  • activeAttachments - Get attachments that haven’t been detached
  • hasActiveAttachments - Check if file is attached to any entity
  • canBeDeleted() - Business rule: only deletable if no active attachments
  • isImage, isAudio, isVideo, isDocument - Type checks
  • isVoiceNote - Check if audio file is a voice note with transcription
  • isInRoot / belongsToFolder() - Hierarchy checks
  • getPreview(PreviewType) - Retrieve specific preview
  • getVersion(int) - Retrieve specific version

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 checks
  • isRoot - Check if this is a root-level folder
  • canBeDeleted() - Business rule: only deletable if empty (no children)
  • isEmpty - Check if folder has no children
  • totalChildren - Count of all child folders and files
  • containsChild(childId, childType) - Check for specific child
  • pathIds - Get parent folder IDs (for path reconstruction)

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.

The domain publishes comprehensive events for file and folder operations:

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;
}

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;
}

Published when an attachment is permanently deleted.

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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'
}

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;
}

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;
}

Published when a folder is permanently deleted.

Published when a folder is moved to a different parent.

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;
}

Published when a file or subfolder is removed from a folder.

The domain implements directives that validate and execute commands:

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;
}

Updates previews and metadata for an attachment.

Permanently deletes an attachment.

Moves an attachment to a different folder.

Attaches a file to an entity (asset, location, etc.).

Detaches a file from an entity.

Creates a new folder.

Permanently deletes a folder.

Moves a folder to a different parent.

Adds a file or subfolder to a folder.

Removes a file or subfolder from a folder.

Triggers server-side transcription of a voice note (OpenAI Whisper).

Updates the transcription text (after server processing completes).

Configures map tile parameters (dimensions, bearing, clipping, cutouts).

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 storagePath fields are no longer used; BlobRef is required for active files

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:

  1. ImageMetadataExtractor - Extracts image dimensions, color space, orientation
  2. PdfMetadataExtractor - Extracts page count, encryption info, page sizes

The MetadataExtractionService coordinates extraction across registered extractors.

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:

  1. ImagePreviewGenerator - Creates resized thumbnails (TODO: implement actual resizing)
  2. PdfPreviewGenerator - Placeholder for PDF page thumbnails (domain-pure; infra handles PDF rendering)

Preview generation is coordinated by PreviewGenerationService.

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.

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),
),
);
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(),
),
);
// Create a root folder
final 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 subfolder
final 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'),
),
);
final directive = AddChildToFolderDirective(
payload: AddChildToFolderPayload(
folderId: FolderId.fromString('folder-id').value,
childId: 'attachment-id',
childType: ChildType.file,
addedBy: UserId.fromString('user-123'),
),
);

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)

The domain maintains backward compatibility with legacy data:

  • Legacy Metadata Map - The untyped metadata field 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) and AttachmentCreatedEventV2 (with BlobRef) are supported for deserialization
  • Robust Deserialization - The aggregate’s fromJson factory includes defensive parsing to handle malformed or legacy documents
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)

Initialize the domain by calling registerAttachmentsV1() or creating an AttachmentsV1 module instance:

// Register all types for serialization
registerAttachmentsV1();
// Or use module approach
void registerDomainTypes() {
AttachmentsV1().registerDomainTypes();
}

This registers:

  • Aggregates: AttachmentAggregate, FolderAggregate
  • All domain events
  • All directives and payloads