1pub mod collections {
5 use flix_model::id::{CollectionId, RawId};
6
7 use chrono::{DateTime, Utc};
8 use sea_orm::entity::prelude::*;
9
10 use crate::entity;
11
12 #[sea_orm::model]
14 #[derive(Debug, Clone, DeriveEntityModel)]
15 #[sea_orm(table_name = "flix_watched_collections")]
16 pub struct Model {
17 #[sea_orm(primary_key, auto_increment = false)]
19 pub id: CollectionId,
20 #[sea_orm(primary_key, auto_increment = false)]
22 pub user_id: RawId,
23 pub watched_date: DateTime<Utc>,
25
26 #[sea_orm(
28 belongs_to,
29 relation_enum = "Info",
30 from = "id",
31 to = "id",
32 on_update = "Cascade",
33 on_delete = "Cascade"
34 )]
35 pub info: HasOne<entity::info::collections::Entity>,
36 #[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
38 pub content: HasOne<entity::content::collections::Entity>,
39 }
40
41 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
42 impl ActiveModelBehavior for ActiveModel {}
43}
44
45pub mod movies {
47 use flix_model::id::{MovieId, RawId};
48
49 use chrono::{DateTime, Utc};
50 use sea_orm::entity::prelude::*;
51
52 use crate::entity;
53
54 #[sea_orm::model]
56 #[derive(Debug, Clone, DeriveEntityModel)]
57 #[sea_orm(table_name = "flix_watched_movies")]
58 pub struct Model {
59 #[sea_orm(primary_key, auto_increment = false)]
61 pub id: MovieId,
62 #[sea_orm(primary_key, auto_increment = false)]
64 pub user_id: RawId,
65 pub watched_date: DateTime<Utc>,
67
68 #[sea_orm(
70 belongs_to,
71 from = "id",
72 to = "id",
73 on_update = "Cascade",
74 on_delete = "Cascade"
75 )]
76 pub info: HasOne<entity::info::movies::Entity>,
77 #[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
79 pub content: HasOne<entity::content::movies::Entity>,
80 }
81
82 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
83 impl ActiveModelBehavior for ActiveModel {}
84}
85
86pub mod shows {
88 use flix_model::id::{RawId, ShowId};
89
90 use chrono::{DateTime, Utc};
91 use sea_orm::entity::prelude::*;
92
93 use crate::entity;
94
95 #[sea_orm::model]
97 #[derive(Debug, Clone, DeriveEntityModel)]
98 #[sea_orm(table_name = "flix_watched_shows")]
99 pub struct Model {
100 #[sea_orm(primary_key, auto_increment = false)]
102 pub id: ShowId,
103 #[sea_orm(primary_key, auto_increment = false)]
105 pub user_id: RawId,
106 pub watched_date: DateTime<Utc>,
108
109 #[sea_orm(
111 belongs_to,
112 relation_enum = "Info",
113 from = "id",
114 to = "id",
115 on_update = "Cascade",
116 on_delete = "Cascade"
117 )]
118 pub info: HasOne<entity::info::shows::Entity>,
119 #[sea_orm(belongs_to, relation_enum = "Content", from = "id", to = "id", skip_fk)]
121 pub content: HasOne<entity::content::shows::Entity>,
122 }
123
124 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
125 impl ActiveModelBehavior for ActiveModel {}
126}
127
128pub mod seasons {
130 use flix_model::id::{RawId, ShowId};
131 use flix_model::numbers::SeasonNumber;
132
133 use chrono::{DateTime, Utc};
134 use sea_orm::entity::prelude::*;
135
136 use crate::entity;
137
138 #[sea_orm::model]
140 #[derive(Debug, Clone, DeriveEntityModel)]
141 #[sea_orm(table_name = "flix_watched_seasons")]
142 pub struct Model {
143 #[sea_orm(primary_key, auto_increment = false)]
145 pub show_id: ShowId,
146 #[sea_orm(primary_key, auto_increment = false)]
148 pub season_number: SeasonNumber,
149 #[sea_orm(primary_key, auto_increment = false)]
151 pub user_id: RawId,
152 pub watched_date: DateTime<Utc>,
154
155 #[sea_orm(
157 belongs_to,
158 relation_enum = "Info",
159 from = "(show_id, season_number)",
160 to = "(show_id, season_number)",
161 on_update = "Cascade",
162 on_delete = "Cascade"
163 )]
164 pub info: HasOne<entity::info::seasons::Entity>,
165 #[sea_orm(
167 belongs_to,
168 relation_enum = "Content",
169 from = "(show_id, season_number)",
170 to = "(show_id, season_number)",
171 skip_fk
172 )]
173 pub content: HasOne<entity::content::seasons::Entity>,
174 }
175
176 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
177 impl ActiveModelBehavior for ActiveModel {}
178}
179
180pub mod episodes {
182 use flix_model::id::{RawId, ShowId};
183 use flix_model::numbers::{EpisodeNumber, SeasonNumber};
184
185 use chrono::{DateTime, Utc};
186 use sea_orm::entity::prelude::*;
187
188 use crate::entity;
189
190 #[sea_orm::model]
192 #[derive(Debug, Clone, DeriveEntityModel)]
193 #[sea_orm(table_name = "flix_watched_episodes")]
194 pub struct Model {
195 #[sea_orm(primary_key, auto_increment = false)]
197 pub show_id: ShowId,
198 #[sea_orm(primary_key, auto_increment = false)]
200 pub season_number: SeasonNumber,
201 #[sea_orm(primary_key, auto_increment = false)]
203 pub episode_number: EpisodeNumber,
204 #[sea_orm(primary_key, auto_increment = false)]
206 pub user_id: RawId,
207 pub watched_date: DateTime<Utc>,
209
210 #[sea_orm(
212 belongs_to,
213 relation_enum = "Info",
214 from = "(show_id, season_number, episode_number)",
215 to = "(show_id, season_number, episode_number)",
216 on_update = "Cascade",
217 on_delete = "Cascade"
218 )]
219 pub info: HasOne<entity::info::episodes::Entity>,
220 #[sea_orm(
222 belongs_to,
223 relation_enum = "Content",
224 from = "(show_id, season_number, episode_number)",
225 to = "(show_id, season_number, episode_number)",
226 skip_fk
227 )]
228 pub content: HasOne<entity::content::episodes::Entity>,
229 }
230
231 #[expect(clippy::missing_trait_methods, reason = "accept default methods")]
232 impl ActiveModelBehavior for ActiveModel {}
233}
234
235#[cfg(test)]
237pub mod test {
238 macro_rules! make_watched_movie {
239 ($db:expr, $id:expr, $user:expr) => {
240 _ = $crate::entity::watched::movies::ActiveModel {
241 id: Set(::flix_model::id::MovieId::from_raw($id)),
242 user_id: Set($user),
243 watched_date: Set(::chrono::Utc::now()),
244 }
245 .insert($db)
246 .await
247 .expect("insert");
248 };
249 }
250 pub(crate) use make_watched_movie;
251
252 macro_rules! make_watched_episode {
253 ($db:expr, $show:expr, $season:expr, $episode:expr, $user:expr) => {
254 _ = $crate::entity::watched::episodes::ActiveModel {
255 show_id: Set(::flix_model::id::ShowId::from_raw($show)),
256 season_number: Set(::flix_model::numbers::SeasonNumber::new($season)),
257 episode_number: Set(::flix_model::numbers::EpisodeNumber::new($episode)),
258 user_id: Set($user),
259 watched_date: Set(::chrono::Utc::now()),
260 }
261 .insert($db)
262 .await
263 .expect("insert");
264 };
265 }
266 pub(crate) use make_watched_episode;
267}
268
269#[cfg(test)]
270mod tests {
271 use flix_model::id::{MovieId, ShowId};
272
273 use chrono::NaiveDate;
274 use sea_orm::ActiveValue::{NotSet, Set};
275 use sea_orm::Condition;
276 use sea_orm::entity::prelude::*;
277 use sea_orm::sqlx::error::ErrorKind;
278
279 use crate::entity::content::test::{
280 make_content_collection, make_content_episode, make_content_library, make_content_movie,
281 make_content_season, make_content_show,
282 };
283 use crate::entity::info::test::{
284 make_info_episode, make_info_movie, make_info_season, make_info_show,
285 };
286 use crate::tests::new_initialized_memory_db;
287
288 use super::super::tests::get_error_kind;
289 use super::super::tests::notsettable;
290 use super::test::{make_watched_episode, make_watched_movie};
291
292 #[cfg(not(miri))]
293 #[tokio::test]
294 #[expect(clippy::missing_panics_doc, reason = "unit test")]
295 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
296 async fn use_test_macros() {
297 let db = new_initialized_memory_db().await;
298
299 make_info_movie!(&db, 1);
300 make_info_show!(&db, 1);
301 make_info_season!(&db, 1, 1);
302 make_info_episode!(&db, 1, 1, 1);
303
304 make_watched_movie!(&db, 1, 1);
305 make_watched_episode!(&db, 1, 1, 1, 1);
306 }
307
308 #[cfg(not(miri))]
309 #[tokio::test]
310 #[expect(clippy::missing_panics_doc, reason = "unit test")]
311 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
312 async fn round_trip_movies() {
313 let db = new_initialized_memory_db().await;
314
315 macro_rules! assert_movie {
316 ($db:expr, $id:literal, $uid:literal, Success $(; $($skip:ident),+)?) => {
317 let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?)
318 .expect("insert");
319
320 assert_eq!(model.id, MovieId::from_raw($id));
321 assert_eq!(model.user_id, $uid);
322 assert_eq!(model.watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
323 };
324 ($db:expr, $id:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => {
325 let model = assert_movie!(@insert, $db, $id, $uid $(; $($skip),+)?)
326 .expect_err("insert");
327
328 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
329 };
330 (@insert, $db:expr, $id:literal, $uid:literal $(; $($skip:ident),+)?) => {
331 super::movies::ActiveModel {
332 id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?),
333 user_id: notsettable!(user_id, $uid $(, $($skip),+)?),
334 watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
335 }.insert($db).await
336 };
337 }
338
339 assert_movie!(&db, 1, 1, ForeignKeyViolation);
340 make_info_movie!(&db, 1);
341 assert_movie!(&db, 1, 1, Success);
342 assert_movie!(&db, 1, 2, Success);
343
344 assert_movie!(&db, 1, 1, UniqueViolation);
345 make_info_movie!(&db, 2);
346 assert_movie!(&db, 2, 1, Success);
347 assert_movie!(&db, 2, 2, Success);
348
349 assert_movie!(&db, 3, 1, NotNullViolation; id);
350 assert_movie!(&db, 4, 1, NotNullViolation; user_id);
351 assert_movie!(&db, 5, 1, NotNullViolation; watched_date);
352 }
353
354 #[cfg(not(miri))]
355 #[tokio::test]
356 #[expect(clippy::missing_panics_doc, reason = "unit test")]
357 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
358 async fn round_trip_episodes() {
359 let db = new_initialized_memory_db().await;
360
361 macro_rules! assert_episode {
362 ($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, Success $(; $($skip:ident),+)?) => {
363 let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?)
364 .expect("insert");
365
366 assert_eq!(model.show_id, ShowId::from_raw($show));
367 assert_eq!(model.season_number, ::flix_model::numbers::SeasonNumber::new($season));
368 assert_eq!(model.episode_number, ::flix_model::numbers::EpisodeNumber::new($episode));
369 assert_eq!(model.user_id, $uid);
370 assert_eq!(model.watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc());
371 };
372 ($db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal, $error:ident $(; $($skip:ident),+)?) => {
373 let model = assert_episode!(@insert, $db, $show, $season, $episode, $uid $(; $($skip),+)?)
374 .expect_err("insert");
375
376 assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
377 };
378 (@insert, $db:expr, $show:literal, $season:literal, $episode:literal, $uid:literal $(; $($skip:ident),+)?) => {
379 super::episodes::ActiveModel {
380 show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?),
381 season_number: notsettable!(season_number, ::flix_model::numbers::SeasonNumber::new($season) $(, $($skip),+)?),
382 episode_number: notsettable!(episode_number, ::flix_model::numbers::EpisodeNumber::new($episode) $(, $($skip),+)?),
383 user_id: notsettable!(user_id, $uid $(, $($skip),+)?),
384 watched_date: notsettable!(watched_date, NaiveDate::from_yo_opt($uid, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?),
385 }.insert($db).await
386 };
387 }
388
389 make_info_show!(&db, 1);
390 make_info_season!(&db, 1, 1);
391 make_info_show!(&db, 2);
392 make_info_season!(&db, 2, 1);
393
394 assert_episode!(&db, 1, 1, 1, 1, ForeignKeyViolation);
395 assert_episode!(&db, 2, 1, 1, 1, ForeignKeyViolation);
396 make_info_episode!(&db, 1, 1, 1);
397 make_info_episode!(&db, 2, 1, 1);
398 assert_episode!(&db, 1, 1, 1, 1, Success);
399 assert_episode!(&db, 1, 1, 1, 2, Success);
400 assert_episode!(&db, 2, 1, 1, 1, Success);
401
402 assert_episode!(&db, 1, 1, 1, 1, UniqueViolation);
403
404 assert_episode!(&db, 3, 1, 1, 1, NotNullViolation; show_id);
405 assert_episode!(&db, 4, 1, 1, 1, NotNullViolation; season_number);
406 assert_episode!(&db, 5, 1, 1, 1, NotNullViolation; episode_number);
407 assert_episode!(&db, 6, 1, 1, 1, NotNullViolation; user_id);
408 assert_episode!(&db, 7, 1, 1, 1, NotNullViolation; watched_date);
409 }
410
411 #[cfg(not(miri))]
412 #[tokio::test]
413 #[expect(clippy::missing_panics_doc, reason = "unit test")]
414 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
415 async fn query_seasons() {
416 let db = new_initialized_memory_db().await;
417
418 macro_rules! assert_season {
419 ($db:expr, $show:literal, $season:literal, $uid:literal, Watched) => {
420 _ = assert_season!(@find, $db, $show, $season, $uid)
421 .ok_or(())
422 .expect("is none");
423 };
424 ($db:expr, $show:literal, $season:literal, $uid:literal, Unwatched) => {
425 assert_season!(@find, $db, $show, $season, $uid)
426 .ok_or(())
427 .expect_err("is some");
428 };
429 (@find, $db:expr, $show:literal, $season:literal, $uid:literal) => {
430 super::seasons::Entity::find()
431 .filter(
432 Condition::all()
433 .add(super::seasons::Column::ShowId.eq($show))
434 .add(super::seasons::Column::SeasonNumber.eq($season))
435 .add(super::seasons::Column::UserId.eq($uid)),
436 )
437 .one(&db)
438 .await
439 .expect("find.filter.one")
440 };
441 }
442
443 make_content_library!(&db, 1);
444 make_content_show!(&db, 1, 1, None);
445
446 make_content_season!(&db, 1, 1, 1);
447 assert_season!(&db, 1, 1, 1, Unwatched);
448 make_content_episode!(&db, 1, 1, 1, 1);
449 assert_season!(&db, 1, 1, 1, Unwatched);
450 make_watched_episode!(&db, 1, 1, 1, 1);
451 assert_season!(&db, 1, 1, 1, Watched);
452 assert_season!(&db, 1, 1, 2, Unwatched);
453 make_content_episode!(&db, 1, 1, 1, 2);
454 assert_season!(&db, 1, 1, 1, Unwatched);
455
456 make_content_season!(&db, 1, 1, 2);
457 make_content_episode!(&db, 1, 1, 2, 1);
458 make_content_episode!(&db, 1, 1, 2, 2, >1);
459 make_info_episode!(&db, 1, 2, 3);
460 make_content_episode!(&db, 1, 1, 2, 4);
461 assert_season!(&db, 1, 2, 1, Unwatched);
462 assert_season!(&db, 1, 2, 2, Unwatched);
463 assert_season!(&db, 1, 2, 3, Unwatched);
464 make_watched_episode!(&db, 1, 2, 1, 1);
465 make_watched_episode!(&db, 1, 2, 1, 2);
466 make_watched_episode!(&db, 1, 2, 2, 3);
467 assert_season!(&db, 1, 2, 1, Unwatched);
468 assert_season!(&db, 1, 2, 2, Unwatched);
469 assert_season!(&db, 1, 2, 3, Unwatched);
470 make_watched_episode!(&db, 1, 2, 2, 1);
471 make_watched_episode!(&db, 1, 2, 2, 2);
472 make_watched_episode!(&db, 1, 2, 1, 3);
473 assert_season!(&db, 1, 2, 1, Unwatched);
474 assert_season!(&db, 1, 2, 2, Unwatched);
475 assert_season!(&db, 1, 2, 3, Unwatched);
476 make_watched_episode!(&db, 1, 2, 4, 1);
477 assert_season!(&db, 1, 2, 1, Watched);
478 assert_season!(&db, 1, 2, 2, Unwatched);
479 assert_season!(&db, 1, 2, 3, Unwatched);
480 }
481
482 #[cfg(not(miri))]
483 #[tokio::test]
484 #[expect(clippy::missing_panics_doc, reason = "unit test")]
485 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
486 async fn query_shows() {
487 let db = new_initialized_memory_db().await;
488
489 macro_rules! assert_show {
490 ($db:expr, $show:literal, $uid:literal, Watched) => {
491 _ = assert_show!(@find, $db, $show, $uid)
492 .ok_or(())
493 .expect("is none");
494 };
495 ($db:expr, $show:literal, $uid:literal, Unwatched) => {
496 assert_show!(@find, $db, $show, $uid)
497 .ok_or(())
498 .expect_err("is some");
499 };
500 (@find, $db:expr, $show:literal, $uid:literal) => {
501 super::shows::Entity::find()
502 .filter(
503 Condition::all()
504 .add(super::shows::Column::Id.eq($show))
505 .add(super::shows::Column::UserId.eq($uid)),
506 )
507 .one(&db)
508 .await
509 .expect("find.filter.one")
510 };
511 }
512
513 make_content_library!(&db, 1);
514 make_content_show!(&db, 1, 1, None);
515
516 assert_show!(&db, 1, 1, Unwatched);
517 make_content_season!(&db, 1, 1, 1);
518 assert_show!(&db, 1, 1, Unwatched);
519 make_content_episode!(&db, 1, 1, 1, 1);
520 assert_show!(&db, 1, 1, Unwatched);
521 make_watched_episode!(&db, 1, 1, 1, 1);
522 assert_show!(&db, 1, 1, Watched);
523 assert_show!(&db, 1, 2, Unwatched);
524 make_content_episode!(&db, 1, 1, 1, 2);
525 assert_show!(&db, 1, 1, Unwatched);
526 assert_show!(&db, 1, 2, Unwatched);
527 make_watched_episode!(&db, 1, 1, 2, 1);
528
529 make_content_season!(&db, 1, 1, 2);
530 assert_show!(&db, 1, 1, Unwatched);
531 assert_show!(&db, 1, 2, Unwatched);
532 make_content_episode!(&db, 1, 1, 2, 1);
533 assert_show!(&db, 1, 1, Unwatched);
534 assert_show!(&db, 1, 2, Unwatched);
535 make_watched_episode!(&db, 1, 2, 1, 1);
536 assert_show!(&db, 1, 1, Watched);
537 assert_show!(&db, 1, 2, Unwatched);
538 }
539
540 #[cfg(not(miri))]
541 #[tokio::test]
542 #[expect(clippy::missing_panics_doc, reason = "unit test")]
543 #[expect(clippy::default_numeric_fallback, reason = "unit test")]
544 async fn query_collections() {
545 let db = new_initialized_memory_db().await;
546
547 macro_rules! assert_collection {
548 ($db:expr, $id:literal, $uid:literal, Watched) => {
549 _ = assert_collection!(@find, $db, $id, $uid)
550 .ok_or(())
551 .expect("is none");
552 };
553 ($db:expr, $id:literal, $uid:literal, Unwatched) => {
554 assert_collection!(@find, $db, $id, $uid)
555 .ok_or(())
556 .expect_err("is some");
557 };
558 (@find, $db:expr, $id:literal, $uid:literal) => {
559 super::collections::Entity::find()
560 .filter(
561 Condition::all()
562 .add(super::collections::Column::Id.eq($id))
563 .add(super::collections::Column::UserId.eq($uid)),
564 )
565 .one(&db)
566 .await
567 .expect("find.filter.one")
568 };
569 }
570
571 make_content_library!(&db, 1);
572 make_content_collection!(&db, 1, 1, None);
573 assert_collection!(&db, 1, 1, Unwatched);
574
575 make_content_movie!(&db, 1, 1, Some(1));
576 assert_collection!(&db, 1, 1, Unwatched);
577 make_info_movie!(&db, 9999);
578 make_watched_movie!(&db, 9999, 1);
579 assert_collection!(&db, 1, 1, Unwatched);
580 make_watched_movie!(&db, 1, 1);
581 assert_collection!(&db, 1, 1, Watched);
582
583 make_content_collection!(&db, 1, 2, Some(1));
584 assert_collection!(&db, 1, 1, Watched);
585 assert_collection!(&db, 2, 1, Unwatched);
586 make_content_movie!(&db, 1, 2, Some(2));
587 assert_collection!(&db, 1, 1, Unwatched);
588 assert_collection!(&db, 2, 1, Unwatched);
589 make_watched_movie!(&db, 2, 1);
590 assert_collection!(&db, 1, 1, Watched);
591 assert_collection!(&db, 2, 1, Watched);
592
593 make_content_show!(&db, 1, 1, Some(2));
594 assert_collection!(&db, 1, 1, Unwatched);
595 assert_collection!(&db, 2, 1, Unwatched);
596 make_content_season!(&db, 1, 1, 1);
597 make_content_episode!(&db, 1, 1, 1, 1);
598 make_watched_episode!(&db, 1, 1, 1, 1);
599 assert_collection!(&db, 1, 1, Watched);
600 assert_collection!(&db, 2, 1, Watched);
601 }
602}