mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:41:09 +02:00
feat(manadeck): complete Supabase removal from frontend and backend
- Frontend: Updated deckStore to use fetch API calls to backend - Frontend: Removed supabase.ts utility and @supabase/supabase-js dependency - Backend: Removed SupabaseService and supabase.service.ts - Backend: Updated validation schema to use DATABASE_URL - Backend: Updated health controller to remove Supabase check - Backend: Marked AI generation endpoint as not implemented (was using Edge Functions) Phase 4 of PostgreSQL/Drizzle migration complete. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1530efa936
commit
be9df4aa85
10 changed files with 87 additions and 553 deletions
|
|
@ -34,14 +34,12 @@
|
|||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-subscription-types": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-supabase": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"svelte-i18n": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Deck, CreateDeckInput, UpdateDeckInput } from '$lib/types/deck';
|
||||
import { getAuthenticatedSupabase } from '$lib/utils/supabase';
|
||||
import { PUBLIC_API_URL } from '$env/static/public';
|
||||
import { authService } from '$lib/auth';
|
||||
|
||||
// Svelte 5 runes-based deck store
|
||||
|
|
@ -8,6 +8,35 @@ let currentDeck = $state<Deck | null>(null);
|
|||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
/**
|
||||
* Helper to make authenticated API requests
|
||||
*/
|
||||
async function apiRequest<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const appToken = await authService.getAppToken();
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`${PUBLIC_API_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `API error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const deckStore = {
|
||||
get decks() {
|
||||
return decks;
|
||||
|
|
@ -30,31 +59,8 @@ export const deckStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const appToken = await authService.getAppToken();
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const user = await authService.getUserFromToken();
|
||||
if (!user) {
|
||||
throw new Error('No user found');
|
||||
}
|
||||
|
||||
const supabase = await getAuthenticatedSupabase(appToken);
|
||||
|
||||
const { data, error: fetchError } = await supabase
|
||||
.from('decks')
|
||||
.select('*, cards(count)')
|
||||
.or(`user_id.eq.${user.id},and(is_public.eq.true,user_id.eq.00000000-0000-0000-0000-000000000001)`)
|
||||
.order('updated_at', { ascending: false });
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
// Map card count
|
||||
decks = (data || []).map((deck: any) => ({
|
||||
...deck,
|
||||
card_count: deck.cards?.[0]?.count || 0
|
||||
}));
|
||||
const response = await apiRequest<{ decks: Deck[]; count: number }>('/v1/api/decks');
|
||||
decks = response.decks || [];
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Failed to fetch decks';
|
||||
console.error('Fetch decks error:', err);
|
||||
|
|
@ -71,23 +77,15 @@ export const deckStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const appToken = await authService.getAppToken();
|
||||
if (!appToken) throw new Error('Not authenticated');
|
||||
|
||||
const supabase = await getAuthenticatedSupabase(appToken);
|
||||
|
||||
const { data, error: fetchError } = await supabase
|
||||
.from('decks')
|
||||
.select('*, cards(count)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
currentDeck = {
|
||||
...data,
|
||||
card_count: data.cards?.[0]?.count || 0
|
||||
};
|
||||
// For now, find in local decks or fetch all
|
||||
// TODO: Add GET /v1/api/decks/:id endpoint to backend
|
||||
if (decks.length === 0) {
|
||||
await this.fetchDecks();
|
||||
}
|
||||
currentDeck = decks.find((d) => d.id === id) || null;
|
||||
if (!currentDeck) {
|
||||
throw new Error('Deck not found');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Failed to fetch deck';
|
||||
console.error('Fetch deck error:', err);
|
||||
|
|
@ -104,35 +102,23 @@ export const deckStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const appToken = await authService.getAppToken();
|
||||
if (!appToken) throw new Error('Not authenticated');
|
||||
const response = await apiRequest<{ success: boolean; deck: Deck }>('/v1/api/decks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
description: input.description || '',
|
||||
isPublic: input.is_public ?? false,
|
||||
tags: input.tags || [],
|
||||
settings: input.settings || {}
|
||||
})
|
||||
});
|
||||
|
||||
const user = await authService.getUserFromToken();
|
||||
if (!user) throw new Error('No user found');
|
||||
|
||||
const supabase = await getAuthenticatedSupabase(appToken);
|
||||
|
||||
const newDeck = {
|
||||
user_id: user.id,
|
||||
title: input.title,
|
||||
description: input.description || '',
|
||||
is_public: input.is_public ?? false,
|
||||
tags: input.tags || [],
|
||||
settings: input.settings || {},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const { data, error: createError } = await supabase
|
||||
.from('decks')
|
||||
.insert(newDeck)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError) throw createError;
|
||||
|
||||
const deck = { ...data, card_count: 0 };
|
||||
decks = [deck, ...decks];
|
||||
return deck;
|
||||
if (response.deck) {
|
||||
const deck = { ...response.deck, card_count: 0 };
|
||||
decks = [deck, ...decks];
|
||||
return deck;
|
||||
}
|
||||
return null;
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Failed to create deck';
|
||||
console.error('Create deck error:', err);
|
||||
|
|
@ -150,26 +136,22 @@ export const deckStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const appToken = await authService.getAppToken();
|
||||
if (!appToken) throw new Error('Not authenticated');
|
||||
const response = await apiRequest<{ success: boolean; deck: Deck }>(
|
||||
`/v1/api/decks/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates)
|
||||
}
|
||||
);
|
||||
|
||||
const supabase = await getAuthenticatedSupabase(appToken);
|
||||
if (response.deck) {
|
||||
// Update in list
|
||||
decks = decks.map((d) => (d.id === id ? { ...d, ...response.deck } : d));
|
||||
|
||||
const { data, error: updateError } = await supabase
|
||||
.from('decks')
|
||||
.update(updates)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
// Update in list
|
||||
decks = decks.map((d) => (d.id === id ? { ...d, ...data } : d));
|
||||
|
||||
// Update current if it's the same
|
||||
if (currentDeck?.id === id) {
|
||||
currentDeck = { ...currentDeck, ...data };
|
||||
// Update current if it's the same
|
||||
if (currentDeck?.id === id) {
|
||||
currentDeck = { ...currentDeck, ...response.deck };
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Failed to update deck';
|
||||
|
|
@ -187,14 +169,9 @@ export const deckStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const appToken = await authService.getAppToken();
|
||||
if (!appToken) throw new Error('Not authenticated');
|
||||
|
||||
const supabase = await getAuthenticatedSupabase(appToken);
|
||||
|
||||
const { error: deleteError } = await supabase.from('decks').delete().eq('id', id);
|
||||
|
||||
if (deleteError) throw deleteError;
|
||||
await apiRequest<{ success: boolean }>(`/v1/api/decks/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
// Remove from list
|
||||
decks = decks.filter((d) => d.id !== id);
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
// Create a singleton Supabase client
|
||||
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get authenticated Supabase client with Mana Core token
|
||||
* This injects the JWT token from Mana Core for RLS policies
|
||||
*/
|
||||
export async function getAuthenticatedSupabase(appToken?: string) {
|
||||
if (!appToken) {
|
||||
// Try to get token from sessionStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
appToken = sessionStorage.getItem('appToken') || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!appToken) {
|
||||
throw new Error('No auth token available');
|
||||
}
|
||||
|
||||
// Create a new client instance with the auth token
|
||||
const authenticatedClient = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`
|
||||
}
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false
|
||||
}
|
||||
});
|
||||
|
||||
return authenticatedClient;
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@
|
|||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"axios": "^1.7.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { AppService } from './app.service';
|
|||
import { ApiController } from './controllers/api.controller';
|
||||
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,
|
||||
|
|
@ -65,7 +64,6 @@ import {
|
|||
],
|
||||
providers: [
|
||||
AppService,
|
||||
SupabaseService,
|
||||
// Database repositories
|
||||
DeckRepository,
|
||||
CardRepository,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ export const validationSchema = Joi.object({
|
|||
MANA_SUPABASE_SECRET_KEY: Joi.string().optional(),
|
||||
SIGNUP_REDIRECT_URL: Joi.string().uri().optional(),
|
||||
|
||||
// Your app's database
|
||||
SUPABASE_URL: Joi.string().uri().required(),
|
||||
SUPABASE_ANON_KEY: Joi.string().required(),
|
||||
SUPABASE_SERVICE_KEY: Joi.string().required(), // Required for edge functions
|
||||
// PostgreSQL Database
|
||||
DATABASE_URL: Joi.string().required(),
|
||||
|
||||
// JWT
|
||||
JWT_SECRET: Joi.string().optional(),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Logger, BadRequestException, Req } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Logger, BadRequestException, NotImplementedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
|
||||
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')
|
||||
|
|
@ -13,7 +12,6 @@ export class ApiController {
|
|||
|
||||
constructor(
|
||||
private readonly creditClient: CreditClientService,
|
||||
private readonly supabaseService: SupabaseService,
|
||||
private readonly deckRepository: DeckRepository,
|
||||
private readonly cardRepository: CardRepository,
|
||||
private readonly userStatsRepository: UserStatsRepository,
|
||||
|
|
@ -150,121 +148,15 @@ export class ApiController {
|
|||
}
|
||||
|
||||
@Post('decks/generate')
|
||||
async generateDeckWithAI(@CurrentUser() user: any, @Body() requestData: any, @Req() req: any) {
|
||||
this.logger.log(`Generating AI deck for user: ${user.sub}`);
|
||||
async generateDeckWithAI(@CurrentUser() user: any, @Body() requestData: any) {
|
||||
this.logger.log(`AI deck generation requested by user: ${user.sub}`);
|
||||
|
||||
const { prompt, deckTitle, deckDescription, cardCount = 10, cardTypes, difficulty, tags } = requestData;
|
||||
|
||||
// Validate required fields
|
||||
if (!prompt || !deckTitle) {
|
||||
throw new BadRequestException({
|
||||
error: 'validation_failed',
|
||||
message: 'prompt and deckTitle are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (cardCount < 1 || cardCount > 50) {
|
||||
throw new BadRequestException({
|
||||
error: 'validation_failed',
|
||||
message: 'cardCount must be between 1 and 50',
|
||||
});
|
||||
}
|
||||
|
||||
const operationType = CreditOperationType.AI_DECK_GENERATION;
|
||||
const creditCost = getCreditCost(operationType);
|
||||
|
||||
try {
|
||||
// 1. Pre-flight credit validation
|
||||
const validation = await this.creditClient.validateCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
);
|
||||
|
||||
if (!validation.hasCredits) {
|
||||
this.logger.warn(
|
||||
`User ${user.sub} has insufficient credits for AI deck generation. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
);
|
||||
|
||||
throw new BadRequestException({
|
||||
error: 'insufficient_credits',
|
||||
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
requiredCredits: creditCost,
|
||||
availableCredits: validation.availableCredits,
|
||||
operation: getOperationDescription(operationType),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Get the Mana token from the request
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
throw new BadRequestException({
|
||||
error: 'authentication_failed',
|
||||
message: 'No authorization token found',
|
||||
});
|
||||
}
|
||||
const manaToken = authHeader.replace('Bearer ', '');
|
||||
|
||||
// 3. Call the edge function via Supabase
|
||||
const result = await this.supabaseService.generateDeckWithAI(
|
||||
user.sub,
|
||||
{
|
||||
prompt,
|
||||
deckTitle,
|
||||
deckDescription,
|
||||
cardCount,
|
||||
cardTypes,
|
||||
difficulty,
|
||||
tags,
|
||||
},
|
||||
manaToken,
|
||||
);
|
||||
|
||||
// 4. Check if the edge function was successful
|
||||
if (!result.success) {
|
||||
throw new BadRequestException({
|
||||
error: 'deck_generation_failed',
|
||||
message: result.error || 'Failed to generate deck',
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Success - Consume credits
|
||||
await this.creditClient.consumeCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
`Generated AI deck: ${deckTitle}`,
|
||||
{
|
||||
deckId: result.deck?.id,
|
||||
deckTitle,
|
||||
cardCount: result.deck?.card_count,
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(`AI deck generated successfully for user ${user.sub}. ${creditCost} credits consumed.`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: user.sub,
|
||||
deck: result.deck,
|
||||
cards: result.cards,
|
||||
creditsUsed: creditCost,
|
||||
message: result.message || 'Deck generated successfully with AI',
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's already a BadRequestException, rethrow it
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log other errors
|
||||
this.logger.error(`Error generating AI deck for user ${user.sub}:`, error);
|
||||
throw new BadRequestException({
|
||||
error: 'deck_generation_failed',
|
||||
message: error.message || 'Failed to generate deck with AI',
|
||||
});
|
||||
}
|
||||
// TODO: Implement AI deck generation with a self-hosted solution
|
||||
// This endpoint previously used Supabase Edge Functions which are no longer available
|
||||
throw new NotImplementedException({
|
||||
error: 'not_implemented',
|
||||
message: 'AI deck generation is currently being migrated to a new infrastructure. Please check back later.',
|
||||
});
|
||||
}
|
||||
|
||||
@Put('decks/:id')
|
||||
|
|
|
|||
|
|
@ -20,11 +20,10 @@ export class HealthController {
|
|||
@HealthCheck()
|
||||
check() {
|
||||
const manaServiceUrl = this.configService.get<string>('MANA_SERVICE_URL')!;
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
|
||||
return this.health.check([
|
||||
() => this.http.pingCheck('mana-core', manaServiceUrl),
|
||||
() => this.http.pingCheck('supabase', `${supabaseUrl}/rest/v1/`),
|
||||
// PostgreSQL health is checked via database module initialization
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ async function bootstrap() {
|
|||
// Debug: Log environment variables before validation
|
||||
logger.log('=== Environment Variables Debug ===');
|
||||
logger.log(`APP_ID: ${process.env.APP_ID ? process.env.APP_ID.substring(0, 20) + '...' : 'NOT SET'}`);
|
||||
logger.log(`SUPABASE_URL: ${process.env.SUPABASE_URL || 'NOT SET'}`);
|
||||
logger.log(`DATABASE_URL: ${process.env.DATABASE_URL ? '[SET]' : 'NOT SET'}`);
|
||||
logger.log(`MANA_SERVICE_URL: ${process.env.MANA_SERVICE_URL || 'NOT SET'}`);
|
||||
logger.log('===================================');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,283 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
@Injectable()
|
||||
export class SupabaseService {
|
||||
private readonly logger = new Logger(SupabaseService.name);
|
||||
private supabase: SupabaseClient;
|
||||
private supabaseServiceRole: SupabaseClient;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
const supabaseAnonKey = this.configService.get<string>('SUPABASE_ANON_KEY')!;
|
||||
const supabaseServiceKey = this.configService.get<string>('SUPABASE_SERVICE_KEY');
|
||||
|
||||
// Client for public operations
|
||||
this.supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Client for service-level operations (optional, only if service key is provided)
|
||||
if (supabaseServiceKey) {
|
||||
this.supabaseServiceRole = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('Supabase service initialized');
|
||||
}
|
||||
|
||||
// Get client with user's token for RLS
|
||||
getClientWithUserToken(token: string): SupabaseClient {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
const supabaseAnonKey = this.configService.get<string>('SUPABASE_ANON_KEY')!;
|
||||
|
||||
return createClient(supabaseUrl, supabaseAnonKey, {
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Example methods for deck operations
|
||||
async getUserDecks(userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching user decks:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createDeck(userId: string, deckData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.insert({
|
||||
...deckData,
|
||||
user_id: userId,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error creating deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateDeck(deckId: string, userId: string, deckData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.update({
|
||||
...deckData,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.eq('id', deckId)
|
||||
.eq('user_id', userId) // Ensure user owns the deck
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error updating deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async deleteDeck(deckId: string, userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { error } = await client
|
||||
.from('decks')
|
||||
.delete()
|
||||
.eq('id', deckId)
|
||||
.eq('user_id', userId); // Ensure user owns the deck
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error deleting deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Example methods for card operations
|
||||
async getUserCards(userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('cards')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching user cards:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createCard(userId: string, cardData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('cards')
|
||||
.insert({
|
||||
...cardData,
|
||||
user_id: userId,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error creating card:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Public methods (no auth required)
|
||||
async getFeaturedDecks(limit = 10) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('decks')
|
||||
.select('*')
|
||||
.eq('is_featured', true)
|
||||
.eq('is_public', true)
|
||||
.limit(limit)
|
||||
.order('featured_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching featured decks:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getLeaderboard(limit = 10) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('user_stats')
|
||||
.select('*')
|
||||
.order('total_wins', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching leaderboard:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getDeckTemplates(category?: string) {
|
||||
let query = this.supabase
|
||||
.from('deck_templates')
|
||||
.select('*')
|
||||
.eq('is_active', true);
|
||||
|
||||
if (category) {
|
||||
query = query.eq('category', category);
|
||||
}
|
||||
|
||||
const { data, error } = await query.order('popularity', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching deck templates:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Service-level operations (using service role key)
|
||||
async adminGetAllUsers() {
|
||||
if (!this.supabaseServiceRole) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabaseServiceRole.auth.admin.listUsers();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching all users:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Edge Function invocations
|
||||
async generateDeckWithAI(
|
||||
userId: string,
|
||||
requestData: {
|
||||
prompt: string;
|
||||
deckTitle: string;
|
||||
deckDescription?: string;
|
||||
cardCount?: number;
|
||||
cardTypes?: string[];
|
||||
difficulty?: string;
|
||||
tags?: string[];
|
||||
},
|
||||
manaToken: string,
|
||||
) {
|
||||
if (!this.supabaseServiceRole) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
|
||||
this.logger.log(`Invoking generate-deck edge function for user ${userId}`);
|
||||
|
||||
const { data, error } = await this.supabaseServiceRole.functions.invoke(
|
||||
'generate-deck',
|
||||
{
|
||||
body: requestData,
|
||||
headers: {
|
||||
Authorization: `Bearer ${manaToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error invoking generate-deck edge function:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue