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

@ -20,6 +20,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@manacore/manadeck-database": "workspace:*",
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.1",

View file

@ -11,6 +11,13 @@ import { PublicController } from './controllers/public.controller';
import { HealthController } from './controllers/health.controller';
import { SupabaseService } from './services/supabase.service';
import { validationSchema } from './config/validation.schema';
import {
DatabaseModule,
DeckRepository,
CardRepository,
UserStatsRepository,
DeckTemplateRepository,
} from './database';
@Module({
imports: [
@ -46,6 +53,9 @@ import { validationSchema } from './config/validation.schema';
// Health checks
TerminusModule,
HttpModule,
// Database (Drizzle/PostgreSQL)
DatabaseModule,
],
controllers: [
AppController,
@ -53,7 +63,15 @@ import { validationSchema } from './config/validation.schema';
PublicController,
HealthController,
],
providers: [AppService, SupabaseService],
providers: [
AppService,
SupabaseService,
// Database repositories
DeckRepository,
CardRepository,
UserStatsRepository,
DeckTemplateRepository,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {

View file

@ -4,6 +4,7 @@ import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
import { CreditClientService } from '@mana-core/nestjs-integration';
import { CreditOperationType, getCreditCost, getOperationDescription } from '../config/credit-operations';
import { SupabaseService } from '../services/supabase.service';
import { DeckRepository, CardRepository, UserStatsRepository } from '../database';
@Controller('api')
@UseGuards(AuthGuard)
@ -13,6 +14,9 @@ export class ApiController {
constructor(
private readonly creditClient: CreditClientService,
private readonly supabaseService: SupabaseService,
private readonly deckRepository: DeckRepository,
private readonly cardRepository: CardRepository,
private readonly userStatsRepository: UserStatsRepository,
) {}
@Get('profile')
@ -58,12 +62,13 @@ export class ApiController {
}
@Get('decks')
getUserDecks(@CurrentUser() user: any) {
// This would fetch from Supabase in a real implementation
async getUserDecks(@CurrentUser() user: any) {
this.logger.log(`Getting decks for user: ${user.sub}`);
const decks = await this.deckRepository.findByUserId(user.sub);
return {
userId: user.sub,
decks: [],
message: 'Fetch user decks from Supabase',
decks,
count: decks.length,
};
}
@ -96,24 +101,27 @@ export class ApiController {
});
}
// 2. Perform the operation (create deck in Supabase)
// TODO: Implement actual deck creation logic with Supabase
const newDeck = {
id: `deck_${Date.now()}`,
...deckData,
// 2. Perform the operation (create deck in PostgreSQL)
const newDeck = await this.deckRepository.create({
userId: user.sub,
createdAt: new Date(),
};
title: deckData.name || deckData.title || 'Untitled Deck',
description: deckData.description,
coverImageUrl: deckData.coverImageUrl,
isPublic: deckData.isPublic ?? false,
settings: deckData.settings || {},
tags: deckData.tags || [],
metadata: deckData.metadata || {},
});
// 3. Success - Consume credits
await this.creditClient.consumeCredits(
user.sub,
operationType,
creditCost,
`Created deck: ${deckData.name || 'Unnamed Deck'}`,
`Created deck: ${newDeck.title}`,
{
deckId: newDeck.id,
deckName: deckData.name,
deckName: newDeck.title,
},
);
@ -260,59 +268,115 @@ export class ApiController {
}
@Put('decks/:id')
updateDeck(
async updateDeck(
@CurrentUser() user: any,
@Param('id') deckId: string,
@Body() deckData: any,
) {
this.logger.log(`Updating deck ${deckId} for user: ${user.sub}`);
const updatedDeck = await this.deckRepository.update(deckId, user.sub, {
title: deckData.title,
description: deckData.description,
coverImageUrl: deckData.coverImageUrl,
isPublic: deckData.isPublic,
settings: deckData.settings,
tags: deckData.tags,
metadata: deckData.metadata,
});
if (!updatedDeck) {
throw new BadRequestException({
error: 'deck_not_found',
message: 'Deck not found or you do not have permission to update it',
});
}
return {
success: true,
userId: user.sub,
deckId,
deck: deckData,
message: 'Deck would be updated in Supabase',
deck: updatedDeck,
};
}
@Delete('decks/:id')
deleteDeck(@CurrentUser() user: any, @Param('id') deckId: string) {
async deleteDeck(@CurrentUser() user: any, @Param('id') deckId: string) {
this.logger.log(`Deleting deck ${deckId} for user: ${user.sub}`);
const deleted = await this.deckRepository.delete(deckId, user.sub);
if (!deleted) {
throw new BadRequestException({
error: 'deck_not_found',
message: 'Deck not found or you do not have permission to delete it',
});
}
return {
success: true,
userId: user.sub,
deckId,
message: 'Deck would be deleted from Supabase',
};
}
@Get('cards')
getUserCards(@CurrentUser() user: any) {
async getUserCards(@CurrentUser() user: any) {
this.logger.log(`Getting cards for user: ${user.sub}`);
const cards = await this.cardRepository.findByUserDecks(user.sub);
return {
userId: user.sub,
cards: [],
message: 'Fetch user cards from Supabase',
cards,
count: cards.length,
};
}
@Post('cards')
createCard(@CurrentUser() user: any, @Body() cardData: any) {
async createCard(@CurrentUser() user: any, @Body() cardData: any) {
this.logger.log(`Creating card for user: ${user.sub}`);
// Verify the deck belongs to the user
const deck = await this.deckRepository.findByIdAndUserId(cardData.deckId, user.sub);
if (!deck) {
throw new BadRequestException({
error: 'deck_not_found',
message: 'Deck not found or you do not have permission to add cards to it',
});
}
const card = await this.cardRepository.create({
deckId: cardData.deckId,
title: cardData.title,
content: cardData.content,
cardType: cardData.cardType || 'flashcard',
position: cardData.position ?? 0,
aiModel: cardData.aiModel,
aiPrompt: cardData.aiPrompt,
});
return {
success: true,
userId: user.sub,
card: cardData,
message: 'Card would be created in Supabase',
card,
};
}
@Get('stats')
getUserStats(@CurrentUser() user: any) {
async getUserStats(@CurrentUser() user: any) {
this.logger.log(`Getting stats for user: ${user.sub}`);
const [stats, totalDecks, totalCards] = await Promise.all([
this.userStatsRepository.findOrCreate(user.sub),
this.deckRepository.countByUserId(user.sub),
this.cardRepository.countByUserId(user.sub),
]);
return {
userId: user.sub,
stats: {
totalDecks: 0,
totalCards: 0,
lastActive: new Date(),
...stats,
totalDecks,
totalCards,
},
message: 'Fetch user stats from Supabase',
};
}
}

View file

@ -1,11 +1,18 @@
import { Controller, Get, UseGuards, Query, Logger } from '@nestjs/common';
import { OptionalAuthGuard } from '@mana-core/nestjs-integration/guards';
import { CurrentUser, Public } from '@mana-core/nestjs-integration/decorators';
import { DeckRepository, UserStatsRepository, DeckTemplateRepository } from '../database';
@Controller('public')
export class PublicController {
private readonly logger = new Logger(PublicController.name);
constructor(
private readonly deckRepository: DeckRepository,
private readonly userStatsRepository: UserStatsRepository,
private readonly deckTemplateRepository: DeckTemplateRepository,
) {}
@Public()
@Get('health')
health() {
@ -24,56 +31,61 @@ export class PublicController {
@UseGuards(OptionalAuthGuard)
@Get('featured-decks')
getFeaturedDecks(@CurrentUser() user?: any) {
// User might be authenticated or null
async getFeaturedDecks(@CurrentUser() user?: any) {
const decks = await this.deckRepository.findFeatured(10);
if (user) {
this.logger.log(`Getting personalized featured decks for user: ${user.sub}`);
return {
type: 'personalized',
userId: user.sub,
decks: [],
message: 'Personalized featured decks based on user preferences',
decks,
count: decks.length,
};
}
this.logger.log('Getting generic featured decks');
return {
type: 'generic',
decks: [],
message: 'Generic featured decks for anonymous users',
decks,
count: decks.length,
};
}
@UseGuards(OptionalAuthGuard)
@Get('leaderboard')
getLeaderboard(@CurrentUser() user?: any, @Query('limit') limit = '10') {
const limitNum = parseInt(limit, 10);
async getLeaderboard(@CurrentUser() user?: any, @Query('limit') limit = '10') {
const limitNum = Math.min(parseInt(limit, 10) || 10, 100);
const leaderboard = await this.userStatsRepository.getLeaderboard(limitNum);
if (user) {
this.logger.log(`Getting leaderboard with user ${user.sub} position`);
const userPosition = await this.userStatsRepository.getUserPosition(user.sub);
return {
leaderboard: [],
userPosition: null,
leaderboard,
userPosition,
userId: user.sub,
limit: limitNum,
message: 'Leaderboard with user position highlighted',
};
}
return {
leaderboard: [],
leaderboard,
limit: limitNum,
message: 'Public leaderboard',
};
}
@Public()
@Get('deck-templates')
getDeckTemplates(@Query('category') category?: string) {
async getDeckTemplates(@Query('category') category?: string) {
const templates = category
? await this.deckTemplateRepository.findByCategory(category)
: await this.deckTemplateRepository.findPublic();
return {
category: category || 'all',
templates: [],
message: 'Public deck templates available for all users',
templates,
count: templates.length,
};
}

View file

@ -0,0 +1,29 @@
import { Module, Global, OnModuleDestroy, Logger } from '@nestjs/common';
import { getDb, closeDb, type Database } from '@manacore/manadeck-database/client';
export const DATABASE_TOKEN = 'DATABASE';
@Global()
@Module({
providers: [
{
provide: DATABASE_TOKEN,
useFactory: () => {
const logger = new Logger('DatabaseModule');
logger.log('Initializing database connection');
return getDb();
},
},
],
exports: [DATABASE_TOKEN],
})
export class DatabaseModule implements OnModuleDestroy {
private readonly logger = new Logger(DatabaseModule.name);
async onModuleDestroy() {
this.logger.log('Closing database connection');
await closeDb();
}
}
export type { Database };

View file

@ -0,0 +1,2 @@
export { DatabaseModule, DATABASE_TOKEN, type Database } from './database.module';
export * from './repositories';

View file

@ -0,0 +1,125 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
cards,
decks,
type Card,
type NewCard,
eq,
and,
asc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class CardRepository {
private readonly logger = new Logger(CardRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findByDeckId(deckId: string): Promise<Card[]> {
this.logger.debug(`Finding cards for deck: ${deckId}`);
return this.db
.select()
.from(cards)
.where(eq(cards.deckId, deckId))
.orderBy(asc(cards.position));
}
async findByDeckIdAndUserId(deckId: string, userId: string): Promise<Card[]> {
// Join with decks to verify ownership
const result = await this.db
.select({
card: cards,
})
.from(cards)
.innerJoin(decks, eq(cards.deckId, decks.id))
.where(and(eq(cards.deckId, deckId), eq(decks.userId, userId)))
.orderBy(asc(cards.position));
return result.map((r) => r.card);
}
async findById(id: string): Promise<Card | null> {
const result = await this.db
.select()
.from(cards)
.where(eq(cards.id, id))
.limit(1);
return result[0] || null;
}
async findByUserDecks(userId: string): Promise<Card[]> {
// Get all cards from decks owned by the user
const result = await this.db
.select({
card: cards,
})
.from(cards)
.innerJoin(decks, eq(cards.deckId, decks.id))
.where(eq(decks.userId, userId))
.orderBy(asc(cards.deckId), asc(cards.position));
return result.map((r) => r.card);
}
async create(data: NewCard): Promise<Card> {
this.logger.debug(`Creating card in deck: ${data.deckId}`);
const result = await this.db.insert(cards).values(data).returning();
return result[0];
}
async createMany(data: NewCard[]): Promise<Card[]> {
if (data.length === 0) return [];
this.logger.debug(`Creating ${data.length} cards`);
return this.db.insert(cards).values(data).returning();
}
async update(
id: string,
data: Partial<Omit<NewCard, 'id' | 'deckId' | 'createdAt'>>
): Promise<Card | null> {
this.logger.debug(`Updating card: ${id}`);
const result = await this.db
.update(cards)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(cards.id, id))
.returning();
return result[0] || null;
}
async delete(id: string): Promise<boolean> {
this.logger.debug(`Deleting card: ${id}`);
const result = await this.db
.delete(cards)
.where(eq(cards.id, id))
.returning({ id: cards.id });
return result.length > 0;
}
async deleteByDeckId(deckId: string): Promise<number> {
const result = await this.db
.delete(cards)
.where(eq(cards.deckId, deckId))
.returning({ id: cards.id });
return result.length;
}
async countByDeckId(deckId: string): Promise<number> {
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(cards)
.where(eq(cards.deckId, deckId));
return result[0]?.count || 0;
}
async countByUserId(userId: string): Promise<number> {
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(cards)
.innerJoin(decks, eq(cards.deckId, decks.id))
.where(eq(decks.userId, userId));
return result[0]?.count || 0;
}
}

View file

@ -0,0 +1,121 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
deckTemplates,
type DeckTemplate,
type NewDeckTemplate,
eq,
and,
desc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class DeckTemplateRepository {
private readonly logger = new Logger(DeckTemplateRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findAll(includeInactive = false): Promise<DeckTemplate[]> {
if (includeInactive) {
return this.db
.select()
.from(deckTemplates)
.orderBy(desc(deckTemplates.popularity));
}
return this.db
.select()
.from(deckTemplates)
.where(eq(deckTemplates.isActive, true))
.orderBy(desc(deckTemplates.popularity));
}
async findById(id: string): Promise<DeckTemplate | null> {
const result = await this.db
.select()
.from(deckTemplates)
.where(eq(deckTemplates.id, id))
.limit(1);
return result[0] || null;
}
async findByCategory(category: string): Promise<DeckTemplate[]> {
return this.db
.select()
.from(deckTemplates)
.where(
and(
eq(deckTemplates.category, category),
eq(deckTemplates.isActive, true)
)
)
.orderBy(desc(deckTemplates.popularity));
}
async findPublic(): Promise<DeckTemplate[]> {
return this.db
.select()
.from(deckTemplates)
.where(
and(eq(deckTemplates.isPublic, true), eq(deckTemplates.isActive, true))
)
.orderBy(desc(deckTemplates.popularity));
}
async create(data: NewDeckTemplate): Promise<DeckTemplate> {
this.logger.debug(`Creating deck template: ${data.title}`);
const result = await this.db
.insert(deckTemplates)
.values(data)
.returning();
return result[0];
}
async update(
id: string,
data: Partial<Omit<NewDeckTemplate, 'id' | 'createdAt'>>
): Promise<DeckTemplate | null> {
this.logger.debug(`Updating deck template: ${id}`);
const result = await this.db
.update(deckTemplates)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(deckTemplates.id, id))
.returning();
return result[0] || null;
}
async delete(id: string): Promise<boolean> {
this.logger.debug(`Deleting deck template: ${id}`);
const result = await this.db
.delete(deckTemplates)
.where(eq(deckTemplates.id, id))
.returning({ id: deckTemplates.id });
return result.length > 0;
}
async incrementPopularity(id: string): Promise<DeckTemplate | null> {
const result = await this.db
.update(deckTemplates)
.set({
popularity: sql`${deckTemplates.popularity} + 1`,
updatedAt: new Date(),
})
.where(eq(deckTemplates.id, id))
.returning();
return result[0] || null;
}
async getCategories(): Promise<string[]> {
const result = await this.db
.selectDistinct({ category: deckTemplates.category })
.from(deckTemplates)
.where(eq(deckTemplates.isActive, true));
return result
.map((r) => r.category)
.filter((c): c is string => c !== null);
}
}

View file

@ -0,0 +1,103 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
decks,
type Deck,
type NewDeck,
eq,
and,
desc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class DeckRepository {
private readonly logger = new Logger(DeckRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findByUserId(userId: string): Promise<Deck[]> {
this.logger.debug(`Finding decks for user: ${userId}`);
return this.db
.select()
.from(decks)
.where(eq(decks.userId, userId))
.orderBy(desc(decks.createdAt));
}
async findById(id: string): Promise<Deck | null> {
const result = await this.db
.select()
.from(decks)
.where(eq(decks.id, id))
.limit(1);
return result[0] || null;
}
async findByIdAndUserId(id: string, userId: string): Promise<Deck | null> {
const result = await this.db
.select()
.from(decks)
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.limit(1);
return result[0] || null;
}
async create(data: NewDeck): Promise<Deck> {
this.logger.debug(`Creating deck: ${data.title}`);
const result = await this.db.insert(decks).values(data).returning();
return result[0];
}
async update(
id: string,
userId: string,
data: Partial<Omit<NewDeck, 'id' | 'userId' | 'createdAt'>>
): Promise<Deck | null> {
this.logger.debug(`Updating deck: ${id}`);
const result = await this.db
.update(decks)
.set({
...data,
updatedAt: new Date(),
})
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.returning();
return result[0] || null;
}
async delete(id: string, userId: string): Promise<boolean> {
this.logger.debug(`Deleting deck: ${id}`);
const result = await this.db
.delete(decks)
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.returning({ id: decks.id });
return result.length > 0;
}
async findFeatured(limit = 10): Promise<Deck[]> {
return this.db
.select()
.from(decks)
.where(and(eq(decks.isFeatured, true), eq(decks.isPublic, true)))
.orderBy(desc(decks.featuredAt))
.limit(limit);
}
async findPublic(limit = 10): Promise<Deck[]> {
return this.db
.select()
.from(decks)
.where(eq(decks.isPublic, true))
.orderBy(desc(decks.createdAt))
.limit(limit);
}
async countByUserId(userId: string): Promise<number> {
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(decks)
.where(eq(decks.userId, userId));
return result[0]?.count || 0;
}
}

View file

@ -0,0 +1,4 @@
export { DeckRepository } from './deck.repository';
export { CardRepository } from './card.repository';
export { UserStatsRepository } from './user-stats.repository';
export { DeckTemplateRepository } from './deck-template.repository';

View file

@ -0,0 +1,146 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { DATABASE_TOKEN, type Database } from '../database.module';
import {
userStats,
type UserStats,
type NewUserStats,
eq,
desc,
sql,
} from '@manacore/manadeck-database';
@Injectable()
export class UserStatsRepository {
private readonly logger = new Logger(UserStatsRepository.name);
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
async findByUserId(userId: string): Promise<UserStats | null> {
this.logger.debug(`Finding stats for user: ${userId}`);
const result = await this.db
.select()
.from(userStats)
.where(eq(userStats.userId, userId))
.limit(1);
return result[0] || null;
}
async findOrCreate(userId: string): Promise<UserStats> {
const existing = await this.findByUserId(userId);
if (existing) return existing;
this.logger.debug(`Creating stats for user: ${userId}`);
const result = await this.db
.insert(userStats)
.values({ userId })
.returning();
return result[0];
}
async update(
userId: string,
data: Partial<Omit<NewUserStats, 'userId' | 'createdAt'>>
): Promise<UserStats | null> {
this.logger.debug(`Updating stats for user: ${userId}`);
const result = await this.db
.update(userStats)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
async getLeaderboard(limit = 10): Promise<UserStats[]> {
return this.db
.select()
.from(userStats)
.orderBy(desc(userStats.totalWins))
.limit(limit);
}
async getLeaderboardByStreak(limit = 10): Promise<UserStats[]> {
return this.db
.select()
.from(userStats)
.orderBy(desc(userStats.streakDays))
.limit(limit);
}
async getUserPosition(userId: string): Promise<number | null> {
// Get user's total wins
const user = await this.findByUserId(userId);
if (!user) return null;
// Count users with higher wins
const result = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(userStats)
.where(sql`${userStats.totalWins} > ${user.totalWins}`);
return (result[0]?.count || 0) + 1;
}
async incrementWins(userId: string, count = 1): Promise<UserStats | null> {
const result = await this.db
.update(userStats)
.set({
totalWins: sql`${userStats.totalWins} + ${count}`,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
async incrementSessions(userId: string): Promise<UserStats | null> {
const result = await this.db
.update(userStats)
.set({
totalSessions: sql`${userStats.totalSessions} + 1`,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
async updateStudyProgress(
userId: string,
cardsStudied: number,
timeSeconds: number,
accuracy: number
): Promise<UserStats | null> {
const today = new Date().toISOString().split('T')[0];
const existing = await this.findByUserId(userId);
if (!existing) {
await this.findOrCreate(userId);
}
// Calculate new average accuracy
const currentAvg = existing ? parseFloat(existing.averageAccuracy) : 0;
const currentTotal = existing?.totalCardsStudied || 0;
const newTotal = currentTotal + cardsStudied;
const newAvg =
newTotal > 0
? (currentAvg * currentTotal + accuracy * cardsStudied) / newTotal
: accuracy;
const result = await this.db
.update(userStats)
.set({
totalCardsStudied: sql`${userStats.totalCardsStudied} + ${cardsStudied}`,
totalTimeSeconds: sql`${userStats.totalTimeSeconds} + ${timeSeconds}`,
averageAccuracy: newAvg.toFixed(2),
lastStudyDate: today,
updatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
.returning();
return result[0] || null;
}
}

View file

@ -3,14 +3,32 @@
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./src/index.ts",
"./schema": "./src/schema/index.ts",
"./client": "./src/client.ts"
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./schema": {
"types": "./dist/schema/index.d.ts",
"import": "./dist/schema/index.js",
"require": "./dist/schema/index.js",
"default": "./dist/schema/index.js"
},
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.js",
"require": "./dist/client.js",
"default": "./dist/client.js"
}
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"prepare": "pnpm build",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"docker:logs": "docker compose logs -f postgres",

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');

View file

@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,

2888
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff