diff --git a/CLAUDE.md b/CLAUDE.md index 8d741a4..c4d9d80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,6 +75,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Styling:** Tailwind CSS v4 - **Database:** PostgreSQL 16+ - **ORM:** Drizzle ORM (TypeScript-first, performant) + - **Important:** Use `(table) => [...]` for indexes/constraints, NOT `(table) => ({...})` - **Queue System:** BullMQ (MIT License) - Async job processing - **In-Memory Store:** Redis 7 - Queue storage, sessions, caching - **Auth:** Cidaas (OIDC/OAuth2) - external platform diff --git a/package.json b/package.json index 74ab6d3..be4a792 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", "db:push": "drizzle-kit push", - "db:seed": "tsx server/database/seed.ts" + "db:clean": "tsx server/database/clean.ts", + "db:seed": "tsx server/database/seed.ts", + "db:reset": "pnpm db:clean && pnpm db:seed" }, "dependencies": { "@nuxtjs/i18n": "^10.1.2", diff --git a/server/database/clean.ts b/server/database/clean.ts new file mode 100644 index 0000000..1751e0a --- /dev/null +++ b/server/database/clean.ts @@ -0,0 +1,75 @@ +/** + * Database Clean Script + * + * Removes all data from the database while preserving the schema. + * Useful for resetting to a clean state before seeding. + * Run with: pnpm db:clean + */ + +import 'dotenv/config' +import { drizzle } from 'drizzle-orm/postgres-js' +import { sql } from 'drizzle-orm' +import postgres from 'postgres' + +async function clean() { + // Get database connection from environment + const connectionString = process.env.DATABASE_URL + if (!connectionString) { + throw new Error('DATABASE_URL environment variable is not set') + } + + console.log('๐Ÿงน Starting database cleanup...\n') + + // Create database connection + const client = postgres(connectionString) + const db = drizzle(client) + + try { + // Delete all data in reverse order of dependencies + console.log('Deleting product-role assignments...') + await db.execute(sql`DELETE FROM product_role_visibility`) + + console.log('Deleting user-role assignments...') + await db.execute(sql`DELETE FROM user_roles`) + + console.log('Deleting order items...') + await db.execute(sql`DELETE FROM order_items`) + + console.log('Deleting orders...') + await db.execute(sql`DELETE FROM orders`) + + console.log('Deleting cart items...') + await db.execute(sql`DELETE FROM cart_items`) + + console.log('Deleting carts...') + await db.execute(sql`DELETE FROM carts`) + + console.log('Deleting products...') + await db.execute(sql`DELETE FROM products`) + + console.log('Deleting roles...') + await db.execute(sql`DELETE FROM roles`) + + console.log('Deleting users...') + await db.execute(sql`DELETE FROM users`) + + console.log('\nโœ… Database cleaned successfully!') + console.log('๐Ÿ’ก Run `pnpm db:seed` to populate with fresh data') + } catch (error) { + console.error('โŒ Error cleaning database:', error) + throw error + } finally { + // Close database connection + await client.end() + } +} + +// Run clean function +clean() + .then(() => { + process.exit(0) + }) + .catch((error) => { + console.error('Fatal error:', error) + process.exit(1) + }) diff --git a/server/database/schema.ts b/server/database/schema.ts index 072e22f..9dd2e21 100644 --- a/server/database/schema.ts +++ b/server/database/schema.ts @@ -149,11 +149,11 @@ export const products = pgTable( createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, - (table) => ({ - navProductIdIdx: index('products_nav_product_id_idx').on(table.navProductId), - activeIdx: index('products_active_idx').on(table.active), - categoryIdx: index('products_category_idx').on(table.category), - }) + (table) => [ + index('products_nav_product_id_idx').on(table.navProductId), + index('products_active_idx').on(table.active), + index('products_category_idx').on(table.category), + ] ) /** @@ -204,15 +204,12 @@ export const userRoles = pgTable( createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, - (table) => ({ + (table) => [ // Unique constraint: User can only have one entry per role - userIdRoleIdUnique: index('user_roles_user_id_role_id_unique').on( - table.userId, - table.roleId - ), - userIdIdx: index('user_roles_user_id_idx').on(table.userId), - statusIdx: index('user_roles_status_idx').on(table.status), - }) + index('user_roles_user_id_role_id_unique').on(table.userId, table.roleId), + index('user_roles_user_id_idx').on(table.userId), + index('user_roles_status_idx').on(table.status), + ] ) /** @@ -233,14 +230,11 @@ export const productRoleVisibility = pgTable( .references(() => roles.id, { onDelete: 'cascade' }), createdAt: timestamp('created_at').defaultNow().notNull(), }, - (table) => ({ + (table) => [ // Unique constraint: Product-Role pair can only exist once - productIdRoleIdUnique: index('product_role_visibility_product_id_role_id_unique').on( - table.productId, - table.roleId - ), - productIdIdx: index('product_role_visibility_product_id_idx').on(table.productId), - }) + index('product_role_visibility_product_id_role_id_unique').on(table.productId, table.roleId), + index('product_role_visibility_product_id_idx').on(table.productId), + ] ) /** @@ -296,11 +290,11 @@ export const orders = pgTable( createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, - (table) => ({ - orderNumberIdx: index('orders_order_number_idx').on(table.orderNumber), - userIdIdx: index('orders_user_id_idx').on(table.userId), - statusIdx: index('orders_status_idx').on(table.status), - }) + (table) => [ + index('orders_order_number_idx').on(table.orderNumber), + index('orders_user_id_idx').on(table.userId), + index('orders_status_idx').on(table.status), + ] ) /** diff --git a/server/database/seed.ts b/server/database/seed.ts index adf3277..c605220 100644 --- a/server/database/seed.ts +++ b/server/database/seed.ts @@ -47,7 +47,16 @@ const standardRoles = [ /** * Sample annual pass products for experimenta */ -const mockProducts = [ +const mockProducts: Array<{ + navProductId: string + name: string + description: string + price: string + stockQuantity: number + category: string + active: boolean + roles: Array<'private' | 'educator' | 'company'> +}> = [ { navProductId: 'MSPACE-JK-2025', name: 'Makerspace Jahreskarte', @@ -57,6 +66,7 @@ const mockProducts = [ stockQuantity: 100, category: 'makerspace-annual-pass', active: true, + roles: ['private', 'educator', 'company'], }, { navProductId: 'EXPERIMENTA-JK-2025', @@ -67,6 +77,7 @@ const mockProducts = [ stockQuantity: 200, category: 'annual-pass', active: true, + roles: ['private', 'educator', 'company'], }, { navProductId: 'PAEDAGOGEN-JK-2025', @@ -77,6 +88,7 @@ const mockProducts = [ stockQuantity: 50, category: 'educator-annual-pass', active: true, + roles: ['educator'], }, ] @@ -151,24 +163,22 @@ async function seed() { console.log(` - ${product.name} (${product.navProductId}) - โ‚ฌ${product.price}`) }) - // 3. Assign Roles to Products (Category-based mapping) - console.log(`\n๐Ÿ”— Assigning roles to products based on category...`) - - // Category to role mapping - const categoryRoleMapping: Record = { - 'makerspace-annual-pass': ['private', 'educator'], - 'annual-pass': ['private'], - 'educator-annual-pass': ['educator'], - } + // 3. Assign Roles to Products (using roles array from mock data) + console.log(`\n๐Ÿ”— Assigning roles to products...`) let assignmentCount = 0 - for (const product of insertedProducts) { - const roleCodes = categoryRoleMapping[product.category] || [] + for (let i = 0; i < insertedProducts.length; i++) { + const product = insertedProducts[i] + const productData = mockProducts[i] + const roleCodes = productData.roles || [] for (const roleCode of roleCodes) { // Find role by code const role = insertedRoles.find((r) => r.code === roleCode) - if (!role) continue + if (!role) { + console.warn(` โš ๏ธ Role '${roleCode}' not found for product ${product.name}`) + continue + } // Check if assignment already exists const existing = await db.query.productRoleVisibility.findFirst({