feat(manadeck): migrate backend from Supabase to PostgreSQL with Drizzle ORM

Phase 3 of ManaDeck database migration:

Database Package (@manacore/manadeck-database):
- Configure package for NodeNext module resolution with .js extensions
- Add build script and proper exports for ESM/CJS compatibility
- Export schema types and Drizzle utilities

Backend Migration:
- Add DatabaseModule with singleton database provider
- Create repository layer with Drizzle ORM:
  - DeckRepository: CRUD operations for decks
  - CardRepository: CRUD operations for cards
  - UserStatsRepository: Stats and leaderboard queries
  - DeckTemplateRepository: Template management
- Update ApiController to use repositories for all database operations
- Update PublicController to use repositories for featured decks, leaderboard, templates
- Add DATABASE_URL environment variable support

The backend now uses PostgreSQL via Drizzle ORM instead of Supabase SDK
for database operations. Supabase is still used for auth (via Mana Core)
and edge functions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 02:39:39 +01:00
parent 84f9343d25
commit 1530efa936
26 changed files with 3448 additions and 253 deletions

View file

@ -1,6 +1,6 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import * as schema from './schema/index.js';
// Singleton instance for the database client
let dbInstance: ReturnType<typeof drizzle<typeof schema>> | null = null;

View file

@ -1,7 +1,7 @@
// Main entry point for @manacore/manadeck-database
// Export database client utilities
export { createClient, getDb, closeDb, type Database } from './client';
export { createClient, getDb, closeDb, type Database } from './client.js';
// Export Drizzle utilities
export {
@ -28,7 +28,7 @@ export {
avg,
min,
max,
} from './client';
} from './client.js';
// Export all schemas and types
export * from './schema';
export * from './schema/index.js';

View file

@ -11,7 +11,7 @@
*/
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
import { getDb, closeDb } from './client';
import { getDb, closeDb } from './client.js';
import {
decks,
cards,
@ -21,7 +21,7 @@ import {
aiGenerations,
userStats,
dailyProgress,
} from './schema';
} from './schema/index.js';
// Initialize Supabase client
const supabaseUrl = process.env.SUPABASE_URL;

View file

@ -1,5 +1,5 @@
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { createClient } from './client';
import { createClient } from './client.js';
import path from 'path';
async function runMigrations() {

View file

@ -9,7 +9,7 @@ import {
pgEnum,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { decks } from './decks';
import { decks } from './decks.js';
// AI generation status enum
export const aiGenerationStatusEnum = pgEnum('ai_generation_status', [

View file

@ -9,7 +9,7 @@ import {
unique,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { cards } from './cards';
import { cards } from './cards.js';
// Progress status enum (SM-2 algorithm states)
export const progressStatusEnum = pgEnum('progress_status', [

View file

@ -11,8 +11,8 @@ import {
pgEnum,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { decks } from './decks';
import { cardProgress } from './cardProgress';
import { decks } from './decks.js';
import { cardProgress } from './cardProgress.js';
// Card type enum
export const cardTypeEnum = pgEnum('card_type', ['text', 'flashcard', 'quiz', 'mixed']);

View file

@ -9,9 +9,9 @@ import {
index,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { cards } from './cards';
import { studySessions } from './studySessions';
import { aiGenerations } from './aiGenerations';
import { cards } from './cards.js';
import { studySessions } from './studySessions.js';
import { aiGenerations } from './aiGenerations.js';
export const decks = pgTable(
'decks',

View file

@ -1,16 +1,16 @@
// Export all schemas
export * from './decks';
export * from './cards';
export * from './studySessions';
export * from './cardProgress';
export * from './deckTemplates';
export * from './aiGenerations';
export * from './userStats';
export * from './dailyProgress';
export * from './decks.js';
export * from './cards.js';
export * from './studySessions.js';
export * from './cardProgress.js';
export * from './deckTemplates.js';
export * from './aiGenerations.js';
export * from './userStats.js';
export * from './dailyProgress.js';
// Re-export relations for use with Drizzle query builder
export { decksRelations } from './decks';
export { cardsRelations } from './cards';
export { studySessionsRelations } from './studySessions';
export { cardProgressRelations } from './cardProgress';
export { aiGenerationsRelations } from './aiGenerations';
export { decksRelations } from './decks.js';
export { cardsRelations } from './cards.js';
export { studySessionsRelations } from './studySessions.js';
export { cardProgressRelations } from './cardProgress.js';
export { aiGenerationsRelations } from './aiGenerations.js';

View file

@ -8,7 +8,7 @@ import {
pgEnum,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { decks } from './decks';
import { decks } from './decks.js';
// Study mode enum
export const studyModeEnum = pgEnum('study_mode', ['all', 'new', 'review', 'favorites', 'random']);

View file

@ -1,5 +1,5 @@
import { getDb, closeDb } from './client';
import { deckTemplates } from './schema';
import { getDb, closeDb } from './client.js';
import { deckTemplates } from './schema/index.js';
/**
* Seed the database with initial data

View file

@ -3,7 +3,7 @@
* Usage: pnpm db:test
*/
import { getDb, closeDb, sql } from './client';
import { getDb, closeDb, sql } from './client.js';
async function testConnection() {
console.log('Testing database connection...\n');