286 lines
8.3 KiB
TypeScript
286 lines
8.3 KiB
TypeScript
/**
|
|
* Domain Value Object: MediaReference
|
|
*
|
|
* Discriminated union representing different types of media references:
|
|
* - system-default: Pre-defined default media (e.g., default avatar/logo)
|
|
* - generated: Media generated by AI/algorithm (references generation request)
|
|
* - uploaded: User-uploaded media (references media entity)
|
|
* - none: Explicit no-media placeholder
|
|
*
|
|
* Follows clean architecture and TDD principles.
|
|
*/
|
|
|
|
import type { ValueObject } from '@core/shared/domain/ValueObject';
|
|
|
|
// Variant types for system-default references
|
|
export type MediaVariant = 'avatar' | 'logo';
|
|
|
|
// Specific variants for avatars (deterministic selection)
|
|
export type AvatarVariant = 'male' | 'female' | 'neutral';
|
|
|
|
// Discriminated union types
|
|
export interface SystemDefaultRef {
|
|
type: 'system-default';
|
|
variant: MediaVariant;
|
|
avatarVariant?: AvatarVariant | undefined; // Only used when variant is 'avatar'
|
|
}
|
|
|
|
export interface GeneratedRef {
|
|
type: 'generated';
|
|
generationRequestId: string;
|
|
}
|
|
|
|
export interface UploadedRef {
|
|
type: 'uploaded';
|
|
mediaId: string;
|
|
}
|
|
|
|
export interface NoneRef {
|
|
type: 'none';
|
|
}
|
|
|
|
// Union of all reference types
|
|
export type MediaReferenceProps = SystemDefaultRef | GeneratedRef | UploadedRef | NoneRef;
|
|
|
|
// Type guards
|
|
function isSystemDefaultRef(props: unknown): props is SystemDefaultRef {
|
|
const typedProps = props as SystemDefaultRef;
|
|
if (typedProps?.type !== 'system-default') {
|
|
return false;
|
|
}
|
|
if (typedProps.variant !== 'avatar' && typedProps.variant !== 'logo') {
|
|
return false;
|
|
}
|
|
// avatarVariant is optional, but if present must be valid
|
|
if (typedProps.avatarVariant !== undefined) {
|
|
return typedProps.avatarVariant === 'male' || typedProps.avatarVariant === 'female' || typedProps.avatarVariant === 'neutral';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isGeneratedRef(props: unknown): props is GeneratedRef {
|
|
const typedProps = props as GeneratedRef;
|
|
return typedProps?.type === 'generated' &&
|
|
typeof typedProps.generationRequestId === 'string' &&
|
|
typedProps.generationRequestId.trim().length > 0;
|
|
}
|
|
|
|
function isUploadedRef(props: unknown): props is UploadedRef {
|
|
const typedProps = props as UploadedRef;
|
|
return typedProps?.type === 'uploaded' &&
|
|
typeof typedProps.mediaId === 'string' &&
|
|
typedProps.mediaId.trim().length > 0;
|
|
}
|
|
|
|
function isNoneRef(props: unknown): props is NoneRef {
|
|
const typedProps = props as NoneRef;
|
|
return typedProps?.type === 'none' &&
|
|
Object.keys(typedProps).length === 1; // Only 'type' property
|
|
}
|
|
|
|
export class MediaReference implements ValueObject<MediaReferenceProps> {
|
|
public readonly props: MediaReferenceProps;
|
|
|
|
private constructor(props: MediaReferenceProps) {
|
|
this.props = props;
|
|
}
|
|
|
|
/**
|
|
* Factory method to create system-default reference
|
|
*/
|
|
static createSystemDefault(variant: MediaVariant = 'avatar', avatarVariant?: AvatarVariant): MediaReference {
|
|
const props: SystemDefaultRef = { type: 'system-default', variant };
|
|
if (variant === 'avatar' && avatarVariant) {
|
|
props.avatarVariant = avatarVariant;
|
|
}
|
|
return new MediaReference(props);
|
|
}
|
|
|
|
/**
|
|
* Factory method for system default (alias for task compatibility)
|
|
*/
|
|
static systemDefault(variant: MediaVariant | AvatarVariant = 'avatar'): MediaReference {
|
|
// If it's an avatar variant, use it
|
|
if (variant === 'male' || variant === 'female' || variant === 'neutral') {
|
|
return new MediaReference({ type: 'system-default', variant: 'avatar', avatarVariant: variant });
|
|
}
|
|
// Otherwise it's a regular variant
|
|
return new MediaReference({ type: 'system-default', variant: variant as MediaVariant });
|
|
}
|
|
|
|
/**
|
|
* Factory method for generated references (alias for task compatibility)
|
|
*/
|
|
static generated(type: string, id: string): MediaReference {
|
|
return new MediaReference({ type: 'generated', generationRequestId: `${type}-${id}` });
|
|
}
|
|
|
|
/**
|
|
* Factory method to create generated reference
|
|
*/
|
|
static createGenerated(generationRequestId: string): MediaReference {
|
|
const trimmed = generationRequestId.trim();
|
|
if (!trimmed) {
|
|
throw new Error('Generation request ID is required');
|
|
}
|
|
return new MediaReference({ type: 'generated', generationRequestId: trimmed });
|
|
}
|
|
|
|
/**
|
|
* Factory method to create uploaded reference
|
|
*/
|
|
static createUploaded(mediaId: string): MediaReference {
|
|
const trimmed = mediaId.trim();
|
|
if (!trimmed) {
|
|
throw new Error('Media ID is required');
|
|
}
|
|
return new MediaReference({ type: 'uploaded', mediaId: trimmed });
|
|
}
|
|
|
|
/**
|
|
* Factory method to create none reference
|
|
*/
|
|
static createNone(): MediaReference {
|
|
return new MediaReference({ type: 'none' });
|
|
}
|
|
|
|
/**
|
|
* Deserialize from JSON
|
|
*/
|
|
static fromJSON(json: Record<string, unknown>): MediaReference {
|
|
if (!json || typeof json !== 'object') {
|
|
throw new Error('Invalid JSON: must be an object');
|
|
}
|
|
|
|
const type = json.type;
|
|
if (!type || typeof type !== 'string') {
|
|
throw new Error('Type is required');
|
|
}
|
|
|
|
switch (type) {
|
|
case 'system-default':
|
|
if (!isSystemDefaultRef(json)) {
|
|
throw new Error('Invalid variant for system-default. Must be "avatar" or "logo"');
|
|
}
|
|
return new MediaReference({
|
|
type: 'system-default',
|
|
variant: json.variant,
|
|
avatarVariant: json.avatarVariant
|
|
});
|
|
|
|
case 'generated':
|
|
if (!isGeneratedRef(json)) {
|
|
throw new Error('Generation request ID is required');
|
|
}
|
|
return new MediaReference({ type: 'generated', generationRequestId: json.generationRequestId });
|
|
|
|
case 'uploaded':
|
|
if (!isUploadedRef(json)) {
|
|
throw new Error('Media ID is required');
|
|
}
|
|
return new MediaReference({ type: 'uploaded', mediaId: json.mediaId });
|
|
|
|
case 'none':
|
|
if (!isNoneRef(json)) {
|
|
throw new Error('None type should not have additional properties');
|
|
}
|
|
return new MediaReference({ type: 'none' });
|
|
|
|
default:
|
|
throw new Error('Invalid type');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialize to JSON
|
|
*/
|
|
toJSON(): MediaReferenceProps {
|
|
return { ...this.props };
|
|
}
|
|
|
|
/**
|
|
* Get the type of this reference
|
|
*/
|
|
get type(): MediaReferenceProps['type'] {
|
|
return this.props.type;
|
|
}
|
|
|
|
/**
|
|
* Get variant (only for system-default)
|
|
*/
|
|
get variant(): MediaVariant | undefined {
|
|
return this.props.type === 'system-default' ? this.props.variant : undefined;
|
|
}
|
|
|
|
/**
|
|
* Get avatar variant (only for system-default with avatar variant)
|
|
*/
|
|
get avatarVariant(): AvatarVariant | undefined {
|
|
return this.props.type === 'system-default' ? this.props.avatarVariant : undefined;
|
|
}
|
|
|
|
/**
|
|
* Get generation request ID (only for generated)
|
|
*/
|
|
get generationRequestId(): string | undefined {
|
|
return this.props.type === 'generated' ? this.props.generationRequestId : undefined;
|
|
}
|
|
|
|
/**
|
|
* Get media ID (only for uploaded)
|
|
*/
|
|
get mediaId(): string | undefined {
|
|
return this.props.type === 'uploaded' ? this.props.mediaId : undefined;
|
|
}
|
|
|
|
/**
|
|
* Equality comparison
|
|
*/
|
|
equals(other: ValueObject<MediaReferenceProps>): boolean {
|
|
if (!(other instanceof MediaReference)) {
|
|
return false;
|
|
}
|
|
|
|
const a = this.props;
|
|
const b = other.props;
|
|
|
|
if (a.type !== b.type) {
|
|
return false;
|
|
}
|
|
|
|
switch (a.type) {
|
|
case 'system-default':
|
|
return b.type === 'system-default' &&
|
|
a.variant === b.variant &&
|
|
a.avatarVariant === b.avatarVariant;
|
|
case 'generated':
|
|
return b.type === 'generated' && a.generationRequestId === b.generationRequestId;
|
|
case 'uploaded':
|
|
return b.type === 'uploaded' && a.mediaId === b.mediaId;
|
|
case 'none':
|
|
return b.type === 'none';
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate hash for this reference
|
|
* Used for caching and comparison
|
|
*/
|
|
hash(): string {
|
|
switch (this.props.type) {
|
|
case 'system-default':
|
|
return `system-default:${this.props.variant}${this.props.avatarVariant ? `:${this.props.avatarVariant}` : ''}`;
|
|
case 'generated':
|
|
return `generated:${this.props.generationRequestId}`;
|
|
case 'uploaded':
|
|
return `uploaded:${this.props.mediaId}`;
|
|
case 'none':
|
|
return 'none';
|
|
default:
|
|
// Exhaustive check - should never reach here
|
|
throw new Error('Unknown type');
|
|
}
|
|
}
|
|
} |