From 488489944d6ef0208d2d79517ecaeff05d2bdf97 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 11:56:25 +0200 Subject: [PATCH] chore(packages): remove 4 dead zero-consumer packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-launch audit found 4 packages with zero workspace consumers that were leftover from before the consolidation: - @mana/cards-database (1475 LOC) Pre-consolidation flashcard backend with its own Docker Compose and Drizzle config. Replaced by the cards module in the unified Mana app: apps/mana/apps/web/src/lib/modules/cards/. Now uses Dexie + mana-sync against mana_platform. - @mana/shared-api-client (1110 LOC) Generic Go-style {data, error} REST client. Only reference left was a string entry in shared-vite-config's noExternal list (not a real import). - @mana/shared-errors (1791 LOC) NestJS-coupled exception filter package from before the Hono migration. The Hono replacement (serviceErrorHandler in @mana/shared-hono) ships in a separate commit. Result + ErrorCode enum bits had no consumers and weren't worth saving standalone — if a need emerges they can grow organically. - @mana/shared-splitscreen (694 LOC) Side-by-side panel layout components. No code consumers; only referenced from shared-vite-config noExternal and an old design doc. The unified Mana app uses its own workbench scenes for multi-pane layouts. Verified zero code consumers via grep across .ts/.svelte/.json before deletion. apps/api type-check stays at 0 errors after the sweep, mana-auth tests still 19/19 passing. Also clean packages/shared-vite-config/src/index.ts noExternal list while we're here: drop the two deleted entries plus 8 ghost packages (shared-feedback-ui/-service/-types, shared-help-ui/ -types/-content, shared-profile-ui, shared-subscription-ui) that were referenced by name but never existed in packages/. List goes from 22 → 12 entries. Net: ~5070 LOC + workspace declarations removed. Tracked as item #29 in docs/REFACTORING_AUDIT_2026_04.md. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cards-database/.env.example | 10 - packages/cards-database/.gitignore | 23 -- packages/cards-database/docker-compose.yml | 39 --- packages/cards-database/drizzle.config.ts | 14 - packages/cards-database/package.json | 58 ---- packages/cards-database/src/client.ts | 97 ------ packages/cards-database/src/index.ts | 34 -- .../src/migrate-from-supabase.ts | 236 ------------- packages/cards-database/src/migrate.ts | 23 -- .../src/schema/aiGenerations.ts | 55 --- .../cards-database/src/schema/cardProgress.ts | 58 ---- packages/cards-database/src/schema/cards.ts | 82 ----- .../src/schema/dailyProgress.ts | 28 -- .../src/schema/deckTemplates.ts | 46 --- packages/cards-database/src/schema/decks.ts | 40 --- packages/cards-database/src/schema/index.ts | 19 -- packages/cards-database/src/schema/schema.ts | 3 - .../src/schema/studySessions.ts | 40 --- .../cards-database/src/schema/userStats.ts | 26 -- packages/cards-database/src/seed.ts | 105 ------ .../cards-database/src/test-connection.ts | 44 --- packages/cards-database/tsconfig.json | 17 - packages/shared-api-client/package.json | 24 -- packages/shared-api-client/src/client.ts | 305 ----------------- packages/shared-api-client/src/index.ts | 51 --- packages/shared-api-client/src/types.ts | 112 ------ packages/shared-api-client/src/utils.ts | 87 ----- packages/shared-api-client/tsconfig.json | 17 - packages/shared-errors/package.json | 45 --- .../shared-errors/src/errors/app-error.ts | 175 ---------- .../shared-errors/src/errors/auth-error.ts | 79 ----- .../shared-errors/src/errors/credit-error.ts | 31 -- .../src/errors/database-error.ts | 45 --- packages/shared-errors/src/errors/index.ts | 9 - .../shared-errors/src/errors/network-error.ts | 53 --- .../src/errors/not-found-error.ts | 42 --- .../src/errors/rate-limit-error.ts | 31 -- .../shared-errors/src/errors/service-error.ts | 91 ----- .../src/errors/validation-error.ts | 60 ---- packages/shared-errors/src/guards/index.ts | 1 - .../shared-errors/src/guards/type-guards.ts | 158 --------- packages/shared-errors/src/index.ts | 100 ------ .../src/nestjs/app-exception.filter.ts | 238 ------------- packages/shared-errors/src/nestjs/index.ts | 1 - .../shared-errors/src/types/error-codes.ts | 162 --------- packages/shared-errors/src/types/index.ts | 2 - packages/shared-errors/src/types/result.ts | 319 ------------------ packages/shared-errors/src/utils/index.ts | 1 - packages/shared-errors/src/utils/wrap.ts | 93 ----- packages/shared-errors/tsconfig.build.json | 18 - packages/shared-errors/tsconfig.json | 19 -- packages/shared-splitscreen/package.json | 39 --- .../src/components/AppPanel.svelte | 155 --------- .../src/components/PanelControls.svelte | 112 ------ .../src/components/ResizeHandle.svelte | 203 ----------- .../src/components/SplitPaneContainer.svelte | 139 -------- packages/shared-splitscreen/src/index.ts | 46 --- .../src/stores/split-panel.svelte.ts | 251 -------------- packages/shared-splitscreen/src/types.ts | 88 ----- .../shared-splitscreen/src/utils/index.ts | 13 - .../src/utils/local-storage.ts | 97 ------ .../shared-splitscreen/src/utils/url-state.ts | 65 ---- packages/shared-splitscreen/tsconfig.json | 18 - packages/shared-vite-config/src/index.ts | 10 - 64 files changed, 4702 deletions(-) delete mode 100644 packages/cards-database/.env.example delete mode 100644 packages/cards-database/.gitignore delete mode 100644 packages/cards-database/docker-compose.yml delete mode 100644 packages/cards-database/drizzle.config.ts delete mode 100644 packages/cards-database/package.json delete mode 100644 packages/cards-database/src/client.ts delete mode 100644 packages/cards-database/src/index.ts delete mode 100644 packages/cards-database/src/migrate-from-supabase.ts delete mode 100644 packages/cards-database/src/migrate.ts delete mode 100644 packages/cards-database/src/schema/aiGenerations.ts delete mode 100644 packages/cards-database/src/schema/cardProgress.ts delete mode 100644 packages/cards-database/src/schema/cards.ts delete mode 100644 packages/cards-database/src/schema/dailyProgress.ts delete mode 100644 packages/cards-database/src/schema/deckTemplates.ts delete mode 100644 packages/cards-database/src/schema/decks.ts delete mode 100644 packages/cards-database/src/schema/index.ts delete mode 100644 packages/cards-database/src/schema/schema.ts delete mode 100644 packages/cards-database/src/schema/studySessions.ts delete mode 100644 packages/cards-database/src/schema/userStats.ts delete mode 100644 packages/cards-database/src/seed.ts delete mode 100644 packages/cards-database/src/test-connection.ts delete mode 100644 packages/cards-database/tsconfig.json delete mode 100644 packages/shared-api-client/package.json delete mode 100644 packages/shared-api-client/src/client.ts delete mode 100644 packages/shared-api-client/src/index.ts delete mode 100644 packages/shared-api-client/src/types.ts delete mode 100644 packages/shared-api-client/src/utils.ts delete mode 100644 packages/shared-api-client/tsconfig.json delete mode 100644 packages/shared-errors/package.json delete mode 100644 packages/shared-errors/src/errors/app-error.ts delete mode 100644 packages/shared-errors/src/errors/auth-error.ts delete mode 100644 packages/shared-errors/src/errors/credit-error.ts delete mode 100644 packages/shared-errors/src/errors/database-error.ts delete mode 100644 packages/shared-errors/src/errors/index.ts delete mode 100644 packages/shared-errors/src/errors/network-error.ts delete mode 100644 packages/shared-errors/src/errors/not-found-error.ts delete mode 100644 packages/shared-errors/src/errors/rate-limit-error.ts delete mode 100644 packages/shared-errors/src/errors/service-error.ts delete mode 100644 packages/shared-errors/src/errors/validation-error.ts delete mode 100644 packages/shared-errors/src/guards/index.ts delete mode 100644 packages/shared-errors/src/guards/type-guards.ts delete mode 100644 packages/shared-errors/src/index.ts delete mode 100644 packages/shared-errors/src/nestjs/app-exception.filter.ts delete mode 100644 packages/shared-errors/src/nestjs/index.ts delete mode 100644 packages/shared-errors/src/types/error-codes.ts delete mode 100644 packages/shared-errors/src/types/index.ts delete mode 100644 packages/shared-errors/src/types/result.ts delete mode 100644 packages/shared-errors/src/utils/index.ts delete mode 100644 packages/shared-errors/src/utils/wrap.ts delete mode 100644 packages/shared-errors/tsconfig.build.json delete mode 100644 packages/shared-errors/tsconfig.json delete mode 100644 packages/shared-splitscreen/package.json delete mode 100644 packages/shared-splitscreen/src/components/AppPanel.svelte delete mode 100644 packages/shared-splitscreen/src/components/PanelControls.svelte delete mode 100644 packages/shared-splitscreen/src/components/ResizeHandle.svelte delete mode 100644 packages/shared-splitscreen/src/components/SplitPaneContainer.svelte delete mode 100644 packages/shared-splitscreen/src/index.ts delete mode 100644 packages/shared-splitscreen/src/stores/split-panel.svelte.ts delete mode 100644 packages/shared-splitscreen/src/types.ts delete mode 100644 packages/shared-splitscreen/src/utils/index.ts delete mode 100644 packages/shared-splitscreen/src/utils/local-storage.ts delete mode 100644 packages/shared-splitscreen/src/utils/url-state.ts delete mode 100644 packages/shared-splitscreen/tsconfig.json diff --git a/packages/cards-database/.env.example b/packages/cards-database/.env.example deleted file mode 100644 index 8b0ed2c55..000000000 --- a/packages/cards-database/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# Database connection URL -# Format: postgresql://user:password@host:port/database -DATABASE_URL=postgresql://postgres:password@localhost:5432/cards - -# Alternative name (used as fallback) -CARDS_DATABASE_URL=postgresql://postgres:password@localhost:5432/cards - -# Supabase credentials (only needed for migration) -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_SERVICE_KEY=your-service-role-key diff --git a/packages/cards-database/.gitignore b/packages/cards-database/.gitignore deleted file mode 100644 index dcec63d4c..000000000 --- a/packages/cards-database/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# Environment variables -.env -.env.local -.env.*.local - -# Dependencies -node_modules/ - -# Build output -dist/ - -# Drizzle migrations (optional - can be tracked) -# drizzle/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db diff --git a/packages/cards-database/docker-compose.yml b/packages/cards-database/docker-compose.yml deleted file mode 100644 index b421904ce..000000000 --- a/packages/cards-database/docker-compose.yml +++ /dev/null @@ -1,39 +0,0 @@ -services: - postgres: - image: postgres:16-alpine - container_name: cards-postgres - restart: unless-stopped - environment: - POSTGRES_USER: cards - POSTGRES_PASSWORD: cards_dev_password - POSTGRES_DB: cards - ports: - - "5433:5432" - volumes: - - cards_postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U cards -d cards"] - interval: 10s - timeout: 5s - retries: 5 - - # Optional: pgAdmin for database management UI - pgadmin: - image: dpage/pgadmin4:latest - container_name: cards-pgadmin - restart: unless-stopped - environment: - PGADMIN_DEFAULT_EMAIL: admin@cards.local - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - ports: - - "5050:80" - volumes: - - cards_pgadmin_data:/var/lib/pgadmin - depends_on: - postgres: - condition: service_healthy - -volumes: - cards_postgres_data: - cards_pgadmin_data: diff --git a/packages/cards-database/drizzle.config.ts b/packages/cards-database/drizzle.config.ts deleted file mode 100644 index bfc8a9599..000000000 --- a/packages/cards-database/drizzle.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'drizzle-kit'; - -export default defineConfig({ - schema: './src/schema/*.ts', - out: './drizzle', - dialect: 'postgresql', - dbCredentials: { - url: - process.env.DATABASE_URL || 'postgresql://mana:devpassword@localhost:5432/mana_platform', - }, - schemaFilter: ['cards'], - verbose: true, - strict: true, -}); diff --git a/packages/cards-database/package.json b/packages/cards-database/package.json deleted file mode 100644 index 1fb34583c..000000000 --- a/packages/cards-database/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@mana/cards-database", - "version": "1.0.0", - "private": true, - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "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", - "db:generate": "dotenv -- drizzle-kit generate", - "db:migrate": "dotenv -- drizzle-kit migrate", - "db:push": "dotenv -- drizzle-kit push --force", - "db:studio": "dotenv -- drizzle-kit studio", - "db:seed": "dotenv -- tsx src/seed.ts", - "db:migrate-from-supabase": "dotenv -- tsx src/migrate-from-supabase.ts", - "db:reset": "docker compose down -v && docker compose up -d && sleep 3 && pnpm db:push", - "db:test": "dotenv -- tsx src/test-connection.ts", - "type-check": "tsc --noEmit", - "lint": "eslint ." - }, - "dependencies": { - "drizzle-orm": "^0.36.0", - "postgres": "^3.4.5" - }, - "devDependencies": { - "@supabase/supabase-js": "^2.81.1", - "dotenv-cli": "^7.4.0", - "drizzle-kit": "^0.28.0", - "tsx": "^4.19.0", - "typescript": "^5.7.3", - "@types/node": "^22.10.0" - } -} diff --git a/packages/cards-database/src/client.ts b/packages/cards-database/src/client.ts deleted file mode 100644 index e3ea23a56..000000000 --- a/packages/cards-database/src/client.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema/index.js'; - -// Singleton instance for the database client -let dbInstance: ReturnType> | null = null; -let pgClient: ReturnType | null = null; - -/** - * Get the database URL from environment variables - */ -function getDatabaseUrl(): string { - const url = process.env.DATABASE_URL || process.env.CARDS_DATABASE_URL; - if (!url) { - throw new Error( - 'Database URL not found. Set DATABASE_URL or CARDS_DATABASE_URL environment variable.' - ); - } - return url; -} - -/** - * Create a new database client - * Uses connection pooling with sensible defaults for serverless environments - */ -export function createClient(connectionString?: string) { - const url = connectionString || getDatabaseUrl(); - - const client = postgres(url, { - max: 10, // Maximum connections in the pool - idle_timeout: 20, // Close idle connections after 20 seconds - connect_timeout: 10, // Connection timeout in seconds - prepare: false, // Disable prepared statements for serverless - }); - - return drizzle(client, { schema }); -} - -/** - * Get the singleton database instance - * Creates a new instance if one doesn't exist - */ -export function getDb() { - if (!dbInstance) { - const url = getDatabaseUrl(); - pgClient = postgres(url, { - max: 10, - idle_timeout: 20, - connect_timeout: 10, - prepare: false, - }); - dbInstance = drizzle(pgClient, { schema }); - } - return dbInstance; -} - -/** - * Close the database connection - * Should be called when shutting down the application - */ -export async function closeDb() { - if (pgClient) { - await pgClient.end(); - pgClient = null; - dbInstance = null; - } -} - -// Export the database type for typing purposes -export type Database = ReturnType; - -// Re-export commonly used Drizzle utilities -export { - eq, - ne, - gt, - gte, - lt, - lte, - and, - or, - not, - inArray, - notInArray, - isNull, - isNotNull, - like, - ilike, - sql, - asc, - desc, - count, - sum, - avg, - min, - max, -} from 'drizzle-orm'; diff --git a/packages/cards-database/src/index.ts b/packages/cards-database/src/index.ts deleted file mode 100644 index b81c2195d..000000000 --- a/packages/cards-database/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Main entry point for @mana/cards-database - -// Export database client utilities -export { createClient, getDb, closeDb, type Database } from './client.js'; - -// Export Drizzle utilities -export { - eq, - ne, - gt, - gte, - lt, - lte, - and, - or, - not, - inArray, - notInArray, - isNull, - isNotNull, - like, - ilike, - sql, - asc, - desc, - count, - sum, - avg, - min, - max, -} from './client.js'; - -// Export all schemas and types -export * from './schema/index.js'; diff --git a/packages/cards-database/src/migrate-from-supabase.ts b/packages/cards-database/src/migrate-from-supabase.ts deleted file mode 100644 index 125dacc30..000000000 --- a/packages/cards-database/src/migrate-from-supabase.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Migration script to move data from Supabase to the new PostgreSQL database - * - * Prerequisites: - * 1. Set SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables - * 2. Set DATABASE_URL for the new PostgreSQL database - * 3. Run migrations on the new database first: pnpm db:migrate - * - * Usage: - * SUPABASE_URL=... SUPABASE_SERVICE_KEY=... DATABASE_URL=... tsx src/migrate-from-supabase.ts - */ - -import { createClient as createSupabaseClient } from '@supabase/supabase-js'; -import { getDb, closeDb } from './client.js'; -import { - decks, - cards, - studySessions, - cardProgress, - deckTemplates, - aiGenerations, - userStats, -} from './schema/index.js'; - -// Initialize Supabase client -const supabaseUrl = process.env.SUPABASE_URL; -const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY; - -if (!supabaseUrl || !supabaseServiceKey) { - console.error('Missing SUPABASE_URL or SUPABASE_SERVICE_KEY environment variables'); - process.exit(1); -} - -const supabase = createSupabaseClient(supabaseUrl, supabaseServiceKey); -const db = getDb(); - -interface MigrationStats { - table: string; - migrated: number; - errors: number; -} - -const stats: MigrationStats[] = []; - -async function migrateTable( - tableName: string, - supabaseTableName: string, - drizzleTable: any, - transformer: (row: any) => T -) { - console.log(`\nMigrating ${tableName}...`); - let migrated = 0; - let errors = 0; - - try { - // Fetch all data from Supabase - const { data, error } = await supabase.from(supabaseTableName).select('*'); - - if (error) { - console.error(`Error fetching ${tableName}:`, error); - stats.push({ table: tableName, migrated: 0, errors: 1 }); - return; - } - - if (!data || data.length === 0) { - console.log(`No data found in ${tableName}`); - stats.push({ table: tableName, migrated: 0, errors: 0 }); - return; - } - - console.log(`Found ${data.length} rows in ${tableName}`); - - // Process in batches of 100 - const batchSize = 100; - for (let i = 0; i < data.length; i += batchSize) { - const batch = data.slice(i, i + batchSize); - const transformed = batch.map(transformer); - - try { - await db.insert(drizzleTable).values(transformed).onConflictDoNothing(); - migrated += batch.length; - process.stdout.write(`\r Migrated ${migrated}/${data.length} rows`); - } catch (err) { - console.error(`\n Error inserting batch:`, err); - errors += batch.length; - } - } - - console.log(`\n Completed: ${migrated} migrated, ${errors} errors`); - } catch (err) { - console.error(`Error migrating ${tableName}:`, err); - errors++; - } - - stats.push({ table: tableName, migrated, errors }); -} - -async function main() { - console.log('=== Cards Data Migration ==='); - console.log('From: Supabase'); - console.log('To: PostgreSQL (Drizzle)'); - console.log('==============================\n'); - - try { - // 1. Migrate decks - await migrateTable('decks', 'decks', decks, (row) => ({ - id: row.id, - userId: row.user_id, - title: row.title, - description: row.description, - coverImageUrl: row.cover_image_url, - isPublic: row.is_public ?? false, - isFeatured: row.is_featured ?? false, - featuredAt: row.featured_at ? new Date(row.featured_at) : null, - settings: row.settings ?? {}, - tags: row.tags ?? [], - metadata: row.metadata ?? {}, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - })); - - // 2. Migrate cards - await migrateTable('cards', 'cards', cards, (row) => ({ - id: row.id, - deckId: row.deck_id, - position: row.position ?? 0, - title: row.title, - content: row.content, - cardType: row.card_type, - aiModel: row.ai_model, - aiPrompt: row.ai_prompt, - version: row.version ?? 1, - isFavorite: row.is_favorite ?? false, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - })); - - // 3. Migrate study sessions - await migrateTable('study_sessions', 'study_sessions', studySessions, (row) => ({ - id: row.id, - deckId: row.deck_id, - userId: row.user_id, - mode: row.mode, - totalCards: row.total_cards ?? 0, - completedCards: row.completed_cards ?? 0, - correctCards: row.correct_cards ?? 0, - startedAt: new Date(row.started_at), - completedAt: row.completed_at ? new Date(row.completed_at) : null, - timeSpentSeconds: row.time_spent_seconds ?? 0, - })); - - // 4. Migrate card progress - await migrateTable('card_progress', 'card_progress', cardProgress, (row) => ({ - id: row.id, - userId: row.user_id, - cardId: row.card_id, - easeFactor: row.ease_factor?.toString() ?? '2.5', - interval: row.interval ?? 0, - repetitions: row.repetitions ?? 0, - lastReviewed: row.last_reviewed ? new Date(row.last_reviewed) : null, - nextReview: row.next_review ? new Date(row.next_review) : null, - status: row.status ?? 'new', - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - })); - - // 5. Migrate deck templates - await migrateTable('deck_templates', 'deck_templates', deckTemplates, (row) => ({ - id: row.id, - title: row.title, - description: row.description, - category: row.category, - templateData: row.template_data ?? { cards: [] }, - isActive: row.is_active ?? true, - isPublic: row.is_public ?? true, - popularity: row.popularity ?? 0, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - })); - - // 6. Migrate AI generations - await migrateTable('ai_generations', 'ai_generations', aiGenerations, (row) => ({ - id: row.id, - userId: row.user_id, - deckId: row.deck_id, - functionName: row.function_name, - prompt: row.prompt, - model: row.model, - status: row.status ?? 'pending', - metadata: row.metadata ?? {}, - completedAt: row.completed_at ? new Date(row.completed_at) : null, - createdAt: new Date(row.created_at), - })); - - // 7. Migrate user stats - await migrateTable('user_stats', 'user_stats', userStats, (row) => ({ - userId: row.user_id, - totalWins: row.total_wins ?? 0, - totalSessions: row.total_sessions ?? 0, - totalCardsStudied: row.total_cards_studied ?? 0, - totalTimeSeconds: row.total_time_seconds ?? 0, - averageAccuracy: row.average_accuracy?.toString() ?? '0', - streakDays: row.streak_days ?? 0, - longestStreak: row.longest_streak ?? 0, - lastStudyDate: row.last_study_date, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - })); - - // Print summary - console.log('\n\n=== Migration Summary ==='); - console.log('-------------------------'); - let totalMigrated = 0; - let totalErrors = 0; - for (const stat of stats) { - console.log(`${stat.table}: ${stat.migrated} migrated, ${stat.errors} errors`); - totalMigrated += stat.migrated; - totalErrors += stat.errors; - } - console.log('-------------------------'); - console.log(`Total: ${totalMigrated} rows migrated, ${totalErrors} errors`); - - if (totalErrors === 0) { - console.log('\n✅ Migration completed successfully!'); - } else { - console.log('\n⚠️ Migration completed with some errors. Please review.'); - } - } catch (error) { - console.error('Migration failed:', error); - process.exit(1); - } finally { - await closeDb(); - } -} - -main(); diff --git a/packages/cards-database/src/migrate.ts b/packages/cards-database/src/migrate.ts deleted file mode 100644 index ce47ebb38..000000000 --- a/packages/cards-database/src/migrate.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { migrate } from 'drizzle-orm/postgres-js/migrator'; -import { createClient } from './client.js'; -import path from 'path'; - -async function runMigrations() { - console.log('Running migrations...'); - - const db = createClient(); - - try { - await migrate(db, { - migrationsFolder: path.join(__dirname, '../drizzle'), - }); - console.log('Migrations completed successfully!'); - } catch (error) { - console.error('Migration failed:', error); - process.exit(1); - } - - process.exit(0); -} - -runMigrations(); diff --git a/packages/cards-database/src/schema/aiGenerations.ts b/packages/cards-database/src/schema/aiGenerations.ts deleted file mode 100644 index 8daed4115..000000000 --- a/packages/cards-database/src/schema/aiGenerations.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { uuid, text, varchar, timestamp, jsonb, index, pgEnum } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { cardsSchema } from './schema.js'; -import { decks } from './decks.js'; - -// AI generation status enum -export const aiGenerationStatusEnum = pgEnum('ai_generation_status', [ - 'pending', - 'processing', - 'completed', - 'failed', -]); - -// AI generation metadata structure -export interface AIGenerationMetadata { - inputTokens?: number; - outputTokens?: number; - totalTokens?: number; - duration?: number; - error?: string; - cardCount?: number; - [key: string]: unknown; -} - -export const aiGenerations = cardsSchema.table( - 'ai_generations', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - deckId: uuid('deck_id').references(() => decks.id, { onDelete: 'set null' }), - functionName: varchar('function_name', { length: 100 }).notNull(), - prompt: text('prompt').notNull(), - model: varchar('model', { length: 100 }), - status: aiGenerationStatusEnum('status').default('pending').notNull(), - metadata: jsonb('metadata').default({}).$type(), - completedAt: timestamp('completed_at', { withTimezone: true }), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_ai_generations_user_id').on(table.userId), - index('idx_ai_generations_deck_id').on(table.deckId), - index('idx_ai_generations_status').on(table.status), - index('idx_ai_generations_created_at').on(table.createdAt), - ] -); - -export const aiGenerationsRelations = relations(aiGenerations, ({ one }) => ({ - deck: one(decks, { - fields: [aiGenerations.deckId], - references: [decks.id], - }), -})); - -export type AIGeneration = typeof aiGenerations.$inferSelect; -export type NewAIGeneration = typeof aiGenerations.$inferInsert; diff --git a/packages/cards-database/src/schema/cardProgress.ts b/packages/cards-database/src/schema/cardProgress.ts deleted file mode 100644 index 9105271c2..000000000 --- a/packages/cards-database/src/schema/cardProgress.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - uuid, - text, - integer, - timestamp, - index, - pgEnum, - decimal, - unique, -} from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { cardsSchema } from './schema.js'; -import { cards } from './cards.js'; - -// Progress status enum (SM-2 algorithm states) -export const progressStatusEnum = pgEnum('progress_status', [ - 'new', - 'learning', - 'review', - 'relearning', -]); - -export const cardProgress = cardsSchema.table( - 'card_progress', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - cardId: uuid('card_id') - .notNull() - .references(() => cards.id, { onDelete: 'cascade' }), - // SM-2 algorithm fields - easeFactor: decimal('ease_factor', { precision: 4, scale: 2 }).default('2.5').notNull(), - interval: integer('interval').default(0).notNull(), // Days until next review - repetitions: integer('repetitions').default(0).notNull(), - lastReviewed: timestamp('last_reviewed', { withTimezone: true }), - nextReview: timestamp('next_review', { withTimezone: true }), - status: progressStatusEnum('status').default('new').notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_card_progress_user_id').on(table.userId), - index('idx_card_progress_card_id').on(table.cardId), - index('idx_card_progress_next_review').on(table.nextReview), - index('idx_card_progress_status').on(table.status), - unique('unique_user_card').on(table.userId, table.cardId), - ] -); - -export const cardProgressRelations = relations(cardProgress, ({ one }) => ({ - card: one(cards, { - fields: [cardProgress.cardId], - references: [cards.id], - }), -})); - -export type CardProgress = typeof cardProgress.$inferSelect; -export type NewCardProgress = typeof cardProgress.$inferInsert; diff --git a/packages/cards-database/src/schema/cards.ts b/packages/cards-database/src/schema/cards.ts deleted file mode 100644 index 610be1397..000000000 --- a/packages/cards-database/src/schema/cards.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - uuid, - varchar, - text, - integer, - boolean, - timestamp, - jsonb, - index, - pgEnum, -} from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { cardsSchema } from './schema.js'; -import { decks } from './decks.js'; -import { cardProgress } from './cardProgress.js'; - -// Card type enum -export const cardTypeEnum = pgEnum('card_type', ['text', 'flashcard', 'quiz', 'mixed']); - -// Card content types -export interface TextContent { - text: string; - formatting?: { - bold?: boolean; - italic?: boolean; - underline?: boolean; - }; -} - -export interface FlashcardContent { - front: string; - back: string; - hint?: string; -} - -export interface QuizContent { - question: string; - options: string[]; - correctAnswer: number; - explanation?: string; -} - -export interface MixedContent { - sections: Array; -} - -export type CardContent = TextContent | FlashcardContent | QuizContent | MixedContent; - -export const cards = cardsSchema.table( - '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().$type(), - cardType: cardTypeEnum('card_type').notNull(), - aiModel: varchar('ai_model', { length: 100 }), - aiPrompt: text('ai_prompt'), - version: integer('version').default(1).notNull(), - isFavorite: boolean('is_favorite').default(false).notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_cards_deck_id').on(table.deckId), - index('idx_cards_position').on(table.deckId, table.position), - ] -); - -export const cardsRelations = relations(cards, ({ one, many }) => ({ - deck: one(decks, { - fields: [cards.deckId], - references: [decks.id], - }), - progress: many(cardProgress), -})); - -export type Card = typeof cards.$inferSelect; -export type NewCard = typeof cards.$inferInsert; diff --git a/packages/cards-database/src/schema/dailyProgress.ts b/packages/cards-database/src/schema/dailyProgress.ts deleted file mode 100644 index b03b3d788..000000000 --- a/packages/cards-database/src/schema/dailyProgress.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { uuid, text, date, integer, decimal, timestamp, index, unique } from 'drizzle-orm/pg-core'; -import { cardsSchema } from './schema.js'; - -export const dailyProgress = cardsSchema.table( - 'daily_progress', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - date: date('date').notNull(), - cardsStudied: integer('cards_studied').default(0).notNull(), - timeSpentMinutes: integer('time_spent_minutes').default(0).notNull(), - accuracyPercentage: decimal('accuracy_percentage', { precision: 5, scale: 2 }) - .default('0') - .notNull(), - decksStudied: text('decks_studied').array().default([]), - sessionsCompleted: integer('sessions_completed').default(0).notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_daily_progress_user_id').on(table.userId), - index('idx_daily_progress_date').on(table.date), - unique('unique_user_date').on(table.userId, table.date), - ] -); - -export type DailyProgress = typeof dailyProgress.$inferSelect; -export type NewDailyProgress = typeof dailyProgress.$inferInsert; diff --git a/packages/cards-database/src/schema/deckTemplates.ts b/packages/cards-database/src/schema/deckTemplates.ts deleted file mode 100644 index 9d71187a3..000000000 --- a/packages/cards-database/src/schema/deckTemplates.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - uuid, - varchar, - text, - boolean, - integer, - timestamp, - jsonb, - index, -} from 'drizzle-orm/pg-core'; -import { cardsSchema } from './schema.js'; - -// Template data structure -export interface DeckTemplateData { - cards: Array<{ - title?: string; - content: Record; - cardType: 'text' | 'flashcard' | 'quiz' | 'mixed'; - }>; - settings?: Record; - tags?: string[]; -} - -export const deckTemplates = cardsSchema.table( - 'deck_templates', - { - id: uuid('id').primaryKey().defaultRandom(), - title: varchar('title', { length: 255 }).notNull(), - description: text('description'), - category: varchar('category', { length: 100 }), - templateData: jsonb('template_data').notNull().$type(), - isActive: boolean('is_active').default(true).notNull(), - isPublic: boolean('is_public').default(true).notNull(), - popularity: integer('popularity').default(0).notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_deck_templates_category').on(table.category), - index('idx_deck_templates_is_active').on(table.isActive), - index('idx_deck_templates_popularity').on(table.popularity), - ] -); - -export type DeckTemplate = typeof deckTemplates.$inferSelect; -export type NewDeckTemplate = typeof deckTemplates.$inferInsert; diff --git a/packages/cards-database/src/schema/decks.ts b/packages/cards-database/src/schema/decks.ts deleted file mode 100644 index 87f2e25ac..000000000 --- a/packages/cards-database/src/schema/decks.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { uuid, text, varchar, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { cardsSchema } from './schema.js'; -import { cards } from './cards.js'; -import { studySessions } from './studySessions.js'; -import { aiGenerations } from './aiGenerations.js'; - -export const decks = cardsSchema.table( - 'decks', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - title: varchar('title', { length: 255 }).notNull(), - description: text('description'), - coverImageUrl: text('cover_image_url'), - isPublic: boolean('is_public').default(false).notNull(), - isFeatured: boolean('is_featured').default(false).notNull(), - featuredAt: timestamp('featured_at', { withTimezone: true }), - settings: jsonb('settings').default({}).$type>(), - tags: text('tags').array().default([]), - metadata: jsonb('metadata').default({}).$type>(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_decks_user_id').on(table.userId), - index('idx_decks_is_public').on(table.isPublic), - index('idx_decks_is_featured').on(table.isFeatured), - index('idx_decks_updated_at').on(table.updatedAt), - ] -); - -export const decksRelations = relations(decks, ({ many }) => ({ - cards: many(cards), - studySessions: many(studySessions), - aiGenerations: many(aiGenerations), -})); - -export type Deck = typeof decks.$inferSelect; -export type NewDeck = typeof decks.$inferInsert; diff --git a/packages/cards-database/src/schema/index.ts b/packages/cards-database/src/schema/index.ts deleted file mode 100644 index 7a246c7e2..000000000 --- a/packages/cards-database/src/schema/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Export schema definition -export * from './schema.js'; - -// Export all schemas -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.js'; -export { cardsRelations } from './cards.js'; -export { studySessionsRelations } from './studySessions.js'; -export { cardProgressRelations } from './cardProgress.js'; -export { aiGenerationsRelations } from './aiGenerations.js'; diff --git a/packages/cards-database/src/schema/schema.ts b/packages/cards-database/src/schema/schema.ts deleted file mode 100644 index 380fef39f..000000000 --- a/packages/cards-database/src/schema/schema.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pgSchema } from 'drizzle-orm/pg-core'; - -export const cardsSchema = pgSchema('cards'); diff --git a/packages/cards-database/src/schema/studySessions.ts b/packages/cards-database/src/schema/studySessions.ts deleted file mode 100644 index 430cf778a..000000000 --- a/packages/cards-database/src/schema/studySessions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { uuid, text, integer, timestamp, index, pgEnum } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { cardsSchema } from './schema.js'; -import { decks } from './decks.js'; - -// Study mode enum -export const studyModeEnum = pgEnum('study_mode', ['all', 'new', 'review', 'favorites', 'random']); - -export const studySessions = cardsSchema.table( - 'study_sessions', - { - id: uuid('id').primaryKey().defaultRandom(), - deckId: uuid('deck_id') - .notNull() - .references(() => decks.id, { onDelete: 'cascade' }), - userId: text('user_id').notNull(), - mode: studyModeEnum('mode').notNull(), - totalCards: integer('total_cards').notNull().default(0), - completedCards: integer('completed_cards').notNull().default(0), - correctCards: integer('correct_cards').notNull().default(0), - startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(), - completedAt: timestamp('completed_at', { withTimezone: true }), - timeSpentSeconds: integer('time_spent_seconds').default(0).notNull(), - }, - (table) => [ - index('idx_study_sessions_user_id').on(table.userId), - index('idx_study_sessions_deck_id').on(table.deckId), - index('idx_study_sessions_started_at').on(table.startedAt), - ] -); - -export const studySessionsRelations = relations(studySessions, ({ one }) => ({ - deck: one(decks, { - fields: [studySessions.deckId], - references: [decks.id], - }), -})); - -export type StudySession = typeof studySessions.$inferSelect; -export type NewStudySession = typeof studySessions.$inferInsert; diff --git a/packages/cards-database/src/schema/userStats.ts b/packages/cards-database/src/schema/userStats.ts deleted file mode 100644 index 9c13568d6..000000000 --- a/packages/cards-database/src/schema/userStats.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { text, integer, decimal, date, timestamp, index } from 'drizzle-orm/pg-core'; -import { cardsSchema } from './schema.js'; - -export const userStats = cardsSchema.table( - 'user_stats', - { - userId: text('user_id').primaryKey(), - totalWins: integer('total_wins').default(0).notNull(), - totalSessions: integer('total_sessions').default(0).notNull(), - totalCardsStudied: integer('total_cards_studied').default(0).notNull(), - totalTimeSeconds: integer('total_time_seconds').default(0).notNull(), - averageAccuracy: decimal('average_accuracy', { precision: 5, scale: 2 }).default('0').notNull(), - streakDays: integer('streak_days').default(0).notNull(), - longestStreak: integer('longest_streak').default(0).notNull(), - lastStudyDate: date('last_study_date'), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('idx_user_stats_total_wins').on(table.totalWins), - index('idx_user_stats_streak_days').on(table.streakDays), - ] -); - -export type UserStats = typeof userStats.$inferSelect; -export type NewUserStats = typeof userStats.$inferInsert; diff --git a/packages/cards-database/src/seed.ts b/packages/cards-database/src/seed.ts deleted file mode 100644 index e413c219a..000000000 --- a/packages/cards-database/src/seed.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getDb, closeDb } from './client.js'; -import { deckTemplates } from './schema/index.js'; - -/** - * Seed the database with initial data - */ -async function seed() { - console.log('Seeding database...'); - - const db = getDb(); - - try { - // Seed deck templates - const templates = [ - { - title: 'Language Basics', - description: 'Learn basic vocabulary and phrases for a new language', - category: 'languages', - templateData: { - cards: [ - { - cardType: 'flashcard' as const, - content: { front: 'Hello', back: 'Hallo', hint: 'Greeting' }, - }, - { - cardType: 'flashcard' as const, - content: { front: 'Goodbye', back: 'Auf Wiedersehen' }, - }, - ], - settings: { language: 'de' }, - tags: ['language', 'basics', 'german'], - }, - isActive: true, - isPublic: true, - popularity: 100, - }, - { - title: 'Math Fundamentals', - description: 'Essential math concepts and formulas', - category: 'education', - templateData: { - cards: [ - { - cardType: 'quiz' as const, - content: { - question: 'What is 2 + 2?', - options: ['3', '4', '5', '6'], - correctAnswer: 1, - explanation: '2 + 2 equals 4', - }, - }, - ], - settings: { difficulty: 'beginner' }, - tags: ['math', 'basics'], - }, - isActive: true, - isPublic: true, - popularity: 80, - }, - { - title: 'Programming Concepts', - description: 'Core programming concepts and terminology', - category: 'technology', - templateData: { - cards: [ - { - cardType: 'flashcard' as const, - content: { - front: 'Variable', - back: 'A named storage location in memory that holds a value', - }, - }, - { - cardType: 'flashcard' as const, - content: { - front: 'Function', - back: 'A reusable block of code that performs a specific task', - }, - }, - ], - settings: {}, - tags: ['programming', 'coding', 'basics'], - }, - isActive: true, - isPublic: true, - popularity: 90, - }, - ]; - - console.log('Inserting deck templates...'); - await db.insert(deckTemplates).values(templates).onConflictDoNothing(); - - console.log('Seeding completed successfully!'); - } catch (error) { - console.error('Seeding failed:', error); - throw error; - } finally { - await closeDb(); - } -} - -seed().catch((error) => { - console.error('Seed script failed:', error); - process.exit(1); -}); diff --git a/packages/cards-database/src/test-connection.ts b/packages/cards-database/src/test-connection.ts deleted file mode 100644 index e36153482..000000000 --- a/packages/cards-database/src/test-connection.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Test database connection - * Usage: pnpm db:test - */ - -import { getDb, closeDb, sql } from './client.js'; - -async function testConnection() { - console.log('Testing database connection...\n'); - - try { - const db = getDb(); - - // Test basic connection - const result = await db.execute(sql`SELECT NOW() as current_time, version() as pg_version`); - console.log('✅ Connection successful!'); - console.log(` Time: ${result[0].current_time}`); - console.log(` PostgreSQL: ${result[0].pg_version}\n`); - - // List tables - const tables = await db.execute(sql` - SELECT tablename - FROM pg_tables - WHERE schemaname = 'public' - ORDER BY tablename - `); - - if (tables.length > 0) { - console.log('📋 Tables in database:'); - tables.forEach((t: any) => console.log(` - ${t.tablename}`)); - } else { - console.log('📋 No tables found. Run "pnpm db:push" to create schema.'); - } - - console.log('\n✅ All tests passed!'); - } catch (error) { - console.error('❌ Connection failed:', error); - process.exit(1); - } finally { - await closeDb(); - } -} - -testConnection(); diff --git a/packages/cards-database/tsconfig.json b/packages/cards-database/tsconfig.json deleted file mode 100644 index c77df5b85..000000000 --- a/packages/cards-database/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src", - "types": ["node"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-api-client/package.json b/packages/shared-api-client/package.json deleted file mode 100644 index e5904e7f9..000000000 --- a/packages/shared-api-client/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@mana/shared-api-client", - "version": "1.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - } - }, - "scripts": { - "build": "tsc", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@mana/shared-utils": "workspace:*" - }, - "devDependencies": { - "typescript": "^5.9.3" - } -} diff --git a/packages/shared-api-client/src/client.ts b/packages/shared-api-client/src/client.ts deleted file mode 100644 index 74e825f2f..000000000 --- a/packages/shared-api-client/src/client.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * API Client Factory - * Creates a configured API client with consistent error handling - */ - -import type { ApiClient, ApiClientConfig, ApiResult, RequestOptions } from './types'; -import { - buildQueryString, - createApiError, - getBaseUrl, - getErrorCodeFromStatus, - isRetryableError, - parseErrorResponse, -} from './utils'; -import { sleep } from '@mana/shared-utils'; - -const DEFAULT_TIMEOUT = 30000; -const DEFAULT_RETRIES = 0; -const DEFAULT_RETRY_DELAY = 1000; - -/** - * Create a configured API client instance - * - * @example - * ```typescript - * import { createApiClient } from '@mana/shared-api-client'; - * import { authStore } from '$lib/stores/auth.svelte'; - * - * export const api = createApiClient({ - * baseUrl: 'http://localhost:3014', - * apiPrefix: '/api/v1', - * getAuthToken: () => authStore.getValidToken(), - * }); - * - * // Usage - * const { data, error } = await api.get('/users'); - * if (error) { - * console.error('Failed:', error.message); - * return; - * } - * // data is typed as User[] - * ``` - */ -export function createApiClient(config: ApiClientConfig): ApiClient { - const { - apiPrefix = '', - getAuthToken, - timeout = DEFAULT_TIMEOUT, - retries = DEFAULT_RETRIES, - retryDelay = DEFAULT_RETRY_DELAY, - onError, - debug = false, - } = config; - - /** - * Internal fetch with error handling, timeout, and retries - */ - async function fetchWithRetry( - endpoint: string, - init: RequestInit, - options: RequestOptions = {}, - attemptNum = 0 - ): Promise> { - const baseUrl = config.useRuntimeUrl !== false ? getBaseUrl(config.baseUrl) : config.baseUrl; - const queryString = options.params ? buildQueryString(options.params) : ''; - const url = baseUrl + apiPrefix + endpoint + queryString; - const requestTimeout = options.timeout ?? timeout; - const maxRetries = options.retries ?? retries; - - // Create abort controller for timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), requestTimeout); - - try { - // Get auth token if not skipping - const headers: Record = { - ...((init.headers as Record) || {}), - ...(options.headers || {}), - }; - - if (!options.skipAuth && getAuthToken) { - const token = await getAuthToken(); - if (token) { - headers['Authorization'] = 'Bearer ' + token; - } - } - - if (debug) { - console.log('[API] ' + init.method + ' ' + url); - } - - const response = await fetch(url, { - ...init, - headers, - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - // Handle 204 No Content - if (response.status === 204) { - return { data: null as T, error: null }; - } - - // Handle error responses - if (!response.ok) { - const errorMessage = await parseErrorResponse(response); - const error = createApiError( - errorMessage, - getErrorCodeFromStatus(response.status), - response.status - ); - - // Retry on server errors - if (isRetryableError(error) && attemptNum < maxRetries) { - if (debug) { - console.log('[API] Retry ' + (attemptNum + 1) + '/' + maxRetries + ' for ' + url); - } - await sleep(retryDelay * (attemptNum + 1)); // Exponential backoff - return fetchWithRetry(endpoint, init, options, attemptNum + 1); - } - - if (onError) { - onError(error, endpoint); - } - - return { data: null, error }; - } - - // Parse JSON response - const contentType = response.headers.get('content-type'); - if (contentType?.includes('application/json')) { - const data = await response.json(); - return { data, error: null }; - } - - // Handle non-JSON responses (e.g., text, blob) - const text = await response.text(); - return { data: text as T, error: null }; - } catch (err) { - clearTimeout(timeoutId); - - // Handle abort (timeout) - if (err instanceof DOMException && err.name === 'AbortError') { - const error = createApiError('Request timed out after ' + requestTimeout + 'ms', 'TIMEOUT'); - - if (attemptNum < maxRetries) { - if (debug) { - console.log( - '[API] Retry ' + (attemptNum + 1) + '/' + maxRetries + ' after timeout for ' + url - ); - } - await sleep(retryDelay * (attemptNum + 1)); - return fetchWithRetry(endpoint, init, options, attemptNum + 1); - } - - if (onError) { - onError(error, endpoint); - } - return { data: null, error }; - } - - // Handle network errors - const error = createApiError( - err instanceof Error ? err.message : 'Network error', - 'NETWORK_ERROR' - ); - - if (attemptNum < maxRetries) { - if (debug) { - console.log( - '[API] Retry ' + (attemptNum + 1) + '/' + maxRetries + ' after network error for ' + url - ); - } - await sleep(retryDelay * (attemptNum + 1)); - return fetchWithRetry(endpoint, init, options, attemptNum + 1); - } - - if (onError) { - onError(error, endpoint); - } - return { data: null, error }; - } - } - - /** - * Prepare request body and headers - */ - function prepareBody(body: unknown): { body?: string; contentType?: string } { - if (body === undefined || body === null) { - return {}; - } - - if (body instanceof FormData) { - // Don't set Content-Type for FormData - browser handles it - return {}; - } - - return { - body: JSON.stringify(body), - contentType: 'application/json', - }; - } - - return { - async get(endpoint: string, options?: RequestOptions): Promise> { - return fetchWithRetry( - endpoint, - { - method: 'GET', - headers: { Accept: 'application/json' }, - }, - options - ); - }, - - async post( - endpoint: string, - body?: unknown, - options?: RequestOptions - ): Promise> { - const { body: jsonBody, contentType } = prepareBody(body); - return fetchWithRetry( - endpoint, - { - method: 'POST', - headers: { - Accept: 'application/json', - ...(contentType ? { 'Content-Type': contentType } : {}), - }, - body: jsonBody, - }, - options - ); - }, - - async put( - endpoint: string, - body?: unknown, - options?: RequestOptions - ): Promise> { - const { body: jsonBody, contentType } = prepareBody(body); - return fetchWithRetry( - endpoint, - { - method: 'PUT', - headers: { - Accept: 'application/json', - ...(contentType ? { 'Content-Type': contentType } : {}), - }, - body: jsonBody, - }, - options - ); - }, - - async patch( - endpoint: string, - body?: unknown, - options?: RequestOptions - ): Promise> { - const { body: jsonBody, contentType } = prepareBody(body); - return fetchWithRetry( - endpoint, - { - method: 'PATCH', - headers: { - Accept: 'application/json', - ...(contentType ? { 'Content-Type': contentType } : {}), - }, - body: jsonBody, - }, - options - ); - }, - - async delete(endpoint: string, options?: RequestOptions): Promise> { - return fetchWithRetry( - endpoint, - { - method: 'DELETE', - headers: { Accept: 'application/json' }, - }, - options - ); - }, - - async upload( - endpoint: string, - formData: FormData, - options?: RequestOptions - ): Promise> { - return fetchWithRetry( - endpoint, - { - method: 'POST', - // Don't set Content-Type - browser handles multipart boundary - headers: { Accept: 'application/json' }, - body: formData, - }, - options - ); - }, - }; -} diff --git a/packages/shared-api-client/src/index.ts b/packages/shared-api-client/src/index.ts deleted file mode 100644 index 7df3d3bd1..000000000 --- a/packages/shared-api-client/src/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @mana/shared-api-client - * - * Unified API client for all Mana web applications. - * Provides consistent error handling, token management, and retry logic. - * - * @example - * ```typescript - * import { createApiClient } from '@mana/shared-api-client'; - * import { authStore } from '$lib/stores/auth.svelte'; - * - * // Create client instance - * export const api = createApiClient({ - * baseUrl: 'http://localhost:3014', - * apiPrefix: '/api/v1', - * getAuthToken: () => authStore.getValidToken(), - * timeout: 30000, - * retries: 2, - * }); - * - * // Make requests - * const { data, error } = await api.get('/users'); - * - * if (error) { - * if (error.code === 'UNAUTHORIZED') { - * // Handle auth error - * } - * console.error('API Error:', error.message); - * return; - * } - * - * // data is typed as User[] - * console.log('Users:', data); - * ``` - */ - -// Client factory -export { createApiClient } from './client'; - -// Types -export type { - ApiClient, - ApiClientConfig, - ApiError, - ApiErrorCode, - ApiResult, - RequestOptions, -} from './types'; - -// Utilities -export { buildQueryString, getBaseUrl } from './utils'; diff --git a/packages/shared-api-client/src/types.ts b/packages/shared-api-client/src/types.ts deleted file mode 100644 index 96ceb14eb..000000000 --- a/packages/shared-api-client/src/types.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * API Client Types - * Go-style Result pattern for consistent error handling - */ - -/** - * Result wrapper for API responses - * Provides explicit success/error handling without try/catch - */ -export interface ApiResult { - data: T | null; - error: ApiError | null; -} - -/** - * Structured API error with type information - */ -export interface ApiError { - message: string; - code: ApiErrorCode; - status?: number; - details?: unknown; -} - -/** - * Error codes for different failure scenarios - */ -export type ApiErrorCode = - | 'NETWORK_ERROR' - | 'TIMEOUT' - | 'UNAUTHORIZED' - | 'FORBIDDEN' - | 'NOT_FOUND' - | 'VALIDATION_ERROR' - | 'SERVER_ERROR' - | 'UNKNOWN'; - -/** - * Configuration for creating an API client - */ -export interface ApiClientConfig { - /** Base URL for API requests (e.g., 'http://localhost:3014') */ - baseUrl: string; - - /** API prefix to prepend to all endpoints (e.g., '/api/v1') */ - apiPrefix?: string; - - /** Async function to get the current auth token (supports auto-refresh) */ - getAuthToken?: () => Promise; - - /** Request timeout in milliseconds (default: 30000) */ - timeout?: number; - - /** Number of retry attempts for failed requests (default: 0) */ - retries?: number; - - /** Delay between retries in milliseconds (default: 1000) */ - retryDelay?: number; - - /** Custom error handler for logging/reporting */ - onError?: (error: ApiError, endpoint: string) => void; - - /** Enable debug logging (default: false) */ - debug?: boolean; - - /** Use window.__PUBLIC_BACKEND_URL__ runtime override (default: true). - * Set to false for cross-app clients that resolve their own base URL. */ - useRuntimeUrl?: boolean; -} - -/** - * Options for individual requests - */ -export interface RequestOptions { - /** Custom headers to merge with defaults */ - headers?: Record; - - /** Override timeout for this request */ - timeout?: number; - - /** Skip authentication for this request */ - skipAuth?: boolean; - - /** Query parameters to append to URL */ - params?: Record; - - /** Override retry count for this request */ - retries?: number; -} - -/** - * API client interface with HTTP methods - */ -export interface ApiClient { - /** GET request */ - get(endpoint: string, options?: RequestOptions): Promise>; - - /** POST request */ - post(endpoint: string, body?: unknown, options?: RequestOptions): Promise>; - - /** PUT request */ - put(endpoint: string, body?: unknown, options?: RequestOptions): Promise>; - - /** PATCH request */ - patch(endpoint: string, body?: unknown, options?: RequestOptions): Promise>; - - /** DELETE request */ - delete(endpoint: string, options?: RequestOptions): Promise>; - - /** Upload file(s) with FormData */ - upload(endpoint: string, formData: FormData, options?: RequestOptions): Promise>; -} diff --git a/packages/shared-api-client/src/utils.ts b/packages/shared-api-client/src/utils.ts deleted file mode 100644 index e1118e790..000000000 --- a/packages/shared-api-client/src/utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * API Client Utilities - */ - -import type { ApiError, ApiErrorCode } from './types'; - -/** - * Build a query string from parameters object - * Handles undefined values and proper encoding - */ -export function buildQueryString( - params: Record -): string { - const searchParams = new URLSearchParams(); - - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null && value !== '') { - searchParams.append(key, String(value)); - } - } - - const queryString = searchParams.toString(); - return queryString ? `?${queryString}` : ''; -} - -/** - * Determine error code from HTTP status - */ -export function getErrorCodeFromStatus(status: number): ApiErrorCode { - if (status === 401) return 'UNAUTHORIZED'; - if (status === 403) return 'FORBIDDEN'; - if (status === 404) return 'NOT_FOUND'; - if (status === 422 || status === 400) return 'VALIDATION_ERROR'; - if (status >= 500) return 'SERVER_ERROR'; - return 'UNKNOWN'; -} - -/** - * Create a standardized API error - */ -export function createApiError( - message: string, - code: ApiErrorCode, - status?: number, - details?: unknown -): ApiError { - return { message, code, status, details }; -} - -/** - * Parse error response body - */ -export async function parseErrorResponse(response: Response): Promise { - try { - const data = await response.json(); - return data.message || data.error || JSON.stringify(data); - } catch { - return response.statusText || 'Unknown error'; - } -} - -/** - * Check if error is retryable (network issues, 5xx errors) - */ -export function isRetryableError(error: ApiError): boolean { - if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') { - return true; - } - if (error.status && error.status >= 500) { - return true; - } - return false; -} - -/** - * Get base URL with runtime injection support for Docker - * Checks window.__PUBLIC_BACKEND_URL__ first, then falls back to provided URL - */ -export function getBaseUrl(configuredUrl: string): string { - if (typeof window !== 'undefined') { - const runtimeUrl = (window as unknown as Record).__PUBLIC_BACKEND_URL__; - if (typeof runtimeUrl === 'string' && runtimeUrl) { - return runtimeUrl; - } - } - return configuredUrl; -} diff --git a/packages/shared-api-client/tsconfig.json b/packages/shared-api-client/tsconfig.json deleted file mode 100644 index 916f65a1b..000000000 --- a/packages/shared-api-client/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-errors/package.json b/packages/shared-errors/package.json deleted file mode 100644 index c574d3607..000000000 --- a/packages/shared-errors/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@mana/shared-errors", - "version": "0.1.0", - "private": true, - "description": "Go-like error handling system for Mana backends", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./nestjs": { - "types": "./dist/nestjs/index.d.ts", - "default": "./dist/nestjs/index.js" - } - }, - "typesVersions": { - "*": { - "nestjs": [ - "./dist/nestjs/index.d.ts" - ] - } - }, - "scripts": { - "build": "tsc -p tsconfig.build.json", - "type-check": "tsc --noEmit", - "clean": "rm -rf dist", - "lint": "eslint ." - }, - "peerDependencies": { - "@nestjs/common": ">=10.0.0" - }, - "peerDependenciesMeta": { - "@nestjs/common": { - "optional": true - } - }, - "devDependencies": { - "@nestjs/common": "^11.0.17", - "@types/express": "^5.0.0", - "@types/node": "^22.0.0", - "typescript": "^5.9.3" - } -} diff --git a/packages/shared-errors/src/errors/app-error.ts b/packages/shared-errors/src/errors/app-error.ts deleted file mode 100644 index 87a95db5c..000000000 --- a/packages/shared-errors/src/errors/app-error.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { - type ErrorCode, - ERROR_CODE_TO_HTTP_STATUS, - ERROR_CODE_RETRYABLE, -} from '../types/error-codes'; - -/** - * Additional context that can be attached to errors. - */ -export interface ErrorContext { - [key: string]: unknown; -} - -/** - * Options for creating an AppError. - */ -export interface AppErrorOptions { - code: ErrorCode; - message: string; - cause?: Error | AppError; - context?: ErrorContext; - httpStatus?: number; - retryable?: boolean; -} - -/** - * Base error class for all application errors. - * - * Follows Go-like error handling principles: - * - Errors are values, not exceptions - * - Support for error wrapping with context - * - Type-safe error checking - * - * @example - * ```typescript - * // Create a basic error - * const error = new AppError({ - * code: ErrorCode.VALIDATION_FAILED, - * message: 'Invalid email format', - * }); - * - * // Wrap an error with context (Go-like) - * const wrapped = error.wrap('validating user input'); - * // Message becomes: "validating user input: Invalid email format" - * - * // Check error codes (like Go's errors.Is) - * if (error.hasCode(ErrorCode.VALIDATION_FAILED)) { - * // Handle validation error - * } - * ``` - */ -export class AppError extends Error { - /** Standardized error code */ - readonly code: ErrorCode; - - /** HTTP status code for API responses */ - readonly httpStatus: number; - - /** Whether the operation can be retried */ - readonly retryable: boolean; - - /** Original error that caused this error (for wrapping) */ - readonly cause?: Error | AppError; - - /** Additional context information */ - readonly context: ErrorContext; - - /** Timestamp when error was created */ - readonly timestamp: string; - - constructor(options: AppErrorOptions) { - super(options.message); - this.name = 'AppError'; - this.code = options.code; - this.cause = options.cause; - this.context = options.context ?? {}; - this.timestamp = new Date().toISOString(); - - // Use provided values or defaults from mappings - this.httpStatus = options.httpStatus ?? ERROR_CODE_TO_HTTP_STATUS[options.code]; - this.retryable = options.retryable ?? ERROR_CODE_RETRYABLE[options.code]; - - // Capture stack trace - Error.captureStackTrace(this, this.constructor); - } - - /** - * Create a wrapped error with additional context. - * Similar to Go's `fmt.Errorf("context: %w", err)`. - * - * @param contextMessage - Description of the operation that failed - * @param additionalContext - Extra context data to include - * @returns A new AppError with the original as its cause - * - * @example - * ```typescript - * const wrapped = originalError.wrap('fetching user data'); - * // Message: "fetching user data: original message" - * ``` - */ - wrap(contextMessage: string, additionalContext?: ErrorContext): AppError { - return new AppError({ - code: this.code, - message: `${contextMessage}: ${this.message}`, - cause: this, - context: { ...this.context, ...additionalContext }, - httpStatus: this.httpStatus, - retryable: this.retryable, - }); - } - - /** - * Get the root cause of the error chain. - * Traverses the cause chain to find the original error. - */ - rootCause(): Error { - let current: Error = this; - while (current instanceof AppError && current.cause) { - current = current.cause; - } - return current; - } - - /** - * Check if this error or any in the chain has the given code. - * Similar to Go's `errors.Is()`. - * - * @param code - The error code to check for - * @returns true if this error or any cause has the given code - * - * @example - * ```typescript - * if (error.hasCode(ErrorCode.INSUFFICIENT_CREDITS)) { - * // Show upgrade prompt - * } - * ``` - */ - hasCode(code: ErrorCode): boolean { - let current: Error | undefined = this; - while (current) { - if (current instanceof AppError && current.code === code) { - return true; - } - current = current instanceof AppError ? current.cause : undefined; - } - return false; - } - - /** - * Convert to JSON for API responses. - * Excludes stack traces and internal details. - */ - toJSON(): Record { - return { - code: this.code, - message: this.message, - httpStatus: this.httpStatus, - retryable: this.retryable, - timestamp: this.timestamp, - ...(Object.keys(this.context).length > 0 && { details: this.context }), - }; - } - - /** - * Convert to full JSON including stack and cause (for logging). - * Use this for server-side logging, not client responses. - */ - toFullJSON(): Record { - return { - ...this.toJSON(), - stack: this.stack, - cause: this.cause instanceof AppError ? this.cause.toFullJSON() : this.cause?.message, - }; - } -} diff --git a/packages/shared-errors/src/errors/auth-error.ts b/packages/shared-errors/src/errors/auth-error.ts deleted file mode 100644 index 022ab2081..000000000 --- a/packages/shared-errors/src/errors/auth-error.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; -import type { ErrorContext } from './app-error'; - -type AuthErrorCode = - | ErrorCode.AUTHENTICATION_REQUIRED - | ErrorCode.INVALID_TOKEN - | ErrorCode.TOKEN_EXPIRED - | ErrorCode.PERMISSION_DENIED - | ErrorCode.RESOURCE_NOT_OWNED; - -/** - * Error for authentication and authorization failures. - * HTTP Status: 401 (auth) or 403 (authorization) - * - * @example - * ```typescript - * // Authentication errors (401) - * return err(AuthError.unauthorized()); - * return err(AuthError.invalidToken('Token has been revoked')); - * return err(AuthError.tokenExpired()); - * - * // Authorization errors (403) - * return err(AuthError.forbidden('Admin access required')); - * return err(AuthError.notOwned('Story', storyId)); - * ``` - */ -export class AuthError extends AppError { - constructor(code: AuthErrorCode, message: string, context?: ErrorContext) { - super({ code, message, context }); - this.name = 'AuthError'; - } - - /** - * Create an error for missing authentication. - * HTTP 401 Unauthorized - */ - static unauthorized(message = 'Authentication required'): AuthError { - return new AuthError(ErrorCode.AUTHENTICATION_REQUIRED, message); - } - - /** - * Create an error for an invalid token. - * HTTP 401 Unauthorized - */ - static invalidToken(message = 'Invalid or malformed token'): AuthError { - return new AuthError(ErrorCode.INVALID_TOKEN, message); - } - - /** - * Create an error for an expired token. - * HTTP 401 Unauthorized - */ - static tokenExpired(message = 'Token has expired'): AuthError { - return new AuthError(ErrorCode.TOKEN_EXPIRED, message); - } - - /** - * Create an error for insufficient permissions. - * HTTP 403 Forbidden - */ - static forbidden(message = 'Permission denied'): AuthError { - return new AuthError(ErrorCode.PERMISSION_DENIED, message); - } - - /** - * Create an error when a user tries to access a resource they don't own. - * HTTP 403 Forbidden - * - * @param resourceType - Type of resource (e.g., 'Story', 'Character') - * @param resourceId - ID of the resource - */ - static notOwned(resourceType: string, resourceId: string): AuthError { - return new AuthError(ErrorCode.RESOURCE_NOT_OWNED, `${resourceType} does not belong to you`, { - resourceType, - resourceId, - }); - } -} diff --git a/packages/shared-errors/src/errors/credit-error.ts b/packages/shared-errors/src/errors/credit-error.ts deleted file mode 100644 index d559af5c3..000000000 --- a/packages/shared-errors/src/errors/credit-error.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; - -/** - * Error for insufficient credits/mana. - * HTTP Status: 402 Payment Required - * - * @example - * ```typescript - * return err(new CreditError(100, 50, 'story_generation')); - * // Message: "Insufficient credits. Required: 100, Available: 50" - * ``` - */ -export class CreditError extends AppError { - /** Credits required for the operation */ - readonly requiredCredits: number; - - /** Credits currently available */ - readonly availableCredits: number; - - constructor(requiredCredits: number, availableCredits: number, operation?: string) { - super({ - code: ErrorCode.INSUFFICIENT_CREDITS, - message: `Insufficient credits. Required: ${requiredCredits}, Available: ${availableCredits}`, - context: { requiredCredits, availableCredits, operation }, - }); - this.name = 'CreditError'; - this.requiredCredits = requiredCredits; - this.availableCredits = availableCredits; - } -} diff --git a/packages/shared-errors/src/errors/database-error.ts b/packages/shared-errors/src/errors/database-error.ts deleted file mode 100644 index bd379f930..000000000 --- a/packages/shared-errors/src/errors/database-error.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; -import type { ErrorContext } from './app-error'; - -type DatabaseErrorCode = ErrorCode.DATABASE_ERROR | ErrorCode.CONSTRAINT_VIOLATION; - -/** - * Error for database-level failures. - * HTTP Status: 500 (database), 409 (constraint violation) - * - * @example - * ```typescript - * // Constraint violation (e.g., unique constraint) - * return err(DatabaseError.constraintViolation('email', 'Email already exists')); - * - * // Generic database error - * return err(DatabaseError.queryFailed('Failed to fetch user data', originalError)); - * ``` - */ -export class DatabaseError extends AppError { - constructor(code: DatabaseErrorCode, message: string, cause?: Error, context?: ErrorContext) { - super({ code, message, cause, context }); - this.name = 'DatabaseError'; - } - - /** - * Create a constraint violation error (e.g., unique constraint). - * - * @param field - The field that violated the constraint - * @param message - Description of the violation - */ - static constraintViolation(field: string, message: string): DatabaseError { - return new DatabaseError(ErrorCode.CONSTRAINT_VIOLATION, message, undefined, { field }); - } - - /** - * Create a generic database query error. - * - * @param message - Description of what went wrong - * @param cause - Original error if available - */ - static queryFailed(message: string, cause?: Error): DatabaseError { - return new DatabaseError(ErrorCode.DATABASE_ERROR, message, cause); - } -} diff --git a/packages/shared-errors/src/errors/index.ts b/packages/shared-errors/src/errors/index.ts deleted file mode 100644 index 9f7b10956..000000000 --- a/packages/shared-errors/src/errors/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { AppError, type ErrorContext, type AppErrorOptions } from './app-error'; -export { ValidationError } from './validation-error'; -export { AuthError } from './auth-error'; -export { NotFoundError } from './not-found-error'; -export { CreditError } from './credit-error'; -export { ServiceError } from './service-error'; -export { RateLimitError } from './rate-limit-error'; -export { NetworkError } from './network-error'; -export { DatabaseError } from './database-error'; diff --git a/packages/shared-errors/src/errors/network-error.ts b/packages/shared-errors/src/errors/network-error.ts deleted file mode 100644 index dc31cb2b5..000000000 --- a/packages/shared-errors/src/errors/network-error.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; -import type { ErrorContext } from './app-error'; - -type NetworkErrorCode = ErrorCode.NETWORK_ERROR | ErrorCode.TIMEOUT | ErrorCode.CONNECTION_REFUSED; - -/** - * Error for network-level failures (timeouts, connection issues, etc.). - * HTTP Status: 502 (gateway), 503 (connection refused), 504 (timeout) - * - * @example - * ```typescript - * // Timeout - * return err(NetworkError.timeout('Fetching user profile')); - * - * // Connection refused - * return err(NetworkError.connectionRefused('Database')); - * - * // Generic network error - * return err(new NetworkError(ErrorCode.NETWORK_ERROR, 'DNS resolution failed')); - * ``` - */ -export class NetworkError extends AppError { - constructor(code: NetworkErrorCode, message: string, cause?: Error, context?: ErrorContext) { - super({ code, message, cause, context }); - this.name = 'NetworkError'; - } - - /** - * Create a timeout error. - * - * @param operation - Description of the operation that timed out - */ - static timeout(operation: string): NetworkError { - return new NetworkError(ErrorCode.TIMEOUT, `Operation timed out: ${operation}`, undefined, { - operation, - }); - } - - /** - * Create a connection refused error. - * - * @param service - Name of the service that refused connection - */ - static connectionRefused(service: string): NetworkError { - return new NetworkError( - ErrorCode.CONNECTION_REFUSED, - `Connection refused: ${service}`, - undefined, - { service } - ); - } -} diff --git a/packages/shared-errors/src/errors/not-found-error.ts b/packages/shared-errors/src/errors/not-found-error.ts deleted file mode 100644 index ef0835bd9..000000000 --- a/packages/shared-errors/src/errors/not-found-error.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; -import type { ErrorContext } from './app-error'; - -/** - * Error for when a requested resource is not found. - * HTTP Status: 404 Not Found - * - * @example - * ```typescript - * // Generic resource not found - * return err(new NotFoundError('User', userId)); - * - * // Using factory methods - * return err(NotFoundError.user(userId)); - * return err(NotFoundError.resource('Story', storyId)); - * ``` - */ -export class NotFoundError extends AppError { - constructor(resourceType: string, identifier: string, context?: ErrorContext) { - super({ - code: ErrorCode.RESOURCE_NOT_FOUND, - message: `${resourceType} not found: ${identifier}`, - context: { resourceType, identifier, ...context }, - }); - this.name = 'NotFoundError'; - } - - /** - * Create a not found error for a user. - */ - static user(userId: string): NotFoundError { - return new NotFoundError('User', userId); - } - - /** - * Create a not found error for any resource type. - */ - static resource(resourceType: string, identifier: string): NotFoundError { - return new NotFoundError(resourceType, identifier); - } -} diff --git a/packages/shared-errors/src/errors/rate-limit-error.ts b/packages/shared-errors/src/errors/rate-limit-error.ts deleted file mode 100644 index cc53644fd..000000000 --- a/packages/shared-errors/src/errors/rate-limit-error.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; - -/** - * Error for rate limiting. - * HTTP Status: 429 Too Many Requests - * - * @example - * ```typescript - * // Basic rate limit error - * return err(new RateLimitError()); - * - * // With retry-after information - * return err(new RateLimitError('Too many requests', 60)); - * // Client should wait 60 seconds before retrying - * ``` - */ -export class RateLimitError extends AppError { - /** Seconds to wait before retrying (if known) */ - readonly retryAfter?: number; - - constructor(message = 'Rate limit exceeded', retryAfter?: number) { - super({ - code: ErrorCode.RATE_LIMIT_EXCEEDED, - message, - context: retryAfter ? { retryAfter } : {}, - }); - this.name = 'RateLimitError'; - this.retryAfter = retryAfter; - } -} diff --git a/packages/shared-errors/src/errors/service-error.ts b/packages/shared-errors/src/errors/service-error.ts deleted file mode 100644 index 094033405..000000000 --- a/packages/shared-errors/src/errors/service-error.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; -import type { ErrorContext } from './app-error'; - -type ServiceErrorCode = - | ErrorCode.INTERNAL_ERROR - | ErrorCode.SERVICE_UNAVAILABLE - | ErrorCode.GENERATION_FAILED - | ErrorCode.EXTERNAL_SERVICE_ERROR; - -/** - * Error for service-level failures (internal errors, external API failures, etc.). - * HTTP Status: 500 (internal), 502 (external), 503 (unavailable) - * - * @example - * ```typescript - * // AI generation failed - * return err(ServiceError.generationFailed('OpenAI', 'Rate limit exceeded', originalError)); - * - * // External service unavailable - * return err(ServiceError.unavailable('Payment Service')); - * - * // External API error - * return err(ServiceError.externalError('Stripe', 'Card declined')); - * - * // Internal error - * return err(ServiceError.internal('Failed to process request')); - * ``` - */ -export class ServiceError extends AppError { - constructor(code: ServiceErrorCode, message: string, cause?: Error, context?: ErrorContext) { - super({ code, message, cause, context }); - this.name = 'ServiceError'; - } - - /** - * Create an error for AI/content generation failures. - * - * @param service - Name of the service (e.g., 'OpenAI', 'Azure OpenAI') - * @param reason - Why the generation failed - * @param cause - Original error if available - */ - static generationFailed(service: string, reason: string, cause?: Error): ServiceError { - return new ServiceError( - ErrorCode.GENERATION_FAILED, - `${service} generation failed: ${reason}`, - cause, - { service } - ); - } - - /** - * Create an error for a service that is temporarily unavailable. - * - * @param service - Name of the unavailable service - */ - static unavailable(service: string): ServiceError { - return new ServiceError( - ErrorCode.SERVICE_UNAVAILABLE, - `${service} is temporarily unavailable`, - undefined, - { service } - ); - } - - /** - * Create an error for external API failures. - * - * @param service - Name of the external service - * @param message - Error message or description - * @param cause - Original error if available - */ - static externalError(service: string, message: string, cause?: Error): ServiceError { - return new ServiceError( - ErrorCode.EXTERNAL_SERVICE_ERROR, - `${service} error: ${message}`, - cause, - { service } - ); - } - - /** - * Create an internal server error. - * - * @param message - Description of what went wrong - * @param cause - Original error if available - */ - static internal(message: string, cause?: Error): ServiceError { - return new ServiceError(ErrorCode.INTERNAL_ERROR, message, cause); - } -} diff --git a/packages/shared-errors/src/errors/validation-error.ts b/packages/shared-errors/src/errors/validation-error.ts deleted file mode 100644 index 14f26b1f3..000000000 --- a/packages/shared-errors/src/errors/validation-error.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ErrorCode } from '../types/error-codes'; -import { AppError } from './app-error'; -import type { ErrorContext } from './app-error'; - -/** - * Error for validation failures (invalid input, missing fields, etc.). - * HTTP Status: 400 Bad Request - * - * @example - * ```typescript - * // Using factory methods - * return err(ValidationError.invalidInput('email', 'must be a valid email address')); - * return err(ValidationError.missingField('password')); - * - * // Direct construction - * return err(new ValidationError('Age must be a positive number', { field: 'age' })); - * ``` - */ -export class ValidationError extends AppError { - constructor(message: string, context?: ErrorContext) { - super({ - code: ErrorCode.VALIDATION_FAILED, - message, - context, - }); - this.name = 'ValidationError'; - } - - /** - * Create a validation error for an invalid field value. - * - * @param field - The field name that failed validation - * @param reason - Why the validation failed - */ - static invalidInput(field: string, reason: string): ValidationError { - return new ValidationError(`Invalid ${field}: ${reason}`, { field, reason }); - } - - /** - * Create a validation error for a missing required field. - * - * @param field - The field name that is missing - */ - static missingField(field: string): ValidationError { - return new ValidationError(`Missing required field: ${field}`, { field }); - } - - /** - * Create a validation error for an invalid format. - * - * @param field - The field name with invalid format - * @param expectedFormat - Description of the expected format - */ - static invalidFormat(field: string, expectedFormat: string): ValidationError { - return new ValidationError(`Invalid format for ${field}: expected ${expectedFormat}`, { - field, - expectedFormat, - }); - } -} diff --git a/packages/shared-errors/src/guards/index.ts b/packages/shared-errors/src/guards/index.ts deleted file mode 100644 index 94fdeaee6..000000000 --- a/packages/shared-errors/src/guards/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './type-guards'; diff --git a/packages/shared-errors/src/guards/type-guards.ts b/packages/shared-errors/src/guards/type-guards.ts deleted file mode 100644 index 72a529fd1..000000000 --- a/packages/shared-errors/src/guards/type-guards.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { AppError } from '../errors/app-error'; -import { ValidationError } from '../errors/validation-error'; -import { AuthError } from '../errors/auth-error'; -import { NotFoundError } from '../errors/not-found-error'; -import { CreditError } from '../errors/credit-error'; -import { ServiceError } from '../errors/service-error'; -import { RateLimitError } from '../errors/rate-limit-error'; -import { NetworkError } from '../errors/network-error'; -import { DatabaseError } from '../errors/database-error'; -import { ErrorCode } from '../types/error-codes'; - -/** - * Check if error is an AppError. - * Similar to Go's `errors.As()`. - * - * @example - * ```typescript - * if (isAppError(error)) { - * console.log(error.code); // TypeScript knows error is AppError - * } - * ``` - */ -export function isAppError(error: unknown): error is AppError { - return error instanceof AppError; -} - -/** - * Check if error is a ValidationError. - */ -export function isValidationError(error: unknown): error is ValidationError { - return error instanceof ValidationError; -} - -/** - * Check if error is an AuthError. - */ -export function isAuthError(error: unknown): error is AuthError { - return error instanceof AuthError; -} - -/** - * Check if error is a NotFoundError. - */ -export function isNotFoundError(error: unknown): error is NotFoundError { - return error instanceof NotFoundError; -} - -/** - * Check if error is a CreditError. - */ -export function isCreditError(error: unknown): error is CreditError { - return error instanceof CreditError; -} - -/** - * Check if error is a ServiceError. - */ -export function isServiceError(error: unknown): error is ServiceError { - return error instanceof ServiceError; -} - -/** - * Check if error is a RateLimitError. - */ -export function isRateLimitError(error: unknown): error is RateLimitError { - return error instanceof RateLimitError; -} - -/** - * Check if error is a NetworkError. - */ -export function isNetworkError(error: unknown): error is NetworkError { - return error instanceof NetworkError; -} - -/** - * Check if error is a DatabaseError. - */ -export function isDatabaseError(error: unknown): error is DatabaseError { - return error instanceof DatabaseError; -} - -/** - * Check if error has a specific error code. - * Similar to Go's `errors.Is()`. - * - * @example - * ```typescript - * if (hasErrorCode(error, ErrorCode.INSUFFICIENT_CREDITS)) { - * showUpgradePrompt(); - * } - * ``` - */ -export function hasErrorCode(error: unknown, code: ErrorCode): boolean { - if (!isAppError(error)) { - return false; - } - return error.hasCode(code); -} - -/** - * Find the first error in the chain matching a predicate. - * Traverses the cause chain looking for a matching error. - * - * @example - * ```typescript - * const creditError = findError(error, isCreditError); - * if (creditError) { - * console.log('Required:', creditError.requiredCredits); - * } - * ``` - */ -export function findError( - error: unknown, - predicate: (e: AppError) => e is T -): T | undefined { - let current: unknown = error; - while (current) { - if (isAppError(current) && predicate(current)) { - return current; - } - current = isAppError(current) ? current.cause : undefined; - } - return undefined; -} - -/** - * Check if error is retryable. - * Works with both AppError and standard Error. - */ -export function isRetryable(error: unknown): boolean { - if (isAppError(error)) { - return error.retryable; - } - return false; -} - -/** - * Get the HTTP status code for an error. - * Returns 500 for non-AppError errors. - */ -export function getHttpStatus(error: unknown): number { - if (isAppError(error)) { - return error.httpStatus; - } - return 500; -} - -/** - * Get the error code for an error. - * Returns UNKNOWN_ERROR for non-AppError errors. - */ -export function getErrorCode(error: unknown): ErrorCode { - if (isAppError(error)) { - return error.code; - } - return ErrorCode.UNKNOWN_ERROR; -} diff --git a/packages/shared-errors/src/index.ts b/packages/shared-errors/src/index.ts deleted file mode 100644 index 2da9493eb..000000000 --- a/packages/shared-errors/src/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @mana/shared-errors - * - * Go-like error handling system for NestJS backends. - * - * Features: - * - Result type for explicit error handling - * - Standardized error codes and HTTP status mappings - * - Error wrapping with context (like Go's fmt.Errorf) - * - Type guards for type-safe error checking (like Go's errors.Is/As) - * - NestJS exception filter for consistent API responses - * - * @example - * ```typescript - * // In a service - * import { - * Result, ok, err, AsyncResult, - * ValidationError, NotFoundError, ServiceError - * } from '@mana/shared-errors'; - * - * async function getUser(id: string): AsyncResult { - * if (!isValidId(id)) { - * return err(ValidationError.invalidInput('id', 'must be a valid UUID')); - * } - * - * const user = await db.findUser(id); - * if (!user) { - * return err(new NotFoundError('User', id)); - * } - * - * return ok(user); - * } - * - * // In a controller - * import { isOk } from '@mana/shared-errors'; - * - * const result = await userService.getUser(id); - * if (!isOk(result)) { - * throw result.error; // Caught by AppExceptionFilter - * } - * return result.value; - * ``` - */ - -// Types -export { ErrorCode, ERROR_CODE_TO_HTTP_STATUS, ERROR_CODE_RETRYABLE } from './types/error-codes'; - -export { - type Result, - type AsyncResult, - ok, - err, - isOk, - isErr, - unwrap, - unwrapOr, - unwrapOrElse, - map, - mapErr, - andThen, - match, - tryCatch, - tryCatchAsync, - combine, - fromNullable, - toNullable, -} from './types/result'; - -// Errors -export { AppError, type ErrorContext, type AppErrorOptions } from './errors/app-error'; - -export { ValidationError } from './errors/validation-error'; -export { AuthError } from './errors/auth-error'; -export { NotFoundError } from './errors/not-found-error'; -export { CreditError } from './errors/credit-error'; -export { ServiceError } from './errors/service-error'; -export { RateLimitError } from './errors/rate-limit-error'; -export { NetworkError } from './errors/network-error'; -export { DatabaseError } from './errors/database-error'; - -// Guards -export { - isAppError, - isValidationError, - isAuthError, - isNotFoundError, - isCreditError, - isServiceError, - isRateLimitError, - isNetworkError, - isDatabaseError, - hasErrorCode, - findError, - isRetryable, - getHttpStatus, - getErrorCode, -} from './guards/type-guards'; - -// Utils -export { wrap, toAppError, cause, rootCause } from './utils/wrap'; diff --git a/packages/shared-errors/src/nestjs/app-exception.filter.ts b/packages/shared-errors/src/nestjs/app-exception.filter.ts deleted file mode 100644 index 3ad4c2048..000000000 --- a/packages/shared-errors/src/nestjs/app-exception.filter.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { Catch, HttpException, HttpStatus, Logger } from '@nestjs/common'; -import type { ExceptionFilter, ArgumentsHost } from '@nestjs/common'; -import type { Request, Response } from 'express'; -import { type AppError } from '../errors/app-error'; -import { isAppError, isCreditError, isRateLimitError } from '../guards/type-guards'; -import { ErrorCode } from '../types/error-codes'; - -/** - * Standard error response format returned by all backends. - */ -export interface ErrorResponseBody { - statusCode: number; - error: string; - message: string; - retryable: boolean; - timestamp: string; - path: string; - details?: Record; -} - -/** - * Global exception filter that converts all errors to a consistent format. - * - * Handles: - * - AppError and subclasses (from shared-errors) - * - NestJS HttpException - * - Standard JavaScript Error - * - Unknown errors - * - * @example - * ```typescript - * // In main.ts - * import { AppExceptionFilter } from '@mana/shared-errors/nestjs'; - * - * async function bootstrap() { - * const app = await NestFactory.create(AppModule); - * app.useGlobalFilters(new AppExceptionFilter()); - * await app.listen(3000); - * } - * ``` - */ -@Catch() -export class AppExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(AppExceptionFilter.name); - - catch(exception: unknown, host: ArgumentsHost): void { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - const errorResponse = this.buildErrorResponse(exception, request); - - this.logError(exception, request, errorResponse); - - response.status(errorResponse.statusCode).json(errorResponse); - } - - /** - * Build the error response body based on the exception type. - */ - private buildErrorResponse(exception: unknown, request: Request): ErrorResponseBody { - // Handle AppError and subclasses - if (isAppError(exception)) { - return this.buildAppErrorResponse(exception, request); - } - - // Handle NestJS HttpException - if (exception instanceof HttpException) { - return this.buildHttpExceptionResponse(exception, request); - } - - // Handle standard Error - if (exception instanceof Error) { - return this.buildStandardErrorResponse(exception, request); - } - - // Handle unknown errors - return this.buildUnknownErrorResponse(request); - } - - /** - * Build response for AppError and subclasses. - */ - private buildAppErrorResponse(exception: AppError, request: Request): ErrorResponseBody { - const baseResponse: ErrorResponseBody = { - statusCode: exception.httpStatus, - error: exception.code, - message: exception.message, - retryable: exception.retryable, - timestamp: exception.timestamp, - path: request.url, - }; - - // Add credit-specific fields for CreditError - if (isCreditError(exception)) { - baseResponse.details = { - requiredCredits: exception.requiredCredits, - availableCredits: exception.availableCredits, - ...exception.context, - }; - } - // Add retry-after for RateLimitError - else if (isRateLimitError(exception) && exception.retryAfter) { - baseResponse.details = { - retryAfter: exception.retryAfter, - ...exception.context, - }; - } - // Add other context if present - else if (Object.keys(exception.context).length > 0) { - baseResponse.details = exception.context; - } - - return baseResponse; - } - - /** - * Build response for NestJS HttpException. - */ - private buildHttpExceptionResponse( - exception: HttpException, - request: Request - ): ErrorResponseBody { - const status = exception.getStatus(); - const exceptionResponse = exception.getResponse(); - - let message: string; - let details: Record | undefined; - - if (typeof exceptionResponse === 'object') { - const responseObj = exceptionResponse as Record; - message = - typeof responseObj.message === 'string' - ? responseObj.message - : Array.isArray(responseObj.message) - ? (responseObj.message as string[]).join(', ') - : exception.message; - - // Extract any additional details - const { message: _, error: __, statusCode: ___, ...rest } = responseObj; - if (Object.keys(rest).length > 0) { - details = rest; - } - } else { - message = String(exceptionResponse); - } - - return { - statusCode: status, - error: this.httpStatusToErrorCode(status), - message, - retryable: status >= 500, - timestamp: new Date().toISOString(), - path: request.url, - ...(details && { details }), - }; - } - - /** - * Build response for standard JavaScript Error. - */ - private buildStandardErrorResponse(exception: Error, request: Request): ErrorResponseBody { - const isProduction = process.env.NODE_ENV === 'production'; - - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - error: ErrorCode.INTERNAL_ERROR, - message: isProduction ? 'An unexpected error occurred' : exception.message, - retryable: true, - timestamp: new Date().toISOString(), - path: request.url, - }; - } - - /** - * Build response for unknown error types. - */ - private buildUnknownErrorResponse(request: Request): ErrorResponseBody { - return { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, - error: ErrorCode.UNKNOWN_ERROR, - message: 'An unexpected error occurred', - retryable: true, - timestamp: new Date().toISOString(), - path: request.url, - }; - } - - /** - * Map HTTP status code to ErrorCode. - */ - private httpStatusToErrorCode(status: number): string { - const statusToCode: Record = { - 400: ErrorCode.VALIDATION_FAILED, - 401: ErrorCode.AUTHENTICATION_REQUIRED, - 402: ErrorCode.PAYMENT_REQUIRED, - 403: ErrorCode.PERMISSION_DENIED, - 404: ErrorCode.RESOURCE_NOT_FOUND, - 409: ErrorCode.CONFLICT, - 429: ErrorCode.RATE_LIMIT_EXCEEDED, - 500: ErrorCode.INTERNAL_ERROR, - 502: ErrorCode.EXTERNAL_SERVICE_ERROR, - 503: ErrorCode.SERVICE_UNAVAILABLE, - 504: ErrorCode.TIMEOUT, - }; - return statusToCode[status] || ErrorCode.UNKNOWN_ERROR; - } - - /** - * Log the error with appropriate level based on status code. - */ - private logError(exception: unknown, request: Request, response: ErrorResponseBody): void { - const logData = { - method: request.method, - url: request.url, - statusCode: response.statusCode, - error: response.error, - message: response.message, - userId: (request as Request & { user?: { sub?: string } }).user?.sub, - }; - - // Log 5xx errors as errors, others as warnings - if (response.statusCode >= 500) { - this.logger.error( - `[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`, - isAppError(exception) - ? JSON.stringify(exception.toFullJSON(), null, 2) - : exception instanceof Error - ? exception.stack - : undefined - ); - } else { - this.logger.warn( - `[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}` - ); - } - } -} diff --git a/packages/shared-errors/src/nestjs/index.ts b/packages/shared-errors/src/nestjs/index.ts deleted file mode 100644 index 3589aad38..000000000 --- a/packages/shared-errors/src/nestjs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AppExceptionFilter, type ErrorResponseBody } from './app-exception.filter'; diff --git a/packages/shared-errors/src/types/error-codes.ts b/packages/shared-errors/src/types/error-codes.ts deleted file mode 100644 index b26dfcc3e..000000000 --- a/packages/shared-errors/src/types/error-codes.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Standardized error codes across all backends. - * Follows pattern: CATEGORY_SPECIFIC_ERROR - */ -export enum ErrorCode { - // Validation Errors (400) - VALIDATION_FAILED = 'VALIDATION_FAILED', - INVALID_INPUT = 'INVALID_INPUT', - MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD', - INVALID_FORMAT = 'INVALID_FORMAT', - - // Authentication Errors (401) - AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED', - INVALID_TOKEN = 'INVALID_TOKEN', - TOKEN_EXPIRED = 'TOKEN_EXPIRED', - - // Authorization Errors (403) - PERMISSION_DENIED = 'PERMISSION_DENIED', - RESOURCE_NOT_OWNED = 'RESOURCE_NOT_OWNED', - - // Not Found Errors (404) - RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', - USER_NOT_FOUND = 'USER_NOT_FOUND', - - // Payment/Credit Errors (402) - INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS', - PAYMENT_REQUIRED = 'PAYMENT_REQUIRED', - - // Conflict Errors (409) - CONFLICT = 'CONFLICT', - DUPLICATE_ENTRY = 'DUPLICATE_ENTRY', - - // Rate Limiting (429) - RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', - TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS', - - // Service Errors (500) - INTERNAL_ERROR = 'INTERNAL_ERROR', - SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', - GENERATION_FAILED = 'GENERATION_FAILED', - EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR', - - // Network Errors (502/503/504) - NETWORK_ERROR = 'NETWORK_ERROR', - TIMEOUT = 'TIMEOUT', - CONNECTION_REFUSED = 'CONNECTION_REFUSED', - - // Database Errors - DATABASE_ERROR = 'DATABASE_ERROR', - CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION', - - // Unknown - UNKNOWN_ERROR = 'UNKNOWN_ERROR', -} - -/** - * Maps error codes to default HTTP status codes. - */ -export const ERROR_CODE_TO_HTTP_STATUS: Record = { - // Validation (400) - [ErrorCode.VALIDATION_FAILED]: 400, - [ErrorCode.INVALID_INPUT]: 400, - [ErrorCode.MISSING_REQUIRED_FIELD]: 400, - [ErrorCode.INVALID_FORMAT]: 400, - - // Authentication (401) - [ErrorCode.AUTHENTICATION_REQUIRED]: 401, - [ErrorCode.INVALID_TOKEN]: 401, - [ErrorCode.TOKEN_EXPIRED]: 401, - - // Authorization (403) - [ErrorCode.PERMISSION_DENIED]: 403, - [ErrorCode.RESOURCE_NOT_OWNED]: 403, - - // Not Found (404) - [ErrorCode.RESOURCE_NOT_FOUND]: 404, - [ErrorCode.USER_NOT_FOUND]: 404, - - // Payment (402) - [ErrorCode.INSUFFICIENT_CREDITS]: 402, - [ErrorCode.PAYMENT_REQUIRED]: 402, - - // Conflict (409) - [ErrorCode.CONFLICT]: 409, - [ErrorCode.DUPLICATE_ENTRY]: 409, - - // Rate Limit (429) - [ErrorCode.RATE_LIMIT_EXCEEDED]: 429, - [ErrorCode.TOO_MANY_REQUESTS]: 429, - - // Service Errors (500) - [ErrorCode.INTERNAL_ERROR]: 500, - [ErrorCode.SERVICE_UNAVAILABLE]: 503, - [ErrorCode.GENERATION_FAILED]: 500, - [ErrorCode.EXTERNAL_SERVICE_ERROR]: 502, - - // Network Errors - [ErrorCode.NETWORK_ERROR]: 502, - [ErrorCode.TIMEOUT]: 504, - [ErrorCode.CONNECTION_REFUSED]: 503, - - // Database Errors - [ErrorCode.DATABASE_ERROR]: 500, - [ErrorCode.CONSTRAINT_VIOLATION]: 409, - - // Unknown - [ErrorCode.UNKNOWN_ERROR]: 500, -}; - -/** - * Maps error codes to default retryable status. - */ -export const ERROR_CODE_RETRYABLE: Record = { - // Validation - not retryable (user needs to fix input) - [ErrorCode.VALIDATION_FAILED]: false, - [ErrorCode.INVALID_INPUT]: false, - [ErrorCode.MISSING_REQUIRED_FIELD]: false, - [ErrorCode.INVALID_FORMAT]: false, - - // Authentication - not retryable (need new credentials) - [ErrorCode.AUTHENTICATION_REQUIRED]: false, - [ErrorCode.INVALID_TOKEN]: false, - [ErrorCode.TOKEN_EXPIRED]: false, - - // Authorization - not retryable (permission issue) - [ErrorCode.PERMISSION_DENIED]: false, - [ErrorCode.RESOURCE_NOT_OWNED]: false, - - // Not Found - not retryable (resource doesn't exist) - [ErrorCode.RESOURCE_NOT_FOUND]: false, - [ErrorCode.USER_NOT_FOUND]: false, - - // Payment - not retryable (need more credits) - [ErrorCode.INSUFFICIENT_CREDITS]: false, - [ErrorCode.PAYMENT_REQUIRED]: false, - - // Conflict - not retryable (data issue) - [ErrorCode.CONFLICT]: false, - [ErrorCode.DUPLICATE_ENTRY]: false, - - // Rate Limit - retryable (after waiting) - [ErrorCode.RATE_LIMIT_EXCEEDED]: true, - [ErrorCode.TOO_MANY_REQUESTS]: true, - - // Service Errors - retryable (transient issues) - [ErrorCode.INTERNAL_ERROR]: true, - [ErrorCode.SERVICE_UNAVAILABLE]: true, - [ErrorCode.GENERATION_FAILED]: true, - [ErrorCode.EXTERNAL_SERVICE_ERROR]: true, - - // Network Errors - retryable (transient issues) - [ErrorCode.NETWORK_ERROR]: true, - [ErrorCode.TIMEOUT]: true, - [ErrorCode.CONNECTION_REFUSED]: true, - - // Database Errors - not retryable (except transient, but safer to say no) - [ErrorCode.DATABASE_ERROR]: false, - [ErrorCode.CONSTRAINT_VIOLATION]: false, - - // Unknown - retryable (might be transient) - [ErrorCode.UNKNOWN_ERROR]: true, -}; diff --git a/packages/shared-errors/src/types/index.ts b/packages/shared-errors/src/types/index.ts deleted file mode 100644 index be2932088..000000000 --- a/packages/shared-errors/src/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './error-codes'; -export * from './result'; diff --git a/packages/shared-errors/src/types/result.ts b/packages/shared-errors/src/types/result.ts deleted file mode 100644 index e85eea15a..000000000 --- a/packages/shared-errors/src/types/result.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { AppError } from '../errors/app-error'; -import { ErrorCode } from './error-codes'; - -/** - * Result type representing either success or failure. - * Inspired by Go's (value, error) return pattern and Rust's Result type. - * - * @example - * ```typescript - * // In a service - * async function getUser(id: string): AsyncResult { - * const user = await db.findUser(id); - * if (!user) { - * return err(new NotFoundError('User', id)); - * } - * return ok(user); - * } - * - * // In a controller (Go-like explicit unwrap) - * const result = await userService.getUser(id); - * if (!isOk(result)) { - * throw result.error; - * } - * return result.value; - * ``` - */ -export type Result = - | { readonly ok: true; readonly value: T; readonly error?: never } - | { readonly ok: false; readonly error: E; readonly value?: never }; - -/** - * Async version of Result - use this as return type for async functions. - */ -export type AsyncResult = Promise>; - -/** - * Create a success Result. - * - * @example - * ```typescript - * return ok({ name: 'John', email: 'john@example.com' }); - * ``` - */ -export function ok(value: T): Result { - return { ok: true, value }; -} - -/** - * Create a failure Result. - * - * @example - * ```typescript - * return err(new ValidationError('Invalid email')); - * return err(NotFoundError.user(userId)); - * ``` - */ -export function err(error: E): Result { - return { ok: false, error }; -} - -/** - * Check if Result is success. - * Use this for type narrowing in conditionals. - * - * @example - * ```typescript - * const result = await service.getData(); - * if (isOk(result)) { - * console.log(result.value); // TypeScript knows value exists - * } - * ``` - */ -export function isOk( - result: Result -): result is { ok: true; value: T } { - return result.ok === true; -} - -/** - * Check if Result is failure. - * Use this for type narrowing in conditionals. - * - * @example - * ```typescript - * const result = await service.getData(); - * if (isErr(result)) { - * console.error(result.error.message); // TypeScript knows error exists - * } - * ``` - */ -export function isErr( - result: Result -): result is { ok: false; error: E } { - return result.ok === false; -} - -/** - * Unwrap the value or throw if error. - * Use sparingly - prefer explicit error checking. - * - * @throws The error if Result is a failure - * - * @example - * ```typescript - * // Use when you want to propagate errors as exceptions - * const value = unwrap(result); - * ``` - */ -export function unwrap(result: Result): T { - if (isOk(result)) { - return result.value; - } - throw result.error; -} - -/** - * Unwrap the value or return a default value. - * - * @example - * ```typescript - * const users = unwrapOr(result, []); // Returns [] if error - * ``` - */ -export function unwrapOr(result: Result, defaultValue: T): T { - return isOk(result) ? result.value : defaultValue; -} - -/** - * Unwrap the value or compute a default from the error. - * - * @example - * ```typescript - * const value = unwrapOrElse(result, (error) => { - * console.error('Failed:', error.message); - * return fallbackValue; - * }); - * ``` - */ -export function unwrapOrElse(result: Result, fn: (error: E) => T): T { - return isOk(result) ? result.value : fn(result.error); -} - -/** - * Map the success value to a new value. - * - * @example - * ```typescript - * const result = await getUser(id); - * const nameResult = map(result, user => user.name); - * ``` - */ -export function map( - result: Result, - fn: (value: T) => U -): Result { - return isOk(result) ? ok(fn(result.value)) : result; -} - -/** - * Map the error to a new error. - * - * @example - * ```typescript - * const result = mapErr(originalResult, error => - * error.wrap('while processing user') - * ); - * ``` - */ -export function mapErr( - result: Result, - fn: (error: E) => F -): Result { - return isErr(result) ? err(fn(result.error)) : result; -} - -/** - * Chain Results (flatMap) - use when the mapping function returns a Result. - * - * @example - * ```typescript - * const result = andThen(getUserResult, user => - * getPermissions(user.id) - * ); - * ``` - */ -export function andThen( - result: Result, - fn: (value: T) => Result -): Result { - return isOk(result) ? fn(result.value) : result; -} - -/** - * Pattern matching for Result - handle both success and failure cases. - * - * @example - * ```typescript - * const message = match(result, { - * ok: (user) => `Welcome, ${user.name}!`, - * err: (error) => `Error: ${error.message}`, - * }); - * ``` - */ -export function match( - result: Result, - handlers: { - ok: (value: T) => U; - err: (error: E) => U; - } -): U { - return isOk(result) ? handlers.ok(result.value) : handlers.err(result.error); -} - -/** - * Try to execute a synchronous function and wrap in Result. - * - * @example - * ```typescript - * const result = tryCatch(() => JSON.parse(jsonString)); - * ``` - */ -export function tryCatch(fn: () => T): Result { - try { - return ok(fn()); - } catch (error) { - if (error instanceof AppError) { - return err(error); - } - return err( - new AppError({ - code: ErrorCode.UNKNOWN_ERROR, - message: error instanceof Error ? error.message : String(error), - cause: error instanceof Error ? error : undefined, - }) - ); - } -} - -/** - * Try to execute an async function and wrap in Result. - * - * @example - * ```typescript - * const result = await tryCatchAsync(() => fetch(url).then(r => r.json())); - * ``` - */ -export async function tryCatchAsync(fn: () => Promise): AsyncResult { - try { - return ok(await fn()); - } catch (error) { - if (error instanceof AppError) { - return err(error); - } - return err( - new AppError({ - code: ErrorCode.UNKNOWN_ERROR, - message: error instanceof Error ? error.message : String(error), - cause: error instanceof Error ? error : undefined, - }) - ); - } -} - -/** - * Combine multiple Results - returns first error or array of all values. - * - * @example - * ```typescript - * const results = await Promise.all([ - * getUser(id1), - * getUser(id2), - * getUser(id3), - * ]); - * const combined = combine(results); - * if (isOk(combined)) { - * const [user1, user2, user3] = combined.value; - * } - * ``` - */ -export function combine(results: Result[]): Result { - const values: T[] = []; - for (const result of results) { - if (isErr(result)) { - return result; - } - values.push(result.value); - } - return ok(values); -} - -/** - * Convert a nullable value to a Result. - * - * @example - * ```typescript - * const result = fromNullable( - * maybeUser, - * () => new NotFoundError('User', id) - * ); - * ``` - */ -export function fromNullable( - value: T | null | undefined, - errorFn: () => E -): Result { - return value != null ? ok(value) : err(errorFn()); -} - -/** - * Convert a Result to a nullable value (loses error information). - * - * @example - * ```typescript - * const user = toNullable(result); // User | null - * ``` - */ -export function toNullable(result: Result): T | null { - return isOk(result) ? result.value : null; -} diff --git a/packages/shared-errors/src/utils/index.ts b/packages/shared-errors/src/utils/index.ts deleted file mode 100644 index deeec5183..000000000 --- a/packages/shared-errors/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './wrap'; diff --git a/packages/shared-errors/src/utils/wrap.ts b/packages/shared-errors/src/utils/wrap.ts deleted file mode 100644 index d62240f8f..000000000 --- a/packages/shared-errors/src/utils/wrap.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { AppError } from '../errors/app-error'; -import type { ErrorContext } from '../errors/app-error'; -import { ErrorCode } from '../types/error-codes'; -import { isAppError } from '../guards/type-guards'; - -/** - * Wrap an error with context. - * Similar to Go's `fmt.Errorf("context: %w", err)`. - * - * @param error - The error to wrap (can be any type) - * @param context - Description of the operation that failed - * @param additionalContext - Extra context data to include - * @returns An AppError with the original as its cause - * - * @example - * ```typescript - * try { - * await fetchData(); - * } catch (error) { - * return err(wrap(error, 'fetching user data')); - * } - * ``` - */ -export function wrap(error: unknown, context: string, additionalContext?: ErrorContext): AppError { - if (isAppError(error)) { - return error.wrap(context, additionalContext); - } - - const message = error instanceof Error ? error.message : String(error); - return new AppError({ - code: ErrorCode.UNKNOWN_ERROR, - message: `${context}: ${message}`, - cause: error instanceof Error ? error : undefined, - context: additionalContext, - }); -} - -/** - * Convert any error to AppError. - * If already an AppError, returns it unchanged. - * - * @example - * ```typescript - * try { - * await riskyOperation(); - * } catch (error) { - * return err(toAppError(error)); - * } - * ``` - */ -export function toAppError(error: unknown): AppError { - if (isAppError(error)) { - return error; - } - - if (error instanceof Error) { - return new AppError({ - code: ErrorCode.UNKNOWN_ERROR, - message: error.message, - cause: error, - }); - } - - return new AppError({ - code: ErrorCode.UNKNOWN_ERROR, - message: String(error), - }); -} - -/** - * Get the cause of an error. - * - * @example - * ```typescript - * const originalError = cause(wrappedError); - * ``` - */ -export function cause(error: AppError): Error | undefined { - return error.cause; -} - -/** - * Get the root cause of an error chain. - * Traverses all causes to find the original error. - * - * @example - * ```typescript - * const original = rootCause(deeplyWrappedError); - * ``` - */ -export function rootCause(error: AppError): Error { - return error.rootCause(); -} diff --git a/packages/shared-errors/tsconfig.build.json b/packages/shared-errors/tsconfig.build.json deleted file mode 100644 index af7d37ecf..000000000 --- a/packages/shared-errors/tsconfig.build.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "moduleResolution": "node", - "lib": ["ES2022"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-errors/tsconfig.json b/packages/shared-errors/tsconfig.json deleted file mode 100644 index be640ab2d..000000000 --- a/packages/shared-errors/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "declaration": true, - "declarationMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} diff --git a/packages/shared-splitscreen/package.json b/packages/shared-splitscreen/package.json deleted file mode 100644 index 26d5f6ea1..000000000 --- a/packages/shared-splitscreen/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@mana/shared-splitscreen", - "version": "0.1.0", - "private": true, - "type": "module", - "svelte": "./src/index.ts", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": { - "svelte": "./src/index.ts", - "types": "./src/index.ts", - "default": "./src/index.ts" - }, - "./store": { - "svelte": "./src/stores/split-panel.svelte.ts", - "default": "./src/stores/split-panel.svelte.ts" - }, - "./types": { - "types": "./src/types.ts", - "default": "./src/types.ts" - }, - "./utils": { - "default": "./src/utils/index.ts" - } - }, - "scripts": { - "lint": "eslint .", - "check": "svelte-check --tsconfig ./tsconfig.json" - }, - "peerDependencies": { - "svelte": "^5.0.0" - }, - "devDependencies": { - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0" - } -} diff --git a/packages/shared-splitscreen/src/components/AppPanel.svelte b/packages/shared-splitscreen/src/components/AppPanel.svelte deleted file mode 100644 index a27699a9b..000000000 --- a/packages/shared-splitscreen/src/components/AppPanel.svelte +++ /dev/null @@ -1,155 +0,0 @@ - - -
- {#if isLoading} -
-
- Loading {panel.name || panel.appId}... -
- {/if} - - {#if hasError} -
- - - - - - Failed to load {panel.name || panel.appId} - -
- {/if} - - -
- - diff --git a/packages/shared-splitscreen/src/components/PanelControls.svelte b/packages/shared-splitscreen/src/components/PanelControls.svelte deleted file mode 100644 index 925b4c237..000000000 --- a/packages/shared-splitscreen/src/components/PanelControls.svelte +++ /dev/null @@ -1,112 +0,0 @@ - - -
- {panelName} - -
- - - -
-
- - diff --git a/packages/shared-splitscreen/src/components/ResizeHandle.svelte b/packages/shared-splitscreen/src/components/ResizeHandle.svelte deleted file mode 100644 index 843573e7b..000000000 --- a/packages/shared-splitscreen/src/components/ResizeHandle.svelte +++ /dev/null @@ -1,203 +0,0 @@ - - - - - diff --git a/packages/shared-splitscreen/src/components/SplitPaneContainer.svelte b/packages/shared-splitscreen/src/components/SplitPaneContainer.svelte deleted file mode 100644 index 9020a2d61..000000000 --- a/packages/shared-splitscreen/src/components/SplitPaneContainer.svelte +++ /dev/null @@ -1,139 +0,0 @@ - - -
- - {#if isResizing} -
- {/if} - -
- {@render children()} -
- - {#if splitPanel.isActive && splitPanel.rightPanel} - - -
- - splitPanel.swapPanels()} - onClose={() => splitPanel.closePanel()} - /> -
- {/if} -
- - diff --git a/packages/shared-splitscreen/src/index.ts b/packages/shared-splitscreen/src/index.ts deleted file mode 100644 index 7f1b39781..000000000 --- a/packages/shared-splitscreen/src/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @mana/shared-splitscreen - * - * Split-screen panel system for Mana apps. - * Enables displaying two apps side-by-side using iFrames. - */ - -// Types -export type { - PanelConfig, - SplitScreenState, - AppDefinition, - PanelEvent, - StorageConfig, - UrlState, -} from './types.js'; - -export { DIVIDER_CONSTRAINTS, MOBILE_BREAKPOINT } from './types.js'; - -// Store -export { - createSplitPanelStore, - setSplitPanelContext, - getSplitPanelContext, - hasSplitPanelContext, - DEFAULT_APPS, - type SplitPanelStore, -} from './stores/split-panel.svelte.js'; - -// Utils -export { - parseUrlState, - updateUrlState, - clearUrlState, - getCurrentUrlState, - savePanelState, - loadPanelState, - clearPanelState, - createStorageConfig, -} from './utils/index.js'; - -// Components (will be added) -export { default as SplitPaneContainer } from './components/SplitPaneContainer.svelte'; -export { default as AppPanel } from './components/AppPanel.svelte'; -export { default as PanelControls } from './components/PanelControls.svelte'; -export { default as ResizeHandle } from './components/ResizeHandle.svelte'; diff --git a/packages/shared-splitscreen/src/stores/split-panel.svelte.ts b/packages/shared-splitscreen/src/stores/split-panel.svelte.ts deleted file mode 100644 index 9bfd13e6f..000000000 --- a/packages/shared-splitscreen/src/stores/split-panel.svelte.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Split-Panel Store - * Svelte 5 runes-based state management for split-screen panels. - */ - -import { getContext, setContext } from 'svelte'; -import type { PanelConfig, AppDefinition, StorageConfig } from '../types.js'; -import { DIVIDER_CONSTRAINTS, MOBILE_BREAKPOINT } from '../types.js'; -import { savePanelState, loadPanelState, createStorageConfig } from '../utils/local-storage.js'; -import { updateUrlState, clearUrlState, getCurrentUrlState } from '../utils/url-state.js'; - -const SPLIT_PANEL_CONTEXT_KEY = Symbol('split-panel'); - -/** - * Available apps that can be opened in split-screen. - */ -export const DEFAULT_APPS: AppDefinition[] = [ - { - id: 'calendar', - name: 'Calendar', - baseUrl: 'http://localhost:5179', - icon: 'calendar', - color: '#3b82f6', - }, - { - id: 'todo', - name: 'Todo', - baseUrl: 'http://localhost:5188', - icon: 'check-square', - color: '#10b981', - }, - { - id: 'contacts', - name: 'Contacts', - baseUrl: 'http://localhost:5184', - icon: 'users', - color: '#8b5cf6', - }, - { - id: 'clock', - name: 'Clock', - baseUrl: 'http://localhost:5187', - icon: 'clock', - color: '#f59e0b', - }, -]; - -export interface SplitPanelStore { - // State - readonly isActive: boolean; - readonly rightPanel: PanelConfig | null; - readonly dividerPosition: number; - readonly isMobile: boolean; - - // Available apps (excluding current) - readonly availableApps: AppDefinition[]; - - // Actions - openPanel: (appId: string, path?: string) => void; - closePanel: () => void; - swapPanels: () => void; - setDividerPosition: (position: number) => void; - resetDividerPosition: () => void; - initialize: () => void; -} - -/** - * Create a split-panel store for an app. - */ -export function createSplitPanelStore( - currentAppId: string, - apps: AppDefinition[] = DEFAULT_APPS -): SplitPanelStore { - // Reactive state using Svelte 5 runes - let isActive = $state(false); - let rightPanel = $state(null); - let dividerPosition = $state(DIVIDER_CONSTRAINTS.DEFAULT); - let isMobile = $state(false); - - // Storage config for persistence - const storageConfig: StorageConfig = createStorageConfig(currentAppId); - - // Filter out current app from available apps - const availableApps = $derived(apps.filter((app) => app.id !== currentAppId)); - - /** - * Open an app in the right panel. - */ - function openPanel(appId: string, path = '/'): void { - if (isMobile) return; - - const app = apps.find((a) => a.id === appId); - if (!app || app.id === currentAppId) return; - - const url = `${app.baseUrl}${path}`; - - rightPanel = { - appId: app.id, - url, - name: app.name, - }; - isActive = true; - - // Persist to URL and localStorage - updateUrlState({ panel: appId, split: dividerPosition }); - savePanelState(storageConfig, { rightPanel, dividerPosition, isActive: true }); - } - - /** - * Close the split panel. - */ - function closePanel(): void { - rightPanel = null; - isActive = false; - - // Clear persistence - clearUrlState(); - savePanelState(storageConfig, { rightPanel: null, dividerPosition, isActive: false }); - } - - /** - * Swap left and right panels (navigate to the right panel app). - */ - function swapPanels(): void { - if (!rightPanel) return; - - // Navigate to the other app - const targetUrl = rightPanel.url; - window.location.href = targetUrl; - } - - /** - * Set the divider position. - */ - function setDividerPosition(position: number): void { - const clamped = Math.max(DIVIDER_CONSTRAINTS.MIN, Math.min(DIVIDER_CONSTRAINTS.MAX, position)); - dividerPosition = clamped; - - // Persist - if (isActive) { - updateUrlState({ panel: rightPanel?.appId, split: clamped }); - savePanelState(storageConfig, { rightPanel, dividerPosition: clamped, isActive }); - } - } - - /** - * Reset divider to default position. - */ - function resetDividerPosition(): void { - setDividerPosition(DIVIDER_CONSTRAINTS.DEFAULT); - } - - /** - * Initialize from URL and localStorage. - */ - function initialize(): void { - if (typeof window === 'undefined') return; - - // Check mobile - const checkMobile = () => { - isMobile = window.innerWidth < MOBILE_BREAKPOINT; - if (isMobile && isActive) { - closePanel(); - } - }; - - checkMobile(); - window.addEventListener('resize', checkMobile); - - // Load from URL first, then localStorage - const urlState = getCurrentUrlState(); - const storedState = loadPanelState(storageConfig); - - const panelAppId = urlState.panel || storedState?.rightPanel?.appId; - const savedPosition = urlState.split || storedState?.dividerPosition; - - if (panelAppId && !isMobile) { - const app = apps.find((a) => a.id === panelAppId); - if (app && app.id !== currentAppId) { - openPanel(panelAppId); - if (savedPosition) { - setDividerPosition(savedPosition); - } - } - } - } - - // Return the store interface with getters for reactive access - return { - get isActive() { - return isActive; - }, - get rightPanel() { - return rightPanel; - }, - get dividerPosition() { - return dividerPosition; - }, - get isMobile() { - return isMobile; - }, - get availableApps() { - return availableApps; - }, - openPanel, - closePanel, - swapPanels, - setDividerPosition, - resetDividerPosition, - initialize, - }; -} - -/** - * Set the split-panel store in Svelte context. - * Call this in your layout component. - */ -export function setSplitPanelContext( - currentAppId: string, - apps: AppDefinition[] = DEFAULT_APPS -): SplitPanelStore { - const store = createSplitPanelStore(currentAppId, apps); - setContext(SPLIT_PANEL_CONTEXT_KEY, store); - return store; -} - -/** - * Get the split-panel store from Svelte context. - * Call this in child components. - */ -export function getSplitPanelContext(): SplitPanelStore { - const store = getContext(SPLIT_PANEL_CONTEXT_KEY); - if (!store) { - throw new Error( - '[SplitScreen] No split-panel context found. Did you call setSplitPanelContext in a parent component?' - ); - } - return store; -} - -/** - * Check if split-panel context exists. - */ -export function hasSplitPanelContext(): boolean { - try { - getContext(SPLIT_PANEL_CONTEXT_KEY); - return true; - } catch { - return false; - } -} diff --git a/packages/shared-splitscreen/src/types.ts b/packages/shared-splitscreen/src/types.ts deleted file mode 100644 index 52c185ca8..000000000 --- a/packages/shared-splitscreen/src/types.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Split-Screen Types - * Type definitions for the split-screen panel system. - */ - -/** - * Configuration for a panel showing an app in an iFrame. - */ -export interface PanelConfig { - /** Unique identifier for the app (e.g., 'calendar', 'todo', 'contacts') */ - appId: string; - /** Full URL to load in the iFrame */ - url: string; - /** Display name for the app */ - name?: string; -} - -/** - * State of the split-screen system. - */ -export interface SplitScreenState { - /** Whether split-screen mode is active */ - isActive: boolean; - /** Configuration for the right panel (null when not in split mode) */ - rightPanel: PanelConfig | null; - /** Position of the divider as percentage (20-80) */ - dividerPosition: number; -} - -/** - * App registration for the split-screen system. - * Used to define which apps can be opened in panels. - */ -export interface AppDefinition { - /** Unique app identifier */ - id: string; - /** Display name */ - name: string; - /** Base URL for the app */ - baseUrl: string; - /** Icon name (Lucide icon) */ - icon?: string; - /** App theme color */ - color?: string; -} - -/** - * Event payload for panel operations. - */ -export interface PanelEvent { - type: 'open' | 'close' | 'swap' | 'resize'; - panel?: PanelConfig; - dividerPosition?: number; -} - -/** - * Storage key configuration. - */ -export interface StorageConfig { - /** Key prefix for localStorage */ - prefix: string; - /** Current app ID for scoped storage */ - currentAppId: string; -} - -/** - * URL state parameters for split-screen. - */ -export interface UrlState { - /** App ID for the right panel */ - panel?: string; - /** Divider position percentage */ - split?: number; -} - -/** - * Minimum and maximum constraints for divider position. - */ -export const DIVIDER_CONSTRAINTS = { - MIN: 20, - MAX: 80, - DEFAULT: 50, -} as const; - -/** - * Breakpoint for disabling split-screen on mobile. - */ -export const MOBILE_BREAKPOINT = 1024; diff --git a/packages/shared-splitscreen/src/utils/index.ts b/packages/shared-splitscreen/src/utils/index.ts deleted file mode 100644 index da6659145..000000000 --- a/packages/shared-splitscreen/src/utils/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Split-Screen Utilities - * Re-export all utility functions. - */ - -export { parseUrlState, updateUrlState, clearUrlState, getCurrentUrlState } from './url-state.js'; - -export { - savePanelState, - loadPanelState, - clearPanelState, - createStorageConfig, -} from './local-storage.js'; diff --git a/packages/shared-splitscreen/src/utils/local-storage.ts b/packages/shared-splitscreen/src/utils/local-storage.ts deleted file mode 100644 index 810a0e731..000000000 --- a/packages/shared-splitscreen/src/utils/local-storage.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * LocalStorage Utilities - * Handle persistent storage for split-screen preferences. - */ - -import type { SplitScreenState, StorageConfig } from '../types.js'; -import { DIVIDER_CONSTRAINTS } from '../types.js'; - -const STORAGE_VERSION = 1; - -interface StoredState { - version: number; - state: Partial; -} - -/** - * Generate storage key for an app. - */ -function getStorageKey(config: StorageConfig): string { - return `${config.prefix}-splitscreen-${config.currentAppId}`; -} - -/** - * Save split-screen state to localStorage. - */ -export function savePanelState(config: StorageConfig, state: Partial): void { - if (typeof window === 'undefined') return; - - try { - const stored: StoredState = { - version: STORAGE_VERSION, - state: { - dividerPosition: state.dividerPosition, - rightPanel: state.rightPanel, - }, - }; - localStorage.setItem(getStorageKey(config), JSON.stringify(stored)); - } catch (_error) { - // localStorage not available or quota exceeded - } -} - -/** - * Load split-screen state from localStorage. - */ -export function loadPanelState(config: StorageConfig): Partial | null { - if (typeof window === 'undefined') return null; - - try { - const raw = localStorage.getItem(getStorageKey(config)); - if (!raw) return null; - - const stored: StoredState = JSON.parse(raw); - - // Version check for future migrations - if (stored.version !== STORAGE_VERSION) { - clearPanelState(config); - return null; - } - - // Validate divider position - if (stored.state.dividerPosition !== undefined) { - stored.state.dividerPosition = Math.max( - DIVIDER_CONSTRAINTS.MIN, - Math.min(DIVIDER_CONSTRAINTS.MAX, stored.state.dividerPosition) - ); - } - - return stored.state; - } catch (_error) { - // localStorage not available or corrupted data - return null; - } -} - -/** - * Clear split-screen state from localStorage. - */ -export function clearPanelState(config: StorageConfig): void { - if (typeof window === 'undefined') return; - - try { - localStorage.removeItem(getStorageKey(config)); - } catch (_error) { - // localStorage not available - } -} - -/** - * Get default storage config with mana prefix. - */ -export function createStorageConfig(currentAppId: string): StorageConfig { - return { - prefix: 'mana', - currentAppId, - }; -} diff --git a/packages/shared-splitscreen/src/utils/url-state.ts b/packages/shared-splitscreen/src/utils/url-state.ts deleted file mode 100644 index 11fd47229..000000000 --- a/packages/shared-splitscreen/src/utils/url-state.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * URL State Utilities - * Handle URL-based state persistence for split-screen. - */ - -import type { UrlState } from '../types.js'; - -/** - * Parse split-screen state from URL search params. - * Reads `?panel=todo&split=60` format. - */ -export function parseUrlState(searchParams: URLSearchParams): UrlState { - const panel = searchParams.get('panel') || undefined; - const splitStr = searchParams.get('split'); - const split = splitStr ? parseInt(splitStr, 10) : undefined; - - return { - panel, - split: split && !isNaN(split) ? split : undefined, - }; -} - -/** - * Update URL with split-screen state without page reload. - * Uses replaceState to avoid adding to browser history. - */ -export function updateUrlState(state: UrlState): void { - if (typeof window === 'undefined') return; - - const url = new URL(window.location.href); - - if (state.panel) { - url.searchParams.set('panel', state.panel); - } else { - url.searchParams.delete('panel'); - } - - if (state.split && state.split !== 50) { - url.searchParams.set('split', state.split.toString()); - } else { - url.searchParams.delete('split'); - } - - window.history.replaceState({}, '', url.toString()); -} - -/** - * Clear split-screen state from URL. - */ -export function clearUrlState(): void { - if (typeof window === 'undefined') return; - - const url = new URL(window.location.href); - url.searchParams.delete('panel'); - url.searchParams.delete('split'); - window.history.replaceState({}, '', url.toString()); -} - -/** - * Get current URL state. - */ -export function getCurrentUrlState(): UrlState { - if (typeof window === 'undefined') return {}; - return parseUrlState(new URLSearchParams(window.location.search)); -} diff --git a/packages/shared-splitscreen/tsconfig.json b/packages/shared-splitscreen/tsconfig.json deleted file mode 100644 index 07a6403c2..000000000 --- a/packages/shared-splitscreen/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "types": ["svelte"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} diff --git a/packages/shared-vite-config/src/index.ts b/packages/shared-vite-config/src/index.ts index 5f915db1e..c1d757916 100644 --- a/packages/shared-vite-config/src/index.ts +++ b/packages/shared-vite-config/src/index.ts @@ -16,23 +16,13 @@ export const MANA_SHARED_PACKAGES = [ '@mana/shared-tailwind', '@mana/shared-theme', '@mana/shared-theme-ui', - '@mana/shared-feedback-ui', - '@mana/shared-feedback-service', - '@mana/shared-feedback-types', '@mana/shared-auth', '@mana/shared-auth-ui', '@mana/shared-branding', - '@mana/shared-subscription-ui', - '@mana/shared-profile-ui', '@mana/shared-i18n', - '@mana/shared-api-client', - '@mana/shared-splitscreen', '@mana/shared-utils', '@mana/shared-tags', '@mana/shared-stores', - '@mana/shared-help-types', - '@mana/shared-help-content', - '@mana/shared-help-ui', ] as const; /**