Base de données standardisée
Le projet Charly utilise une base de données Supabase (PostgreSQL) gérée avec Drizzle ORM, une bibliothèque légère et typée pour définir et interroger les schémas. Les schémas sont définis dans packages/db/src/schema/ et re-exportés avec Zod dans packages/db/src/zod/. Cette page explique l'approche codebase first adoptée pour la gestion de la base de données, décrit les tables actuelles avec une justification de leurs champs, et fournit un tutoriel rapide sur l'utilisation de Drizzle, en mettant l'accent sur les commandes db:push et db:studio.
Approche codebase first
Dans Charly, nous utilisons une approche codebase first pour gérer la base de données. Cela signifie que les schémas sont définis directement dans le code (dans packages/db/src/schema/ avec Drizzle) et servent de source de vérité pour la structure de la base de données. Contrairement à une approche database first (où la base de données est modifiée manuellement et le code est généré à partir de celle-ci), ou migration first (où les migrations sont écrites manuellement), l'approche codebase first privilégie :
- Définition dans le code : Les tables, colonnes, et relations sont définis en TypeScript (ex.
pgTabledansschema/events.ts), garantissant une typisation forte. - Synchronisation automatique : Les outils comme
db:pushappliquent les schémas directement à la base de données, réduisant les erreurs manuelles. - Validation intégrée : Les schémas sont associés à des validations Zod (dans
zod/*.ts) pour garantir la cohérence des données. - Facilité de maintenance : Les modifications sont effectuées dans le code, puis propagées à la base de données, simplifiant les itérations.
Cette approche est idéale pour Charly car elle :
- Aligne la base de données avec le code TypeScript, assurant une cohérence avec le frontend et le backend.
- Réduit la complexité des migrations manuelles, particulièrement en phase de développement rapide.
- Facilite la collaboration grâce à des schémas versionnés dans Git.
Description des tables
Les tables sont définies dans packages/db/src/schema/ et représentent les entités principales du projet : events, locations, categories, organizers, et les tables d'authentification (user, session, account, verification). Chaque table utilise Row-Level Security (RLS) pour contrôler l'accès aux données. Voici une description de chaque table avec une justification des champs.
Table events
Définition dans schema/events.ts :
export const events = pgTable("events", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
description: text("description"),
startDate: timestamp("start_date").notNull(),
endDate: timestamp("end_date"),
eventPrice: numeric("event_price"),
url: text("url"),
isRecurring: boolean("is_recurring").default(false),
maxAttendees: integer("max_attendees"),
currentAttendees: integer("current_attendees").default(0),
status: eventStatus("status").default("draft"),
ticketUrl: text("ticket_url"),
coverImage: text("cover_image"),
videoUrl: text("video_url"),
picturesUrls: text("pictures_urls").array(),
popularityScore: numeric("popularity_score"),
priceRange: jsonb("price_range").$type<{ min: number; max: number }>(),
relevantTags: text("relevant_tags").array(),
ageRestriction: integer("age_restriction"),
accessibilityInfo: text("accessibility_info"),
cancellationPolicy: text("cancellation_policy"),
locationId: uuid("location_id").references(() => locations.id, {
onDelete: "cascade",
}),
organizerEmail: text("organizer_email").references(() => organizers.email, {
onDelete: "cascade",
}),
categorySlug: text("category_slug").references(() => categories.slug, {
onDelete: "cascade",
}),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}).enableRLS();Justification des champs :
id: Identifiant unique (UUID) pour chaque événement, généré automatiquement.name,description: Informations de base,namerequis pour l'affichage.startDate,endDate: Dates de début et de fin,startDaterequis pour l'agenda.eventPrice,priceRange: Prix fixe ou plage de prix pour la billetterie.url,ticketUrl: Liens vers le site de l'événement ou l'achat de billets.isRecurring,maxAttendees,currentAttendees: Gestion de la récurrence et de la capacité.status: État de l'événement (draft,published,cancelled,sold_out) via un enum.coverImage,videoUrl,picturesUrls: Médias pour enrichir l'affichage.popularityScore,relevantTags: Métadonnées pour la recherche et le classement.ageRestriction,accessibilityInfo,cancellationPolicy: Informations pratiques pour les utilisateurs.locationId,organizerEmail,categorySlug: Clés étrangères pour lier aux tables correspondantes, avec suppression en cascade.createdAt,updatedAt: Suivi des modifications pour l'audit.
Table locations
Définition dans schema/locations.ts :
export const locations = pgTable("locations", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
street: text("street"),
city: text("city"),
state: text("state"),
postalCode: text("postal_code"),
country: text("country"),
latitude: numeric("latitude"),
longitude: numeric("longitude"),
venueType: venueType("venue_type"),
openingHours: jsonb("opening_hours").$type<OpeningHour[]>(),
website: text("website"),
phone: text("phone"),
capacity: integer("capacity"),
amenities: text("amenities").array(),
accessibilityFeatures: text("accessibility_features").array(),
parkingAvailable: boolean("parking_available").default(false),
publicTransportAccess: text("public_transport_access"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}).enableRLS();Justification des champs :
id: Identifiant unique (UUID) pour chaque lieu.name: Nom requis pour l'affichage.street,city,state,postalCode,country: Adresse complète pour la localisation.latitude,longitude: Coordonnées pour l'intégration avec des cartes (ex. Google Maps).venueType: Type de lieu (indoor,outdoor, etc.) via un enum pour le filtrage.openingHours: Horaires d'ouverture au format JSON pour une flexibilité maximale.website,phone: Informations de contact.capacity: Capacité maximale pour les événements.amenities,accessibilityFeatures: Listes pour décrire les équipements et l'accessibilité.parkingAvailable,publicTransportAccess: Informations pratiques pour les visiteurs.createdAt,updatedAt: Suivi des modifications.
Table categories
Définition dans schema/categories.ts :
export const categories = pgTable("categories", {
slug: text("slug").primaryKey(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}).enableRLS();Justification des champs :
slug: Identifiant unique (chaîne) utilisé comme clé primaire et pour les URLs.name: Nom affiché pour la catégorie (ex. "Concert", "Conférence").createdAt,updatedAt: Suivi des modifications.
Table organizers
Définition dans schema/organizers.ts :
export const organizers = pgTable("organizers", {
email: text("email").primaryKey(),
name: text("name").notNull().unique(),
website: text("website"),
phone: text("phone"),
description: text("description"),
logo: text("logo"),
socialMediaLinks: text("social_media_links").array(),
verifiedStatus: verifiedStatus("verified_status").default("pending"),
organizationType: organizationType("organization_type"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
}).enableRLS();Justification des champs :
email: Clé primaire pour identifier l'organisateur, utilisé comme référence dansevents.name: Nom unique requis pour l'affichage.website,phone,description: Informations de contact et de présentation.logo,socialMediaLinks: Médias pour enrichir le profil.verifiedStatus: Statut de vérification (pending,verified,rejected) via un enum.organizationType: Type d'organisation (non_profit,for_profit, etc.) via un enum.createdAt,updatedAt: Suivi des modifications.
Tables d'authentification (user, session, account, verification)
Définition dans schema/auth.ts :
export const user = pgTable("user", {
id: uuid("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull(),
image: text("image"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
}).enableRLS();
export const session = pgTable("session", {
id: uuid("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
}).enableRLS();
export const account = pgTable("account", {
id: uuid("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
}).enableRLS();
export const verification = pgTable("verification", {
id: uuid("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at"),
updatedAt: timestamp("updated_at"),
}).enableRLS();Justification des champs :
user: Stocke les informations de base des utilisateurs (id,name,email,emailVerified,image) pour l'authentification et le profil.session: Gère les sessions actives (token,expiresAt,userId) avec des métadonnées (ipAddress,userAgent) pour la sécurité.account: Supporte l'authentification via des fournisseurs externes (providerId,accessToken, etc.) ou par mot de passe.verification: Gère les tokens de vérification (ex. pour la confirmation d'email) avec une date d'expiration.- Clés étrangères :
session.userIdetaccount.userIdlient aux utilisateurs avec suppression en cascade. createdAt,updatedAt: Suivi des modifications pour toutes les tables.
Tutoriel rapide : Utilisation de Drizzle
Drizzle est utilisé pour définir les schémas, générer des migrations, et interroger la base de données. Dans Charly, nous privilégions db:push pour synchroniser les schémas en développement et db:studio pour explorer la base de données. Voici un guide rapide pour commencer.
Prérequis
- Une instance PostgreSQL en cours d'exécution.
- Le fichier
packages/db/.envconfiguré avecDATABASE_URL(voir Installation). - Les dépendances installées avec
bun installà la racine du monorepo.
Étape 1 : Synchroniser les schémas avec db:push
La commande db:push applique les schémas définis dans packages/db/src/schema/ à la base de données, en créant ou modifiant les tables selon les besoins.
Assurez-vous que
DATABASE_URLest défini danspackages/db/.env:bashDATABASE_URL=postgresql://username:password@localhost:5432/postgresExécutez la commande à la racine du monorepo :
bashbun run db:pushCela synchronise toutes les tables (
events,locations, etc.) avec la base de données. Drizzle détecte les différences entre le schéma actuel et la base de données, puis applique les modifications nécessaires (ajout de tables, colonnes, etc.).
Étape 2 : Explorer la base de données avec db:studio
La commande db:studio lance une interface web pour visualiser et interagir avec la base de données.
Exécutez la commande :
bashbun run db:studioOuvrez l'URL affichée dans le terminal (généralement
http://localhost:4983) dans votre navigateur.Utilisez l'interface pour :
- Visualiser les tables et leurs colonnes.
- Exécuter des requêtes SQL pour tester les données.
- Vérifier les relations (ex. clés étrangères entre
eventsetlocations).
Étape 3 : Ajouter une nouvelle table (exemple)
Pour illustrer, ajoutons une table reviews pour stocker les avis sur les événements.
Créez un fichier
packages/db/src/schema/reviews.ts:tsimport { pgTable, uuid, text, integer, timestamp, } from "drizzle-orm/pg-core"; import { events } from "./events"; export const reviews = pgTable("reviews", { id: uuid("id").defaultRandom().primaryKey(), eventId: uuid("event_id") .notNull() .references(() => events.id, { onDelete: "cascade" }), rating: integer("rating").notNull(), comment: text("comment"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }).enableRLS();Ajoutez la table à
packages/db/src/schema/index.ts:tsexport * from "./reviews.ts";Synchronisez la base de données :
bashbun run db:pushVérifiez la nouvelle table dans
db:studio:bashbun run db:studio(Facultatif) Créez une validation Zod dans
packages/db/src/zod/reviews.ts:tsimport { createSelectSchema, createInsertSchema } from "drizzle-zod"; import { reviews } from "../schema/reviews"; import { z } from "zod"; export const reviewsSelectSchema = createSelectSchema(reviews); export const reviewsInsertSchema = createInsertSchema(reviews); export type ReviewsSelect = z.infer<typeof reviewsSelectSchema>; export type ReviewsInsert = z.infer<typeof reviewsInsertSchema>;
Bonnes pratiques
- Utilisez
db:pushen développement : Parfait pour itérer rapidement, mais passez àdb:migratepour la production. - Vérifiez avec
db:studio: Utilisez l'interface pour valider les schémas et tester les données. - Ajoutez des validations Zod : Chaque nouvelle table doit avoir des schémas Zod correspondants pour la validation.
- Activez RLS : Assurez-vous que
.enableRLS()est appelé sur chaque table pour la sécurité. - Testez les relations : Vérifiez les clés étrangères et les suppressions en cascade dans
db:studio.