feat: TypeORM entities — User, Role, SubscriptionPlan, Video, Playlist + relations

This commit is contained in:
2026-03-14 06:53:02 +01:00
parent 4f3c0e6433
commit 2f47be1305
10 changed files with 346 additions and 1 deletions

View File

@@ -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: [],
});

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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<string, unknown> | null;
@Column({ type: "boolean", default: true })
isActive!: boolean;
@OneToMany(() => UserSubscription, (sub) => sub.plan)
subscriptions!: UserSubscription[];
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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[];
}