Browse Source

Enhance database management with cleanup and reset scripts

- Added a new database cleanup script to remove all data while preserving the schema, facilitating a clean state before seeding.
- Updated package.json to include new commands for database cleaning and resetting.
- Modified schema definitions to use an array format for index and constraint definitions, improving clarity and consistency.
- Updated product seeding logic to utilize role-based assignments directly from mock data, enhancing flexibility in product-role relationships.
main
Bastian Masanek 2 months ago
parent
commit
268d91f548
  1. 1
      CLAUDE.md
  2. 4
      package.json
  3. 75
      server/database/clean.ts
  4. 44
      server/database/schema.ts
  5. 36
      server/database/seed.ts

1
CLAUDE.md

@ -75,6 +75,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Styling:** Tailwind CSS v4 - **Styling:** Tailwind CSS v4
- **Database:** PostgreSQL 16+ - **Database:** PostgreSQL 16+
- **ORM:** Drizzle ORM (TypeScript-first, performant) - **ORM:** Drizzle ORM (TypeScript-first, performant)
- **Important:** Use `(table) => [...]` for indexes/constraints, NOT `(table) => ({...})`
- **Queue System:** BullMQ (MIT License) - Async job processing - **Queue System:** BullMQ (MIT License) - Async job processing
- **In-Memory Store:** Redis 7 - Queue storage, sessions, caching - **In-Memory Store:** Redis 7 - Queue storage, sessions, caching
- **Auth:** Cidaas (OIDC/OAuth2) - external platform - **Auth:** Cidaas (OIDC/OAuth2) - external platform

4
package.json

@ -15,7 +15,9 @@
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"db:push": "drizzle-kit push", "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": { "dependencies": {
"@nuxtjs/i18n": "^10.1.2", "@nuxtjs/i18n": "^10.1.2",

75
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)
})

44
server/database/schema.ts

@ -149,11 +149,11 @@ export const products = pgTable(
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, },
(table) => ({ (table) => [
navProductIdIdx: index('products_nav_product_id_idx').on(table.navProductId), index('products_nav_product_id_idx').on(table.navProductId),
activeIdx: index('products_active_idx').on(table.active), index('products_active_idx').on(table.active),
categoryIdx: index('products_category_idx').on(table.category), index('products_category_idx').on(table.category),
}) ]
) )
/** /**
@ -204,15 +204,12 @@ export const userRoles = pgTable(
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, },
(table) => ({ (table) => [
// Unique constraint: User can only have one entry per role // Unique constraint: User can only have one entry per role
userIdRoleIdUnique: index('user_roles_user_id_role_id_unique').on( index('user_roles_user_id_role_id_unique').on(table.userId, table.roleId),
table.userId, index('user_roles_user_id_idx').on(table.userId),
table.roleId index('user_roles_status_idx').on(table.status),
), ]
userIdIdx: index('user_roles_user_id_idx').on(table.userId),
statusIdx: index('user_roles_status_idx').on(table.status),
})
) )
/** /**
@ -233,14 +230,11 @@ export const productRoleVisibility = pgTable(
.references(() => roles.id, { onDelete: 'cascade' }), .references(() => roles.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}, },
(table) => ({ (table) => [
// Unique constraint: Product-Role pair can only exist once // Unique constraint: Product-Role pair can only exist once
productIdRoleIdUnique: index('product_role_visibility_product_id_role_id_unique').on( index('product_role_visibility_product_id_role_id_unique').on(table.productId, table.roleId),
table.productId, index('product_role_visibility_product_id_idx').on(table.productId),
table.roleId ]
),
productIdIdx: index('product_role_visibility_product_id_idx').on(table.productId),
})
) )
/** /**
@ -296,11 +290,11 @@ export const orders = pgTable(
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, },
(table) => ({ (table) => [
orderNumberIdx: index('orders_order_number_idx').on(table.orderNumber), index('orders_order_number_idx').on(table.orderNumber),
userIdIdx: index('orders_user_id_idx').on(table.userId), index('orders_user_id_idx').on(table.userId),
statusIdx: index('orders_status_idx').on(table.status), index('orders_status_idx').on(table.status),
}) ]
) )
/** /**

36
server/database/seed.ts

@ -47,7 +47,16 @@ const standardRoles = [
/** /**
* Sample annual pass products for experimenta * 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', navProductId: 'MSPACE-JK-2025',
name: 'Makerspace Jahreskarte', name: 'Makerspace Jahreskarte',
@ -57,6 +66,7 @@ const mockProducts = [
stockQuantity: 100, stockQuantity: 100,
category: 'makerspace-annual-pass', category: 'makerspace-annual-pass',
active: true, active: true,
roles: ['private', 'educator', 'company'],
}, },
{ {
navProductId: 'EXPERIMENTA-JK-2025', navProductId: 'EXPERIMENTA-JK-2025',
@ -67,6 +77,7 @@ const mockProducts = [
stockQuantity: 200, stockQuantity: 200,
category: 'annual-pass', category: 'annual-pass',
active: true, active: true,
roles: ['private', 'educator', 'company'],
}, },
{ {
navProductId: 'PAEDAGOGEN-JK-2025', navProductId: 'PAEDAGOGEN-JK-2025',
@ -77,6 +88,7 @@ const mockProducts = [
stockQuantity: 50, stockQuantity: 50,
category: 'educator-annual-pass', category: 'educator-annual-pass',
active: true, active: true,
roles: ['educator'],
}, },
] ]
@ -151,24 +163,22 @@ async function seed() {
console.log(` - ${product.name} (${product.navProductId}) - €${product.price}`) console.log(` - ${product.name} (${product.navProductId}) - €${product.price}`)
}) })
// 3. Assign Roles to Products (Category-based mapping) // 3. Assign Roles to Products (using roles array from mock data)
console.log(`\n🔗 Assigning roles to products based on category...`) console.log(`\n🔗 Assigning roles to products...`)
// Category to role mapping
const categoryRoleMapping: Record<string, string[]> = {
'makerspace-annual-pass': ['private', 'educator'],
'annual-pass': ['private'],
'educator-annual-pass': ['educator'],
}
let assignmentCount = 0 let assignmentCount = 0
for (const product of insertedProducts) { for (let i = 0; i < insertedProducts.length; i++) {
const roleCodes = categoryRoleMapping[product.category] || [] const product = insertedProducts[i]
const productData = mockProducts[i]
const roleCodes = productData.roles || []
for (const roleCode of roleCodes) { for (const roleCode of roleCodes) {
// Find role by code // Find role by code
const role = insertedRoles.find((r) => r.code === roleCode) 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 // Check if assignment already exists
const existing = await db.query.productRoleVisibility.findFirst({ const existing = await db.query.productRoleVisibility.findFirst({

Loading…
Cancel
Save