From 2f47be13051cb551094709beb740b596c6a582a6 Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Sat, 14 Mar 2026 06:53:02 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20TypeORM=20entities=20=E2=80=94=20User,?= =?UTF-8?q?=20Role,=20SubscriptionPlan,=20Video,=20Playlist=20+=20relation?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/config/data-source.ts | 12 ++++- backend/src/entities/Playlist.ts | 47 +++++++++++++++++++ backend/src/entities/PlaylistShare.ts | 46 +++++++++++++++++++ backend/src/entities/PlaylistVideo.ts | 24 ++++++++++ backend/src/entities/Role.ts | 19 ++++++++ backend/src/entities/SubscriptionPlan.ts | 35 ++++++++++++++ backend/src/entities/User.ts | 45 ++++++++++++++++++ backend/src/entities/UserRole.ts | 20 ++++++++ backend/src/entities/UserSubscription.ts | 41 +++++++++++++++++ backend/src/entities/Video.ts | 58 ++++++++++++++++++++++++ 10 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 backend/src/entities/Playlist.ts create mode 100644 backend/src/entities/PlaylistShare.ts create mode 100644 backend/src/entities/PlaylistVideo.ts create mode 100644 backend/src/entities/Role.ts create mode 100644 backend/src/entities/SubscriptionPlan.ts create mode 100644 backend/src/entities/User.ts create mode 100644 backend/src/entities/UserRole.ts create mode 100644 backend/src/entities/UserSubscription.ts create mode 100644 backend/src/entities/Video.ts diff --git a/backend/src/config/data-source.ts b/backend/src/config/data-source.ts index 97d4d02..cc15a90 100644 --- a/backend/src/config/data-source.ts +++ b/backend/src/config/data-source.ts @@ -10,7 +10,17 @@ export const AppDataSource = new DataSource({ database: process.env.DB_NAME ?? "originsdigital", synchronize: false, logging: process.env.NODE_ENV === "development", - entities: ["src/entities/**/*.ts"], + entities: [ + require("../entities/User").User, + require("../entities/Role").Role, + require("../entities/UserRole").UserRole, + require("../entities/SubscriptionPlan").SubscriptionPlan, + require("../entities/UserSubscription").UserSubscription, + require("../entities/Video").Video, + require("../entities/Playlist").Playlist, + require("../entities/PlaylistVideo").PlaylistVideo, + require("../entities/PlaylistShare").PlaylistShare, + ], migrations: ["src/migrations/**/*.ts"], subscribers: [], }); diff --git a/backend/src/entities/Playlist.ts b/backend/src/entities/Playlist.ts new file mode 100644 index 0000000..25d112b --- /dev/null +++ b/backend/src/entities/Playlist.ts @@ -0,0 +1,47 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, OneToMany, JoinColumn, + CreateDateColumn, UpdateDateColumn, +} from "typeorm"; +import { User } from "./User"; +import { PlaylistVideo } from "./PlaylistVideo"; +import { PlaylistShare } from "./PlaylistShare"; + +export type PlaylistVisibility = "private" | "shared" | "public"; + +@Entity("playlists") +export class Playlist { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "uuid" }) + ownerId!: string; + + @Column({ type: "varchar", length: 255 }) + title!: string; + + @Column({ type: "text", nullable: true }) + description!: string | null; + + // private → visible uniquement par le propriétaire + // shared → visible par les utilisateurs invités via PlaylistShare + // public → visible par tous + @Column({ type: "enum", enum: ["private", "shared", "public"], default: "private" }) + visibility!: PlaylistVisibility; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ManyToOne(() => User, (user) => user.playlists, { onDelete: "CASCADE" }) + @JoinColumn({ name: "ownerId" }) + owner!: User; + + @OneToMany(() => PlaylistVideo, (pv) => pv.playlist) + playlistVideos!: PlaylistVideo[]; + + @OneToMany(() => PlaylistShare, (share) => share.playlist) + shares!: PlaylistShare[]; +} diff --git a/backend/src/entities/PlaylistShare.ts b/backend/src/entities/PlaylistShare.ts new file mode 100644 index 0000000..89bc6e0 --- /dev/null +++ b/backend/src/entities/PlaylistShare.ts @@ -0,0 +1,46 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, +} from "typeorm"; +import { Playlist } from "./Playlist"; +import { User } from "./User"; + +export type SharePermission = "view" | "edit"; +export type ShareStatus = "pending" | "active" | "revoked"; + +@Entity("playlist_shares") +export class PlaylistShare { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "uuid" }) + playlistId!: string; + + @Column({ type: "uuid" }) + userId!: string; + + // Permissions gérées par le propriétaire de la playlist + @Column({ type: "enum", enum: ["view", "edit"], default: "view" }) + permission!: SharePermission; + + // Statut géré par le propriétaire : + // pending → invitation envoyée, pas encore acceptée + // active → invité a accepté + // revoked → propriétaire a révoqué l'accès + @Column({ type: "enum", enum: ["pending", "active", "revoked"], default: "pending" }) + status!: ShareStatus; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @ManyToOne(() => Playlist, (playlist) => playlist.shares, { onDelete: "CASCADE" }) + @JoinColumn({ name: "playlistId" }) + playlist!: Playlist; + + @ManyToOne(() => User, (user) => user.playlistShares, { onDelete: "CASCADE" }) + @JoinColumn({ name: "userId" }) + user!: User; +} diff --git a/backend/src/entities/PlaylistVideo.ts b/backend/src/entities/PlaylistVideo.ts new file mode 100644 index 0000000..889ce5c --- /dev/null +++ b/backend/src/entities/PlaylistVideo.ts @@ -0,0 +1,24 @@ +import { Entity, ManyToOne, JoinColumn, PrimaryColumn, Column } from "typeorm"; +import { Playlist } from "./Playlist"; +import { Video } from "./Video"; + +@Entity("playlist_videos") +export class PlaylistVideo { + @PrimaryColumn({ type: "uuid" }) + playlistId!: string; + + @PrimaryColumn({ type: "uuid" }) + videoId!: string; + + // Ordre d'affichage dans la playlist — géré par le propriétaire ou les éditeurs + @Column({ type: "int", unsigned: true, default: 0 }) + position!: number; + + @ManyToOne(() => Playlist, (playlist) => playlist.playlistVideos, { onDelete: "CASCADE" }) + @JoinColumn({ name: "playlistId" }) + playlist!: Playlist; + + @ManyToOne(() => Video, (video) => video.playlistVideos, { onDelete: "CASCADE" }) + @JoinColumn({ name: "videoId" }) + video!: Video; +} diff --git a/backend/src/entities/Role.ts b/backend/src/entities/Role.ts new file mode 100644 index 0000000..cc5b700 --- /dev/null +++ b/backend/src/entities/Role.ts @@ -0,0 +1,19 @@ +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"; +import { UserRole } from "./UserRole"; + +export type RoleSlug = "user" | "moderator" | "admin" | "super_admin"; + +@Entity("roles") +export class Role { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 50, unique: true }) + slug!: RoleSlug; + + @Column({ type: "varchar", length: 100 }) + name!: string; + + @OneToMany(() => UserRole, (userRole) => userRole.role) + userRoles!: UserRole[]; +} diff --git a/backend/src/entities/SubscriptionPlan.ts b/backend/src/entities/SubscriptionPlan.ts new file mode 100644 index 0000000..4776c17 --- /dev/null +++ b/backend/src/entities/SubscriptionPlan.ts @@ -0,0 +1,35 @@ +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"; +import { UserSubscription } from "./UserSubscription"; + +export type PlanSlug = "free" | "basic" | "pro" | "enterprise"; + +@Entity("subscription_plans") +export class SubscriptionPlan { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 50, unique: true }) + slug!: PlanSlug; + + @Column({ type: "varchar", length: 100 }) + name!: string; + + // Niveau d'accès — comparaison entière : userPlanLevel >= video.requiredLevel + // free=0, basic=1, pro=2, enterprise=3 + @Column({ type: "tinyint", unsigned: true, default: 0 }) + level!: number; + + // Prix en centimes (0 = gratuit). Évite les flottants en DB. + @Column({ type: "int", unsigned: true, default: 0 }) + priceInCents!: number; + + // Features libres — permet la personnalisation marque blanche sans migration + @Column({ type: "json", nullable: true }) + features!: Record | null; + + @Column({ type: "boolean", default: true }) + isActive!: boolean; + + @OneToMany(() => UserSubscription, (sub) => sub.plan) + subscriptions!: UserSubscription[]; +} diff --git a/backend/src/entities/User.ts b/backend/src/entities/User.ts new file mode 100644 index 0000000..56c1035 --- /dev/null +++ b/backend/src/entities/User.ts @@ -0,0 +1,45 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + OneToMany, CreateDateColumn, UpdateDateColumn, +} from "typeorm"; +import { UserRole } from "./UserRole"; +import { UserSubscription } from "./UserSubscription"; +import { Playlist } from "./Playlist"; +import { PlaylistShare } from "./PlaylistShare"; + +@Entity("users") +export class User { + @PrimaryGeneratedColumn("uuid") + id!: string; + + // Identifiant SuperOAuth — clé de réconciliation avec le service auth + @Column({ type: "varchar", length: 255, unique: true }) + superOAuthId!: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + email!: string | null; + + @Column({ type: "varchar", length: 100 }) + nickname!: string; + + @Column({ type: "boolean", default: true }) + isActive!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @OneToMany(() => UserRole, (userRole) => userRole.user) + userRoles!: UserRole[]; + + @OneToMany(() => UserSubscription, (sub) => sub.user) + subscriptions!: UserSubscription[]; + + @OneToMany(() => Playlist, (playlist) => playlist.owner) + playlists!: Playlist[]; + + @OneToMany(() => PlaylistShare, (share) => share.user) + playlistShares!: PlaylistShare[]; +} diff --git a/backend/src/entities/UserRole.ts b/backend/src/entities/UserRole.ts new file mode 100644 index 0000000..e3cff85 --- /dev/null +++ b/backend/src/entities/UserRole.ts @@ -0,0 +1,20 @@ +import { Entity, ManyToOne, JoinColumn, PrimaryColumn } from "typeorm"; +import { User } from "./User"; +import { Role } from "./Role"; + +@Entity("user_roles") +export class UserRole { + @PrimaryColumn({ type: "uuid" }) + userId!: string; + + @PrimaryColumn({ type: "uuid" }) + roleId!: string; + + @ManyToOne(() => User, (user) => user.userRoles, { onDelete: "CASCADE" }) + @JoinColumn({ name: "userId" }) + user!: User; + + @ManyToOne(() => Role, (role) => role.userRoles, { onDelete: "CASCADE" }) + @JoinColumn({ name: "roleId" }) + role!: Role; +} diff --git a/backend/src/entities/UserSubscription.ts b/backend/src/entities/UserSubscription.ts new file mode 100644 index 0000000..0372f67 --- /dev/null +++ b/backend/src/entities/UserSubscription.ts @@ -0,0 +1,41 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, JoinColumn, CreateDateColumn, +} from "typeorm"; +import { User } from "./User"; +import { SubscriptionPlan } from "./SubscriptionPlan"; + +export type SubscriptionStatus = "active" | "expired" | "cancelled" | "trial"; + +@Entity("user_subscriptions") +export class UserSubscription { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "uuid" }) + userId!: string; + + @Column({ type: "uuid" }) + planId!: string; + + @Column({ type: "enum", enum: ["active", "expired", "cancelled", "trial"], default: "active" }) + status!: SubscriptionStatus; + + @Column({ type: "datetime" }) + startsAt!: Date; + + // null = pas d'expiration (plan gratuit permanent) + @Column({ type: "datetime", nullable: true }) + endsAt!: Date | null; + + @CreateDateColumn() + createdAt!: Date; + + @ManyToOne(() => User, (user) => user.subscriptions, { onDelete: "CASCADE" }) + @JoinColumn({ name: "userId" }) + user!: User; + + @ManyToOne(() => SubscriptionPlan, (plan) => plan.subscriptions) + @JoinColumn({ name: "planId" }) + plan!: SubscriptionPlan; +} diff --git a/backend/src/entities/Video.ts b/backend/src/entities/Video.ts new file mode 100644 index 0000000..60f7e38 --- /dev/null +++ b/backend/src/entities/Video.ts @@ -0,0 +1,58 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + OneToMany, CreateDateColumn, UpdateDateColumn, +} from "typeorm"; +import { PlaylistVideo } from "./PlaylistVideo"; + +export type StorageType = "youtube" | "s3" | "local" | "external"; + +@Entity("videos") +export class Video { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 255 }) + title!: string; + + @Column({ type: "text", nullable: true }) + description!: string | null; + + @Column({ type: "varchar", length: 500, nullable: true }) + thumbnailUrl!: string | null; + + // Durée en secondes + @Column({ type: "int", unsigned: true, nullable: true }) + duration!: number | null; + + @Column({ type: "enum", enum: ["youtube", "s3", "local", "external"] }) + storageType!: StorageType; + + // Référence flexible selon storageType : + // youtube → videoId YouTube (ex: "dQw4w9WgXcQ") + // s3 → clé S3 (ex: "videos/2026/my-video.mp4") + // local → chemin relatif (ex: "uploads/my-video.mp4") + // external → URL complète + @Column({ type: "varchar", length: 500 }) + storageKey!: string; + + // Niveau de plan minimum requis pour accéder à cette vidéo + // 0 = libre, 1 = basic, 2 = pro, 3 = enterprise + // Comparaison : userSubscription.plan.level >= video.requiredLevel + @Column({ type: "tinyint", unsigned: true, default: 0 }) + requiredLevel!: number; + + @Column({ type: "boolean", default: false }) + isPublished!: boolean; + + @Column({ type: "datetime", nullable: true }) + publishedAt!: Date | null; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @OneToMany(() => PlaylistVideo, (pv) => pv.video) + playlistVideos!: PlaylistVideo[]; +}