feat: TypeORM entities — User, Role, SubscriptionPlan, Video, Playlist + relations
This commit is contained in:
@@ -10,7 +10,17 @@ export const AppDataSource = new DataSource({
|
|||||||
database: process.env.DB_NAME ?? "originsdigital",
|
database: process.env.DB_NAME ?? "originsdigital",
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: process.env.NODE_ENV === "development",
|
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"],
|
migrations: ["src/migrations/**/*.ts"],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
});
|
});
|
||||||
|
|||||||
47
backend/src/entities/Playlist.ts
Normal file
47
backend/src/entities/Playlist.ts
Normal 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[];
|
||||||
|
}
|
||||||
46
backend/src/entities/PlaylistShare.ts
Normal file
46
backend/src/entities/PlaylistShare.ts
Normal 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;
|
||||||
|
}
|
||||||
24
backend/src/entities/PlaylistVideo.ts
Normal file
24
backend/src/entities/PlaylistVideo.ts
Normal 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;
|
||||||
|
}
|
||||||
19
backend/src/entities/Role.ts
Normal file
19
backend/src/entities/Role.ts
Normal 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[];
|
||||||
|
}
|
||||||
35
backend/src/entities/SubscriptionPlan.ts
Normal file
35
backend/src/entities/SubscriptionPlan.ts
Normal 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[];
|
||||||
|
}
|
||||||
45
backend/src/entities/User.ts
Normal file
45
backend/src/entities/User.ts
Normal 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[];
|
||||||
|
}
|
||||||
20
backend/src/entities/UserRole.ts
Normal file
20
backend/src/entities/UserRole.ts
Normal 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;
|
||||||
|
}
|
||||||
41
backend/src/entities/UserSubscription.ts
Normal file
41
backend/src/entities/UserSubscription.ts
Normal 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;
|
||||||
|
}
|
||||||
58
backend/src/entities/Video.ts
Normal file
58
backend/src/entities/Video.ts
Normal 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[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user