mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -63,6 +63,7 @@ Dieses Dokument beschreibt die Migration von ManaDeck von Supabase zu einer selb
|
|||
### Tabellen
|
||||
|
||||
#### 1. `decks`
|
||||
|
||||
```sql
|
||||
CREATE TABLE decks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
|
@ -86,6 +87,7 @@ CREATE INDEX idx_decks_is_featured ON decks(is_featured);
|
|||
```
|
||||
|
||||
#### 2. `cards`
|
||||
|
||||
```sql
|
||||
CREATE TABLE cards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
|
@ -107,6 +109,7 @@ CREATE INDEX idx_cards_position ON cards(deck_id, position);
|
|||
```
|
||||
|
||||
#### 3. `study_sessions`
|
||||
|
||||
```sql
|
||||
CREATE TABLE study_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
|
@ -126,6 +129,7 @@ CREATE INDEX idx_study_sessions_deck_id ON study_sessions(deck_id);
|
|||
```
|
||||
|
||||
#### 4. `card_progress`
|
||||
|
||||
```sql
|
||||
CREATE TABLE card_progress (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
|
@ -147,6 +151,7 @@ CREATE INDEX idx_card_progress_next_review ON card_progress(next_review);
|
|||
```
|
||||
|
||||
#### 5. `deck_templates`
|
||||
|
||||
```sql
|
||||
CREATE TABLE deck_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
|
@ -166,6 +171,7 @@ CREATE INDEX idx_deck_templates_is_active ON deck_templates(is_active);
|
|||
```
|
||||
|
||||
#### 6. `ai_generations`
|
||||
|
||||
```sql
|
||||
CREATE TABLE ai_generations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
|
@ -185,6 +191,7 @@ CREATE INDEX idx_ai_generations_status ON ai_generations(status);
|
|||
```
|
||||
|
||||
#### 7. `user_stats` (für Leaderboard)
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_stats (
|
||||
user_id UUID PRIMARY KEY,
|
||||
|
|
@ -233,23 +240,24 @@ manadeck/
|
|||
### Schema-Definitionen (Drizzle)
|
||||
|
||||
#### `schema/decks.ts`
|
||||
|
||||
```typescript
|
||||
import { pgTable, uuid, varchar, text, boolean, timestamp, jsonb } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const decks = pgTable('decks', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').notNull(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
coverImageUrl: text('cover_image_url'),
|
||||
isPublic: boolean('is_public').default(false),
|
||||
isFeatured: boolean('is_featured').default(false),
|
||||
featuredAt: timestamp('featured_at', { withTimezone: true }),
|
||||
settings: jsonb('settings').default({}),
|
||||
tags: text('tags').array().default([]),
|
||||
metadata: jsonb('metadata').default({}),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').notNull(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
coverImageUrl: text('cover_image_url'),
|
||||
isPublic: boolean('is_public').default(false),
|
||||
isFeatured: boolean('is_featured').default(false),
|
||||
featuredAt: timestamp('featured_at', { withTimezone: true }),
|
||||
settings: jsonb('settings').default({}),
|
||||
tags: text('tags').array().default([]),
|
||||
metadata: jsonb('metadata').default({}),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
||||
});
|
||||
|
||||
export type Deck = typeof decks.$inferSelect;
|
||||
|
|
@ -257,25 +265,37 @@ export type NewDeck = typeof decks.$inferInsert;
|
|||
```
|
||||
|
||||
#### `schema/cards.ts`
|
||||
|
||||
```typescript
|
||||
import { pgTable, uuid, varchar, text, integer, boolean, timestamp, jsonb } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
integer,
|
||||
boolean,
|
||||
timestamp,
|
||||
jsonb,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { decks } from './decks';
|
||||
|
||||
export const cardTypeEnum = pgEnum('card_type', ['text', 'flashcard', 'quiz', 'mixed']);
|
||||
|
||||
export const cards = pgTable('cards', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
deckId: uuid('deck_id').notNull().references(() => decks.id, { onDelete: 'cascade' }),
|
||||
position: integer('position').notNull().default(0),
|
||||
title: varchar('title', { length: 255 }),
|
||||
content: jsonb('content').notNull(),
|
||||
cardType: cardTypeEnum('card_type').notNull(),
|
||||
aiModel: varchar('ai_model', { length: 100 }),
|
||||
aiPrompt: text('ai_prompt'),
|
||||
version: integer('version').default(1),
|
||||
isFavorite: boolean('is_favorite').default(false),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
deckId: uuid('deck_id')
|
||||
.notNull()
|
||||
.references(() => decks.id, { onDelete: 'cascade' }),
|
||||
position: integer('position').notNull().default(0),
|
||||
title: varchar('title', { length: 255 }),
|
||||
content: jsonb('content').notNull(),
|
||||
cardType: cardTypeEnum('card_type').notNull(),
|
||||
aiModel: varchar('ai_model', { length: 100 }),
|
||||
aiPrompt: text('ai_prompt'),
|
||||
version: integer('version').default(1),
|
||||
isFavorite: boolean('is_favorite').default(false),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
||||
});
|
||||
|
||||
export type Card = typeof cards.$inferSelect;
|
||||
|
|
@ -283,6 +303,7 @@ export type NewCard = typeof cards.$inferInsert;
|
|||
```
|
||||
|
||||
#### `schema/index.ts`
|
||||
|
||||
```typescript
|
||||
export * from './decks';
|
||||
export * from './cards';
|
||||
|
|
@ -300,6 +321,7 @@ export { cardsRelations } from './cards';
|
|||
### Client Setup
|
||||
|
||||
#### `client.ts`
|
||||
|
||||
```typescript
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
|
|
@ -309,9 +331,9 @@ const connectionString = process.env.DATABASE_URL!;
|
|||
|
||||
// For connection pooling in serverless environments
|
||||
const client = postgres(connectionString, {
|
||||
max: 10,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 10,
|
||||
max: 10,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 10,
|
||||
});
|
||||
|
||||
export const db = drizzle(client, { schema });
|
||||
|
|
@ -325,6 +347,7 @@ export type Database = typeof db;
|
|||
### Phase 1: Setup (Tag 1-2)
|
||||
|
||||
#### 1.1 PostgreSQL Server aufsetzen
|
||||
|
||||
```bash
|
||||
# Option A: Railway.app (empfohlen für Staging)
|
||||
# Erstelle neues Projekt auf railway.app
|
||||
|
|
@ -344,6 +367,7 @@ docker run -d \
|
|||
```
|
||||
|
||||
#### 1.2 Database Package erstellen
|
||||
|
||||
```bash
|
||||
cd /Users/tillschneider/Documents/__00__Code/manacore-monorepo
|
||||
mkdir -p packages/manadeck-database
|
||||
|
|
@ -352,6 +376,7 @@ pnpm init
|
|||
```
|
||||
|
||||
#### 1.3 Dependencies installieren
|
||||
|
||||
```bash
|
||||
pnpm add drizzle-orm postgres
|
||||
pnpm add -D drizzle-kit typescript @types/node
|
||||
|
|
@ -360,17 +385,20 @@ pnpm add -D drizzle-kit typescript @types/node
|
|||
### Phase 2: Schema & Migration (Tag 2-3)
|
||||
|
||||
#### 2.1 Drizzle Schema erstellen
|
||||
|
||||
- Alle Tabellen wie oben definiert
|
||||
- Relations definieren
|
||||
- Indexes definieren
|
||||
|
||||
#### 2.2 Initial Migration generieren
|
||||
|
||||
```bash
|
||||
pnpm drizzle-kit generate
|
||||
pnpm drizzle-kit migrate
|
||||
```
|
||||
|
||||
#### 2.3 Daten von Supabase exportieren
|
||||
|
||||
```bash
|
||||
# In Supabase Dashboard: SQL Editor
|
||||
# Export alle Tabellen als CSV oder pg_dump
|
||||
|
|
@ -401,56 +429,54 @@ import { decks, cards } from '@manadeck/database/schema';
|
|||
import { eq, and, or, desc } from 'drizzle-orm';
|
||||
|
||||
export class DeckRepository {
|
||||
async findAllByUser(userId: string) {
|
||||
return db.query.decks.findMany({
|
||||
where: eq(decks.userId, userId),
|
||||
orderBy: desc(decks.updatedAt),
|
||||
with: {
|
||||
cards: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
async findAllByUser(userId: string) {
|
||||
return db.query.decks.findMany({
|
||||
where: eq(decks.userId, userId),
|
||||
orderBy: desc(decks.updatedAt),
|
||||
with: {
|
||||
cards: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return db.query.decks.findFirst({
|
||||
where: eq(decks.id, id),
|
||||
with: {
|
||||
cards: {
|
||||
orderBy: (cards, { asc }) => [asc(cards.position)],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
async findById(id: string) {
|
||||
return db.query.decks.findFirst({
|
||||
where: eq(decks.id, id),
|
||||
with: {
|
||||
cards: {
|
||||
orderBy: (cards, { asc }) => [asc(cards.position)],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findPublicAndUserDecks(userId: string) {
|
||||
return db.query.decks.findMany({
|
||||
where: or(
|
||||
eq(decks.userId, userId),
|
||||
and(eq(decks.isPublic, true), eq(decks.isFeatured, true))
|
||||
),
|
||||
orderBy: desc(decks.updatedAt),
|
||||
});
|
||||
}
|
||||
async findPublicAndUserDecks(userId: string) {
|
||||
return db.query.decks.findMany({
|
||||
where: or(
|
||||
eq(decks.userId, userId),
|
||||
and(eq(decks.isPublic, true), eq(decks.isFeatured, true))
|
||||
),
|
||||
orderBy: desc(decks.updatedAt),
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: NewDeck) {
|
||||
const [deck] = await db.insert(decks).values(data).returning();
|
||||
return deck;
|
||||
}
|
||||
async create(data: NewDeck) {
|
||||
const [deck] = await db.insert(decks).values(data).returning();
|
||||
return deck;
|
||||
}
|
||||
|
||||
async update(id: string, userId: string, data: Partial<NewDeck>) {
|
||||
const [deck] = await db
|
||||
.update(decks)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
|
||||
.returning();
|
||||
return deck;
|
||||
}
|
||||
async update(id: string, userId: string, data: Partial<NewDeck>) {
|
||||
const [deck] = await db
|
||||
.update(decks)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
|
||||
.returning();
|
||||
return deck;
|
||||
}
|
||||
|
||||
async delete(id: string, userId: string) {
|
||||
await db
|
||||
.delete(decks)
|
||||
.where(and(eq(decks.id, id), eq(decks.userId, userId)));
|
||||
}
|
||||
async delete(id: string, userId: string) {
|
||||
await db.delete(decks).where(and(eq(decks.id, id), eq(decks.userId, userId)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -461,30 +487,30 @@ export class DeckRepository {
|
|||
import { DeckRepository } from '../repositories/deck.repository';
|
||||
|
||||
export class DeckService {
|
||||
constructor(private deckRepo = new DeckRepository()) {}
|
||||
constructor(private deckRepo = new DeckRepository()) {}
|
||||
|
||||
async getUserDecks(userId: string) {
|
||||
return this.deckRepo.findPublicAndUserDecks(userId);
|
||||
}
|
||||
async getUserDecks(userId: string) {
|
||||
return this.deckRepo.findPublicAndUserDecks(userId);
|
||||
}
|
||||
|
||||
async getDeck(id: string) {
|
||||
return this.deckRepo.findById(id);
|
||||
}
|
||||
async getDeck(id: string) {
|
||||
return this.deckRepo.findById(id);
|
||||
}
|
||||
|
||||
async createDeck(userId: string, data: CreateDeckInput) {
|
||||
return this.deckRepo.create({
|
||||
...data,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
async createDeck(userId: string, data: CreateDeckInput) {
|
||||
return this.deckRepo.create({
|
||||
...data,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
async updateDeck(id: string, userId: string, data: UpdateDeckInput) {
|
||||
return this.deckRepo.update(id, userId, data);
|
||||
}
|
||||
async updateDeck(id: string, userId: string, data: UpdateDeckInput) {
|
||||
return this.deckRepo.update(id, userId, data);
|
||||
}
|
||||
|
||||
async deleteDeck(id: string, userId: string) {
|
||||
return this.deckRepo.delete(id, userId);
|
||||
}
|
||||
async deleteDeck(id: string, userId: string) {
|
||||
return this.deckRepo.delete(id, userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -498,37 +524,33 @@ import { getToken } from '$lib/auth';
|
|||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
async function fetchApi<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
async function fetchApi<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = getToken();
|
||||
|
||||
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'API Error');
|
||||
}
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'API Error');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(endpoint: string) => fetchApi<T>(endpoint),
|
||||
post: <T>(endpoint: string, data: unknown) =>
|
||||
fetchApi<T>(endpoint, { method: 'POST', body: JSON.stringify(data) }),
|
||||
put: <T>(endpoint: string, data: unknown) =>
|
||||
fetchApi<T>(endpoint, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: <T>(endpoint: string) =>
|
||||
fetchApi<T>(endpoint, { method: 'DELETE' }),
|
||||
get: <T>(endpoint: string) => fetchApi<T>(endpoint),
|
||||
post: <T>(endpoint: string, data: unknown) =>
|
||||
fetchApi<T>(endpoint, { method: 'POST', body: JSON.stringify(data) }),
|
||||
put: <T>(endpoint: string, data: unknown) =>
|
||||
fetchApi<T>(endpoint, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: <T>(endpoint: string) => fetchApi<T>(endpoint, { method: 'DELETE' }),
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -540,87 +562,95 @@ import { api } from '$lib/api/client';
|
|||
import type { Deck, CreateDeckInput, UpdateDeckInput } from '$lib/types/deck';
|
||||
|
||||
function createDeckStore() {
|
||||
let decks = $state<Deck[]>([]);
|
||||
let currentDeck = $state<Deck | null>(null);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let decks = $state<Deck[]>([]);
|
||||
let currentDeck = $state<Deck | null>(null);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
return {
|
||||
get decks() { return decks; },
|
||||
get currentDeck() { return currentDeck; },
|
||||
get loading() { return loading; },
|
||||
get error() { return error; },
|
||||
return {
|
||||
get decks() {
|
||||
return decks;
|
||||
},
|
||||
get currentDeck() {
|
||||
return currentDeck;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
async fetchDecks() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
decks = await api.get<Deck[]>('/api/decks');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch decks';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
async fetchDecks() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
decks = await api.get<Deck[]>('/api/decks');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch decks';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchDeck(id: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
currentDeck = await api.get<Deck>(`/api/decks/${id}`);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch deck';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
async fetchDeck(id: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
currentDeck = await api.get<Deck>(`/api/decks/${id}`);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch deck';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async createDeck(data: CreateDeckInput) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const deck = await api.post<Deck>('/api/decks', data);
|
||||
decks = [deck, ...decks];
|
||||
return deck;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create deck';
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
async createDeck(data: CreateDeckInput) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const deck = await api.post<Deck>('/api/decks', data);
|
||||
decks = [deck, ...decks];
|
||||
return deck;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create deck';
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async updateDeck(id: string, data: UpdateDeckInput) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const deck = await api.put<Deck>(`/api/decks/${id}`, data);
|
||||
decks = decks.map(d => d.id === id ? deck : d);
|
||||
if (currentDeck?.id === id) currentDeck = deck;
|
||||
return deck;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update deck';
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
async updateDeck(id: string, data: UpdateDeckInput) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const deck = await api.put<Deck>(`/api/decks/${id}`, data);
|
||||
decks = decks.map((d) => (d.id === id ? deck : d));
|
||||
if (currentDeck?.id === id) currentDeck = deck;
|
||||
return deck;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update deck';
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteDeck(id: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
await api.delete(`/api/decks/${id}`);
|
||||
decks = decks.filter(d => d.id !== id);
|
||||
if (currentDeck?.id === id) currentDeck = null;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete deck';
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
async deleteDeck(id: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
await api.delete(`/api/decks/${id}`);
|
||||
decks = decks.filter((d) => d.id !== id);
|
||||
if (currentDeck?.id === id) currentDeck = null;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete deck';
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const deckStore = createDeckStore();
|
||||
|
|
@ -629,6 +659,7 @@ export const deckStore = createDeckStore();
|
|||
### Phase 5: Testing & Cutover (Tag 7-10)
|
||||
|
||||
#### 5.1 Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/deck.integration.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
|
|
@ -637,111 +668,111 @@ import { decks } from '@manadeck/database/schema';
|
|||
import { DeckService } from '../services/deck.service';
|
||||
|
||||
describe('DeckService', () => {
|
||||
const service = new DeckService();
|
||||
const testUserId = 'test-user-id';
|
||||
const service = new DeckService();
|
||||
const testUserId = 'test-user-id';
|
||||
|
||||
afterAll(async () => {
|
||||
await db.delete(decks).where(eq(decks.userId, testUserId));
|
||||
});
|
||||
afterAll(async () => {
|
||||
await db.delete(decks).where(eq(decks.userId, testUserId));
|
||||
});
|
||||
|
||||
it('should create a deck', async () => {
|
||||
const deck = await service.createDeck(testUserId, {
|
||||
title: 'Test Deck',
|
||||
description: 'A test deck',
|
||||
});
|
||||
it('should create a deck', async () => {
|
||||
const deck = await service.createDeck(testUserId, {
|
||||
title: 'Test Deck',
|
||||
description: 'A test deck',
|
||||
});
|
||||
|
||||
expect(deck.id).toBeDefined();
|
||||
expect(deck.title).toBe('Test Deck');
|
||||
});
|
||||
expect(deck.id).toBeDefined();
|
||||
expect(deck.title).toBe('Test Deck');
|
||||
});
|
||||
|
||||
it('should fetch user decks', async () => {
|
||||
const userDecks = await service.getUserDecks(testUserId);
|
||||
expect(userDecks.length).toBeGreaterThan(0);
|
||||
});
|
||||
it('should fetch user decks', async () => {
|
||||
const userDecks = await service.getUserDecks(testUserId);
|
||||
expect(userDecks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 5.2 Datenmigrations-Script
|
||||
|
||||
```typescript
|
||||
// scripts/migrate-data.ts
|
||||
import { db as newDb } from '@manadeck/database';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { decks, cards } from '@manadeck/database/schema';
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_KEY!
|
||||
);
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!);
|
||||
|
||||
async function migrateDecks() {
|
||||
console.log('Migrating decks...');
|
||||
console.log('Migrating decks...');
|
||||
|
||||
const { data: supabaseDecks, error } = await supabase
|
||||
.from('decks')
|
||||
.select('*');
|
||||
const { data: supabaseDecks, error } = await supabase.from('decks').select('*');
|
||||
|
||||
if (error) throw error;
|
||||
if (error) throw error;
|
||||
|
||||
for (const deck of supabaseDecks) {
|
||||
await newDb.insert(decks).values({
|
||||
id: deck.id,
|
||||
userId: deck.user_id,
|
||||
title: deck.title,
|
||||
description: deck.description,
|
||||
coverImageUrl: deck.cover_image_url,
|
||||
isPublic: deck.is_public,
|
||||
isFeatured: deck.is_featured,
|
||||
featuredAt: deck.featured_at,
|
||||
settings: deck.settings,
|
||||
tags: deck.tags,
|
||||
metadata: deck.metadata,
|
||||
createdAt: deck.created_at,
|
||||
updatedAt: deck.updated_at,
|
||||
}).onConflictDoNothing();
|
||||
}
|
||||
for (const deck of supabaseDecks) {
|
||||
await newDb
|
||||
.insert(decks)
|
||||
.values({
|
||||
id: deck.id,
|
||||
userId: deck.user_id,
|
||||
title: deck.title,
|
||||
description: deck.description,
|
||||
coverImageUrl: deck.cover_image_url,
|
||||
isPublic: deck.is_public,
|
||||
isFeatured: deck.is_featured,
|
||||
featuredAt: deck.featured_at,
|
||||
settings: deck.settings,
|
||||
tags: deck.tags,
|
||||
metadata: deck.metadata,
|
||||
createdAt: deck.created_at,
|
||||
updatedAt: deck.updated_at,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
console.log(`Migrated ${supabaseDecks.length} decks`);
|
||||
console.log(`Migrated ${supabaseDecks.length} decks`);
|
||||
}
|
||||
|
||||
async function migrateCards() {
|
||||
console.log('Migrating cards...');
|
||||
console.log('Migrating cards...');
|
||||
|
||||
const { data: supabaseCards, error } = await supabase
|
||||
.from('cards')
|
||||
.select('*');
|
||||
const { data: supabaseCards, error } = await supabase.from('cards').select('*');
|
||||
|
||||
if (error) throw error;
|
||||
if (error) throw error;
|
||||
|
||||
for (const card of supabaseCards) {
|
||||
await newDb.insert(cards).values({
|
||||
id: card.id,
|
||||
deckId: card.deck_id,
|
||||
position: card.position,
|
||||
title: card.title,
|
||||
content: card.content,
|
||||
cardType: card.card_type,
|
||||
aiModel: card.ai_model,
|
||||
aiPrompt: card.ai_prompt,
|
||||
version: card.version,
|
||||
isFavorite: card.is_favorite,
|
||||
createdAt: card.created_at,
|
||||
updatedAt: card.updated_at,
|
||||
}).onConflictDoNothing();
|
||||
}
|
||||
for (const card of supabaseCards) {
|
||||
await newDb
|
||||
.insert(cards)
|
||||
.values({
|
||||
id: card.id,
|
||||
deckId: card.deck_id,
|
||||
position: card.position,
|
||||
title: card.title,
|
||||
content: card.content,
|
||||
cardType: card.card_type,
|
||||
aiModel: card.ai_model,
|
||||
aiPrompt: card.ai_prompt,
|
||||
version: card.version,
|
||||
isFavorite: card.is_favorite,
|
||||
createdAt: card.created_at,
|
||||
updatedAt: card.updated_at,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
console.log(`Migrated ${supabaseCards.length} cards`);
|
||||
console.log(`Migrated ${supabaseCards.length} cards`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await migrateDecks();
|
||||
await migrateCards();
|
||||
// ... andere Tabellen
|
||||
console.log('Migration completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
await migrateDecks();
|
||||
await migrateCards();
|
||||
// ... andere Tabellen
|
||||
console.log('Migration completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -751,13 +782,13 @@ main();
|
|||
|
||||
## Zeitplan
|
||||
|
||||
| Phase | Beschreibung | Dauer | Status |
|
||||
|-------|--------------|-------|--------|
|
||||
| 1 | Setup (PostgreSQL, Package) | 1-2 Tage | ⬜ Pending |
|
||||
| 2 | Schema & Migration | 1-2 Tage | ⬜ Pending |
|
||||
| 3 | Backend Migration | 2-3 Tage | ⬜ Pending |
|
||||
| 4 | Frontend Migration | 2-3 Tage | ⬜ Pending |
|
||||
| 5 | Testing & Cutover | 2-3 Tage | ⬜ Pending |
|
||||
| Phase | Beschreibung | Dauer | Status |
|
||||
| ----- | --------------------------- | -------- | ---------- |
|
||||
| 1 | Setup (PostgreSQL, Package) | 1-2 Tage | ⬜ Pending |
|
||||
| 2 | Schema & Migration | 1-2 Tage | ⬜ Pending |
|
||||
| 3 | Backend Migration | 2-3 Tage | ⬜ Pending |
|
||||
| 4 | Frontend Migration | 2-3 Tage | ⬜ Pending |
|
||||
| 5 | Testing & Cutover | 2-3 Tage | ⬜ Pending |
|
||||
|
||||
**Gesamtdauer: ~10-13 Tage**
|
||||
|
||||
|
|
@ -766,6 +797,7 @@ main();
|
|||
## Checkliste
|
||||
|
||||
### Pre-Migration
|
||||
|
||||
- [ ] PostgreSQL Server aufgesetzt
|
||||
- [ ] Database Package erstellt
|
||||
- [ ] Drizzle Schema definiert
|
||||
|
|
@ -773,6 +805,7 @@ main();
|
|||
- [ ] Supabase Daten exportiert
|
||||
|
||||
### Backend
|
||||
|
||||
- [ ] Repository Pattern implementiert
|
||||
- [ ] DeckRepository
|
||||
- [ ] CardRepository
|
||||
|
|
@ -786,18 +819,21 @@ main();
|
|||
- [ ] Authorization Middleware (ersetzt RLS)
|
||||
|
||||
### Frontend
|
||||
|
||||
- [ ] API Client erstellt
|
||||
- [ ] deckStore migriert
|
||||
- [ ] Supabase imports entfernt
|
||||
- [ ] Environment Variables aktualisiert
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Unit Tests für Repositories
|
||||
- [ ] Integration Tests für Services
|
||||
- [ ] E2E Tests für API
|
||||
- [ ] Manual Testing aller Features
|
||||
|
||||
### Cutover
|
||||
|
||||
- [ ] Daten migriert (Script ausgeführt)
|
||||
- [ ] DNS/Environment umgestellt
|
||||
- [ ] Rollback Plan dokumentiert
|
||||
|
|
@ -819,12 +855,12 @@ Supabase-Daten bleiben während der Migration unberührt.
|
|||
|
||||
## Kosten nach Migration
|
||||
|
||||
| Service | Geschätzte Kosten |
|
||||
|---------|-------------------|
|
||||
| PostgreSQL (Railway) | ~$5-20/Monat |
|
||||
| PostgreSQL (Neon Free) | $0/Monat |
|
||||
| Backup (optional) | ~$5/Monat |
|
||||
| **Gesamt** | **$5-25/Monat** |
|
||||
| Service | Geschätzte Kosten |
|
||||
| ---------------------- | ----------------- |
|
||||
| PostgreSQL (Railway) | ~$5-20/Monat |
|
||||
| PostgreSQL (Neon Free) | $0/Monat |
|
||||
| Backup (optional) | ~$5/Monat |
|
||||
| **Gesamt** | **$5-25/Monat** |
|
||||
|
||||
vs. Supabase Pro: $25-300/Monat
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue