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.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
Normal file
75
server/database/clean.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
@@ -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),
|
||||||
})
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user