refactor(media-gallery): ♻️ Standardize entity structure in Album, Device, FaceHashRegistry, Identity, and Photo classes for cleaner data modeling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f85e4799ab
commit
d8a5ac1c19
6 changed files with 0 additions and 341 deletions
|
|
@ -1,55 +0,0 @@
|
|||
import { BaseEntity } from '@lilith/typeorm-entities';
|
||||
import { Entity, Column, ManyToOne, ManyToMany, JoinColumn, JoinTable, Index } from 'typeorm';
|
||||
|
||||
import type { DeviceEntity } from './device.entity';
|
||||
import type { PhotoEntity } from './photo.entity';
|
||||
|
||||
export type AlbumType = 'user' | 'smart' | 'shared' | 'system';
|
||||
|
||||
@Entity('albums')
|
||||
@Index('IDX_albums_device_local_id', ['deviceId', 'localIdentifier'], { unique: true })
|
||||
export class AlbumEntity extends BaseEntity {
|
||||
@Column({ name: 'device_id', type: 'uuid' })
|
||||
deviceId!: string;
|
||||
|
||||
// Use string reference to avoid circular dependency at runtime
|
||||
@ManyToOne('DeviceEntity', { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'device_id' })
|
||||
device!: DeviceEntity;
|
||||
|
||||
/** PHAssetCollection.localIdentifier from Photos.app */
|
||||
@Column({ name: 'local_identifier', type: 'varchar', length: 255 })
|
||||
localIdentifier!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
title!: string;
|
||||
|
||||
@Column({ name: 'album_type', type: 'varchar', length: 20 })
|
||||
albumType!: AlbumType;
|
||||
|
||||
@Column({ name: 'photo_count', type: 'integer', default: 0 })
|
||||
photoCount!: number;
|
||||
|
||||
/** UUID of the photo used as cover */
|
||||
@Column({ name: 'cover_photo_id', type: 'uuid', nullable: true })
|
||||
coverPhotoId?: string | null;
|
||||
|
||||
@Column({ name: 'start_date', type: 'timestamptz', nullable: true })
|
||||
startDate?: Date | null;
|
||||
|
||||
@Column({ name: 'end_date', type: 'timestamptz', nullable: true })
|
||||
endDate?: Date | null;
|
||||
|
||||
/** Sort order for display */
|
||||
@Column({ name: 'sort_order', type: 'integer', default: 0 })
|
||||
sortOrder!: number;
|
||||
|
||||
// Use string reference to avoid circular dependency at runtime
|
||||
@ManyToMany('PhotoEntity', 'albums')
|
||||
@JoinTable({
|
||||
name: 'album_photos',
|
||||
joinColumn: { name: 'album_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'photo_id', referencedColumnName: 'id' },
|
||||
})
|
||||
photos!: PhotoEntity[];
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import { BaseEntity } from '@lilith/typeorm-entities';
|
||||
import { Entity, Column, OneToMany } from 'typeorm';
|
||||
|
||||
import type { PhotoEntity } from './photo.entity';
|
||||
|
||||
export type DevicePlatform = 'macos' | 'ios';
|
||||
|
||||
@Entity('devices')
|
||||
export class DeviceEntity extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
name!: string;
|
||||
|
||||
@Column({ name: 'hardware_id', type: 'varchar', length: 255, unique: true })
|
||||
hardwareId!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 10 })
|
||||
platform!: DevicePlatform;
|
||||
|
||||
@Column({ name: 'os_version', type: 'varchar', length: 50 })
|
||||
osVersion!: string;
|
||||
|
||||
@Column({ name: 'auth_code', type: 'varchar', length: 6, nullable: true })
|
||||
authCode?: string | null;
|
||||
|
||||
@Column({ name: 'auth_code_expires', type: 'timestamptz', nullable: true })
|
||||
authCodeExpires?: Date | null;
|
||||
|
||||
@Column({ name: 'jwt_secret', type: 'varchar', length: 255, nullable: true })
|
||||
jwtSecret?: string | null;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: false })
|
||||
isActive!: boolean;
|
||||
|
||||
@Column({ name: 'last_sync_at', type: 'timestamptz', nullable: true })
|
||||
lastSyncAt?: Date | null;
|
||||
|
||||
@Column({ name: 'last_seen', type: 'timestamptz', nullable: true })
|
||||
lastSeen?: Date | null;
|
||||
|
||||
@Column({ name: 'photo_count', type: 'integer', default: 0 })
|
||||
photoCount!: number;
|
||||
|
||||
@Column({ name: 'user_id', type: 'varchar', length: 255, default: 'lilith-default' })
|
||||
userId!: string;
|
||||
|
||||
// Use string reference to avoid circular dependency at runtime
|
||||
@OneToMany('PhotoEntity', 'device')
|
||||
photos!: PhotoEntity[];
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
import type { Relation } from 'typeorm';
|
||||
|
||||
import type { PhotoEntity } from './photo.entity';
|
||||
|
||||
@Entity('face_hash_registry')
|
||||
export class FaceHashRegistryEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'lsh_hash', type: 'varchar', length: 32 })
|
||||
lshHash!: string;
|
||||
|
||||
@Column({ name: 'lsh_band_hashes', type: 'text', array: true })
|
||||
lshBandHashes!: string[];
|
||||
|
||||
@Column({ name: 'photo_id', type: 'uuid' })
|
||||
photoId!: string;
|
||||
|
||||
@ManyToOne('PhotoEntity', { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'photo_id' })
|
||||
photo!: Relation<PhotoEntity>;
|
||||
|
||||
@Column({ name: 'face_index', type: 'integer', default: 0 })
|
||||
faceIndex!: number;
|
||||
|
||||
@Column({ name: 'blocklisted', type: 'boolean', default: false })
|
||||
blocklisted!: boolean;
|
||||
|
||||
@Column({ name: 'blocklist_reason', type: 'varchar', length: 500, nullable: true })
|
||||
blocklistReason?: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { BaseEntity } from '@lilith/typeorm-entities';
|
||||
import { Entity, Column, ManyToMany, JoinTable } from 'typeorm';
|
||||
|
||||
import type { Relation } from 'typeorm';
|
||||
import type { PhotoEntity } from './photo.entity';
|
||||
|
||||
@Entity('identities')
|
||||
export class IdentityEntity extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
/** User-flagged as the gallery owner (self) */
|
||||
@Column({ name: 'is_self', type: 'boolean', default: false })
|
||||
isSelf!: boolean;
|
||||
|
||||
/** Denormalized count updated on photo assignment */
|
||||
@Column({ name: 'photo_count', type: 'integer', default: 0 })
|
||||
photoCount!: number;
|
||||
|
||||
/** UUID of the photo used as identity cover thumbnail */
|
||||
@Column({ name: 'cover_photo_id', type: 'uuid', nullable: true })
|
||||
coverPhotoId?: string | null;
|
||||
|
||||
@Column({ name: 'user_id', type: 'varchar', length: 255, default: 'lilith-default' })
|
||||
userId!: string;
|
||||
|
||||
@Column({ name: 'centroid_status', type: 'varchar', length: 20, default: 'empty' })
|
||||
centroidStatus!: 'empty' | 'building' | 'ready';
|
||||
|
||||
@Column({ name: 'centroid_photo_count', type: 'integer', default: 0 })
|
||||
centroidPhotoCount!: number;
|
||||
|
||||
@Column({ name: 'imajin_synced_at', type: 'timestamptz', nullable: true })
|
||||
imajinSyncedAt?: Date | null;
|
||||
|
||||
@ManyToMany('PhotoEntity', 'identities')
|
||||
@JoinTable({
|
||||
name: 'identity_photos',
|
||||
joinColumn: { name: 'identity_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'photo_id', referencedColumnName: 'id' },
|
||||
})
|
||||
photos!: Relation<PhotoEntity>[];
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Re-export entities with types
|
||||
// Note: Import order matters - entities without dependencies first
|
||||
export { DeviceEntity, type DevicePlatform } from './device.entity';
|
||||
export { PhotoEntity, type MediaType, type PhotoExif } from './photo.entity';
|
||||
export { AlbumEntity, type AlbumType } from './album.entity';
|
||||
export { IdentityEntity } from './identity.entity';
|
||||
export { FaceHashRegistryEntity } from './face-hash-registry.entity';
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
import { BaseEntity } from '@lilith/typeorm-entities';
|
||||
import { Entity, Column, ManyToOne, ManyToMany, JoinColumn, Index } from 'typeorm';
|
||||
|
||||
import type { AlbumEntity } from './album.entity';
|
||||
import type { DeviceEntity } from './device.entity';
|
||||
import type { IdentityEntity } from './identity.entity';
|
||||
|
||||
export type MediaType = 'image' | 'video' | 'live_photo';
|
||||
|
||||
export interface PhotoExif {
|
||||
make?: string;
|
||||
model?: string;
|
||||
aperture?: number;
|
||||
iso?: number;
|
||||
focalLength?: number;
|
||||
exposureTime?: string;
|
||||
lensModel?: string;
|
||||
software?: string;
|
||||
}
|
||||
|
||||
@Entity('photos')
|
||||
@Index('IDX_photos_device_local_id', ['deviceId', 'localIdentifier'], { unique: true })
|
||||
@Index('IDX_photos_captured_at', ['capturedAt'])
|
||||
@Index('IDX_photos_media_type', ['mediaType'])
|
||||
export class PhotoEntity extends BaseEntity {
|
||||
@Column({ name: 'device_id', type: 'uuid' })
|
||||
deviceId!: string;
|
||||
|
||||
// Use string reference to avoid circular dependency at runtime
|
||||
@ManyToOne('DeviceEntity', 'photos', { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'device_id' })
|
||||
device!: DeviceEntity;
|
||||
|
||||
/** PHAsset.localIdentifier from Photos.app - unique per device */
|
||||
@Column({ name: 'local_identifier', type: 'varchar', length: 255 })
|
||||
localIdentifier!: string;
|
||||
|
||||
@Column({ name: 'media_type', type: 'varchar', length: 20 })
|
||||
mediaType!: MediaType;
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
width!: number;
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
height!: number;
|
||||
|
||||
@Column({ name: 'file_size', type: 'bigint', nullable: true })
|
||||
fileSize?: number | null;
|
||||
|
||||
/** Duration in seconds for video/live_photo */
|
||||
@Column({ name: 'duration_seconds', type: 'float', nullable: true })
|
||||
durationSeconds?: number | null;
|
||||
|
||||
@Column({ name: 'captured_at', type: 'timestamptz' })
|
||||
capturedAt!: Date;
|
||||
|
||||
@Column({ name: 'modified_at', type: 'timestamptz', nullable: true })
|
||||
modifiedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'imported_at', type: 'timestamptz', default: () => 'now()' })
|
||||
importedAt!: Date;
|
||||
|
||||
/** MinIO storage path for original file */
|
||||
@Column({ name: 'storage_key', type: 'varchar', length: 500, nullable: true })
|
||||
storageKey?: string | null;
|
||||
|
||||
/** MinIO storage path for 300x300 thumbnail */
|
||||
@Column({ name: 'thumbnail_key', type: 'varchar', length: 500, nullable: true })
|
||||
thumbnailKey?: string | null;
|
||||
|
||||
/** MinIO storage path for 1200px preview */
|
||||
@Column({ name: 'preview_key', type: 'varchar', length: 500, nullable: true })
|
||||
previewKey?: string | null;
|
||||
|
||||
/** Original filename from Photos.app */
|
||||
@Column({ name: 'original_filename', type: 'varchar', length: 255, nullable: true })
|
||||
originalFilename?: string | null;
|
||||
|
||||
/** MIME type of the original file */
|
||||
@Column({ name: 'mime_type', type: 'varchar', length: 100, nullable: true })
|
||||
mimeType?: string | null;
|
||||
|
||||
// Location data
|
||||
@Column({ type: 'float', nullable: true })
|
||||
latitude?: number | null;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
longitude?: number | null;
|
||||
|
||||
@Column({ name: 'location_name', type: 'varchar', length: 500, nullable: true })
|
||||
locationName?: string | null;
|
||||
|
||||
// Flags from Photos.app
|
||||
@Column({ name: 'is_favorite', type: 'boolean', default: false })
|
||||
isFavorite!: boolean;
|
||||
|
||||
@Column({ name: 'is_hidden', type: 'boolean', default: false })
|
||||
isHidden!: boolean;
|
||||
|
||||
@Column({ name: 'is_screenshot', type: 'boolean', default: false })
|
||||
isScreenshot!: boolean;
|
||||
|
||||
@Column({ name: 'is_selfie', type: 'boolean', default: false })
|
||||
isSelfie!: boolean;
|
||||
|
||||
@Column({ name: 'is_burst', type: 'boolean', default: false })
|
||||
isBurst!: boolean;
|
||||
|
||||
@Column({ name: 'burst_identifier', type: 'varchar', length: 255, nullable: true })
|
||||
burstIdentifier?: string | null;
|
||||
|
||||
/** EXIF metadata stored as JSONB */
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
exif?: PhotoExif | null;
|
||||
|
||||
/** Hash of the original file for deduplication */
|
||||
@Column({ name: 'content_hash', type: 'varchar', length: 64, nullable: true })
|
||||
contentHash?: string | null;
|
||||
|
||||
/** Processing status: pending, processing, completed, failed */
|
||||
@Column({ name: 'processing_status', type: 'varchar', length: 20, default: 'pending' })
|
||||
processingStatus!: string;
|
||||
|
||||
@Column({ name: 'processing_error', type: 'text', nullable: true })
|
||||
processingError?: string | null;
|
||||
|
||||
/** AI-assigned classification category */
|
||||
@Column({ name: 'category', type: 'varchar', length: 50, nullable: true })
|
||||
category?: string | null;
|
||||
|
||||
/** Semantic attribute scores from imajin-semantic (SigLIP zero-shot detection) */
|
||||
@Column({ name: 'semantic_tags', type: 'jsonb', nullable: true })
|
||||
semanticTags?: Record<string, number> | null;
|
||||
|
||||
/** Classification pipeline status: pending, processing, completed, failed, skipped */
|
||||
@Column({ name: 'classification_status', type: 'varchar', length: 20, default: 'pending' })
|
||||
classificationStatus!: string;
|
||||
|
||||
@Column({ name: 'face_extraction_status', type: 'varchar', length: 20, default: 'pending' })
|
||||
faceExtractionStatus!: 'pending' | 'processing' | 'completed' | 'failed' | 'skipped' | 'no_face';
|
||||
|
||||
@Column({ name: 'face_count', type: 'integer', nullable: true })
|
||||
faceCount?: number | null;
|
||||
|
||||
// Use string reference to avoid circular dependency at runtime
|
||||
@ManyToMany('AlbumEntity', 'photos')
|
||||
albums!: AlbumEntity[];
|
||||
|
||||
@ManyToMany('IdentityEntity', 'photos')
|
||||
identities!: IdentityEntity[];
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue