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