diff --git a/crates/adapters/postgres/migrations/0026_person_enrichment.sql b/crates/adapters/postgres/migrations/0026_person_enrichment.sql new file mode 100644 index 0000000..4d46f6d --- /dev/null +++ b/crates/adapters/postgres/migrations/0026_person_enrichment.sql @@ -0,0 +1,8 @@ +ALTER TABLE persons ADD COLUMN biography TEXT; +ALTER TABLE persons ADD COLUMN birthday TEXT; +ALTER TABLE persons ADD COLUMN deathday TEXT; +ALTER TABLE persons ADD COLUMN place_of_birth TEXT; +ALTER TABLE persons ADD COLUMN also_known_as TEXT; +ALTER TABLE persons ADD COLUMN homepage TEXT; +ALTER TABLE persons ADD COLUMN imdb_id TEXT; +ALTER TABLE persons ADD COLUMN enriched_at TIMESTAMPTZ; diff --git a/crates/adapters/postgres/src/persons.rs b/crates/adapters/postgres/src/persons.rs index 8251549..431e90d 100644 --- a/crates/adapters/postgres/src/persons.rs +++ b/crates/adapters/postgres/src/persons.rs @@ -117,76 +117,58 @@ impl PersonCommand for PostgresPersonAdapter { async fn update_enrichment( &self, - _id: &PersonId, - _data: &PersonEnrichmentData, + id: &PersonId, + data: &PersonEnrichmentData, ) -> Result<(), DomainError> { - todo!("person enrichment persistence") + let also_known_as_json = + serde_json::to_string(&data.also_known_as).unwrap_or_else(|_| "[]".into()); + let now = chrono::Utc::now(); + sqlx::query( + "UPDATE persons SET biography = $1, birthday = $2, deathday = $3, place_of_birth = $4, also_known_as = $5, homepage = $6, imdb_id = $7, enriched_at = $8 WHERE id = $9", + ) + .bind(&data.biography) + .bind(data.birthday.map(|d| d.to_string())) + .bind(data.deathday.map(|d| d.to_string())) + .bind(&data.place_of_birth) + .bind(&also_known_as_json) + .bind(&data.homepage) + .bind(&data.imdb_id) + .bind(now) + .bind(id.value().to_string()) + .execute(&self.pool) + .await + .map_err(map_err)?; + Ok(()) } } #[async_trait] impl PersonQuery for PostgresPersonAdapter { async fn get_by_id(&self, id: &PersonId) -> Result, DomainError> { - #[derive(sqlx::FromRow)] - struct Row { - id: String, - external_id: String, - name: String, - known_for_department: Option, - profile_path: Option, - } - - let row = sqlx::query_as::<_, Row>( - "SELECT id, external_id, name, known_for_department, profile_path FROM persons WHERE id = $1", + let row = sqlx::query_as::<_, PersonRow>( + "SELECT id, external_id, name, known_for_department, profile_path, biography, birthday, deathday, place_of_birth, also_known_as, homepage, imdb_id, enriched_at FROM persons WHERE id = $1", ) .bind(id.value().to_string()) .fetch_optional(&self.pool) .await .map_err(map_err)?; - Ok(row.map(|r| { - let ext = ExternalPersonId::new(r.external_id); - Person::basic( - PersonId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), - ext, - r.name, - r.known_for_department, - r.profile_path, - ) - })) + Ok(row.map(PersonRow::into_person)) } async fn get_by_external_id( &self, id: &ExternalPersonId, ) -> Result, DomainError> { - #[derive(sqlx::FromRow)] - struct Row { - id: String, - external_id: String, - name: String, - known_for_department: Option, - profile_path: Option, - } - - let row = sqlx::query_as::<_, Row>( - "SELECT id, external_id, name, known_for_department, profile_path FROM persons WHERE external_id = $1", + let row = sqlx::query_as::<_, PersonRow>( + "SELECT id, external_id, name, known_for_department, profile_path, biography, birthday, deathday, place_of_birth, also_known_as, homepage, imdb_id, enriched_at FROM persons WHERE external_id = $1", ) .bind(id.value()) .fetch_optional(&self.pool) .await .map_err(map_err)?; - Ok(row.map(|r| { - let ext = ExternalPersonId::new(r.external_id); - Person::basic( - PersonId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), - ext, - r.name, - r.known_for_department, - r.profile_path, - ) - })) + Ok(row.map(PersonRow::into_person)) } async fn get_credits(&self, id: &PersonId) -> Result { @@ -272,17 +254,8 @@ impl PersonQuery for PostgresPersonAdapter { } async fn list_page(&self, limit: u32, offset: u32) -> Result, DomainError> { - #[derive(sqlx::FromRow)] - struct Row { - id: String, - external_id: String, - name: String, - known_for_department: Option, - profile_path: Option, - } - - let rows = sqlx::query_as::<_, Row>( - "SELECT id, external_id, name, known_for_department, profile_path FROM persons ORDER BY id LIMIT $1 OFFSET $2", + let rows = sqlx::query_as::<_, PersonRow>( + "SELECT id, external_id, name, known_for_department, profile_path, biography, birthday, deathday, place_of_birth, also_known_as, homepage, imdb_id, enriched_at FROM persons ORDER BY id LIMIT $1 OFFSET $2", ) .bind(limit as i64) .bind(offset as i64) @@ -290,19 +263,7 @@ impl PersonQuery for PostgresPersonAdapter { .await .map_err(map_err)?; - Ok(rows - .into_iter() - .map(|r| { - let ext = ExternalPersonId::new(r.external_id); - Person::basic( - PersonId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap_or_default()), - ext, - r.name, - r.known_for_department, - r.profile_path, - ) - }) - .collect()) + Ok(rows.into_iter().map(PersonRow::into_person).collect()) } async fn list_orphaned_persons(&self) -> Result, DomainError> { @@ -326,3 +287,57 @@ impl PersonQuery for PostgresPersonAdapter { .collect()) } } + +// ── Row types ──────────────────────────────────────────────────────────────── + +#[derive(sqlx::FromRow)] +struct PersonRow { + id: String, + external_id: String, + name: String, + known_for_department: Option, + profile_path: Option, + biography: Option, + birthday: Option, + deathday: Option, + place_of_birth: Option, + also_known_as: Option, + homepage: Option, + imdb_id: Option, + enriched_at: Option, +} + +impl PersonRow { + fn into_person(self) -> Person { + let ext = ExternalPersonId::new(self.external_id); + let also_known_as = self + .also_known_as + .and_then(|s| serde_json::from_str::>(&s).ok()) + .unwrap_or_default(); + let birthday = self + .birthday + .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()); + let deathday = self + .deathday + .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()); + let enriched_at = self + .enriched_at + .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) + .map(|d| d.with_timezone(&chrono::Utc)); + Person::new( + PersonId::from_uuid(uuid::Uuid::parse_str(&self.id).unwrap_or_default()), + ext, + self.name, + self.known_for_department, + self.profile_path, + self.biography, + birthday, + deathday, + self.place_of_birth, + also_known_as, + self.homepage, + self.imdb_id, + enriched_at, + ) + } +} diff --git a/crates/adapters/sqlite/migrations/0026_person_enrichment.sql b/crates/adapters/sqlite/migrations/0026_person_enrichment.sql new file mode 100644 index 0000000..23159ad --- /dev/null +++ b/crates/adapters/sqlite/migrations/0026_person_enrichment.sql @@ -0,0 +1,8 @@ +ALTER TABLE persons ADD COLUMN biography TEXT; +ALTER TABLE persons ADD COLUMN birthday TEXT; +ALTER TABLE persons ADD COLUMN deathday TEXT; +ALTER TABLE persons ADD COLUMN place_of_birth TEXT; +ALTER TABLE persons ADD COLUMN also_known_as TEXT; +ALTER TABLE persons ADD COLUMN homepage TEXT; +ALTER TABLE persons ADD COLUMN imdb_id TEXT; +ALTER TABLE persons ADD COLUMN enriched_at TEXT; diff --git a/crates/adapters/sqlite/src/persons.rs b/crates/adapters/sqlite/src/persons.rs index 07b4bde..2e71c1c 100644 --- a/crates/adapters/sqlite/src/persons.rs +++ b/crates/adapters/sqlite/src/persons.rs @@ -117,10 +117,28 @@ impl PersonCommand for SqlitePersonAdapter { async fn update_enrichment( &self, - _id: &PersonId, - _data: &PersonEnrichmentData, + id: &PersonId, + data: &PersonEnrichmentData, ) -> Result<(), DomainError> { - todo!("person enrichment persistence") + let also_known_as_json = + serde_json::to_string(&data.also_known_as).unwrap_or_else(|_| "[]".into()); + let now = chrono::Utc::now().to_rfc3339(); + sqlx::query( + "UPDATE persons SET biography = ?, birthday = ?, deathday = ?, place_of_birth = ?, also_known_as = ?, homepage = ?, imdb_id = ?, enriched_at = ? WHERE id = ?", + ) + .bind(&data.biography) + .bind(data.birthday.map(|d| d.to_string())) + .bind(data.deathday.map(|d| d.to_string())) + .bind(&data.place_of_birth) + .bind(&also_known_as_json) + .bind(&data.homepage) + .bind(&data.imdb_id) + .bind(&now) + .bind(id.value().to_string()) + .execute(&self.pool) + .await + .map_err(map_err)?; + Ok(()) } } @@ -128,7 +146,7 @@ impl PersonCommand for SqlitePersonAdapter { impl PersonQuery for SqlitePersonAdapter { async fn get_by_id(&self, id: &PersonId) -> Result, DomainError> { let row = sqlx::query_as::<_, PersonRow>( - "SELECT id, external_id, name, known_for_department, profile_path FROM persons WHERE id = ?", + "SELECT id, external_id, name, known_for_department, profile_path, biography, birthday, deathday, place_of_birth, also_known_as, homepage, imdb_id, enriched_at FROM persons WHERE id = ?", ) .bind(id.value().to_string()) .fetch_optional(&self.pool) @@ -143,7 +161,7 @@ impl PersonQuery for SqlitePersonAdapter { id: &ExternalPersonId, ) -> Result, DomainError> { let row = sqlx::query_as::<_, PersonRow>( - "SELECT id, external_id, name, known_for_department, profile_path FROM persons WHERE external_id = ?", + "SELECT id, external_id, name, known_for_department, profile_path, biography, birthday, deathday, place_of_birth, also_known_as, homepage, imdb_id, enriched_at FROM persons WHERE external_id = ?", ) .bind(id.value()) .fetch_optional(&self.pool) @@ -223,7 +241,7 @@ impl PersonQuery for SqlitePersonAdapter { async fn list_page(&self, limit: u32, offset: u32) -> Result, DomainError> { let rows = sqlx::query_as::<_, PersonRow>( - "SELECT id, external_id, name, known_for_department, profile_path FROM persons ORDER BY id LIMIT ? OFFSET ?", + "SELECT id, external_id, name, known_for_department, profile_path, biography, birthday, deathday, place_of_birth, also_known_as, homepage, imdb_id, enriched_at FROM persons ORDER BY id LIMIT ? OFFSET ?", ) .bind(limit) .bind(offset) @@ -265,17 +283,47 @@ struct PersonRow { name: String, known_for_department: Option, profile_path: Option, + biography: Option, + birthday: Option, + deathday: Option, + place_of_birth: Option, + also_known_as: Option, + homepage: Option, + imdb_id: Option, + enriched_at: Option, } impl PersonRow { fn into_person(self) -> Person { let ext = ExternalPersonId::new(self.external_id); - Person::basic( + let also_known_as = self + .also_known_as + .and_then(|s| serde_json::from_str::>(&s).ok()) + .unwrap_or_default(); + let birthday = self + .birthday + .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()); + let deathday = self + .deathday + .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()); + let enriched_at = self + .enriched_at + .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) + .map(|d| d.with_timezone(&chrono::Utc)); + Person::new( PersonId::from_uuid(uuid::Uuid::parse_str(&self.id).unwrap_or_default()), ext, self.name, self.known_for_department, self.profile_path, + self.biography, + birthday, + deathday, + self.place_of_birth, + also_known_as, + self.homepage, + self.imdb_id, + enriched_at, ) } }