485 lines
11 KiB
Markdown

# Database Seeding
## Overview
The database seeding process populates initial data required for the application to function. Seeding runs automatically after migrations in development but must be run manually in production.
**Seed Script:** `api/prisma/seed.ts`
**Seed Data:**
- Default super admin user
- Default map settings (Edmonton coordinates)
- 6 page blocks for landing page builder
- 4 email templates (campaign email, response verification, shift signup confirmation, shift details reminder)
---
## Running Seed
### Development
```bash
cd api
npm run seed
# OR
npx prisma db seed
```
### Production (Docker)
```bash
docker compose exec api npx prisma db seed
```
### CI/CD
Seed runs automatically after `prisma migrate deploy` if configured in `package.json`:
```json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
```
---
## Seed Data Details
### 1. Default Admin User
**Email:** `admin@cmlite.org`
**Password:** `ChangeMe2025!`
**Role:** `SUPER_ADMIN`
**Status:** `ACTIVE`
**Email Verified:** `true`
**Code:**
```typescript
const hashedPassword = await bcrypt.hash('ChangeMe2025!', 10);
const admin = await prisma.user.upsert({
where: { email: 'admin@cmlite.org' },
update: {
password: hashedPassword,
emailVerified: true,
status: 'ACTIVE',
},
create: {
email: 'admin@cmlite.org',
password: hashedPassword,
name: 'Admin',
role: UserRole.SUPER_ADMIN,
emailVerified: true,
},
});
```
**Security Note:** Change default password immediately after first login!
### 2. Default Map Settings
**ID:** `default` (singleton)
**Coordinates:** Edmonton, AB (53.5461°N, 113.4938°W)
**Zoom:** 11
**Walk Sheet:** Blank titles/footers
**Code:**
```typescript
await prisma.mapSettings.upsert({
where: { id: 'default' },
update: {},
create: {
id: 'default',
latitude: 53.5461,
longitude: -113.4938,
zoom: 11,
walkSheetTitle: 'Walk Sheet',
walkSheetSubtitle: '',
walkSheetFooter: '',
},
});
```
### 3. Page Blocks (6 blocks)
#### Hero Section
```typescript
{
id: 'default-hero',
type: 'hero',
label: 'Hero Section',
category: 'Headers',
sortOrder: 1,
schema: {
title: { type: 'string', label: 'Title' },
subtitle: { type: 'string', label: 'Subtitle' },
backgroundImage: { type: 'string', label: 'Background Image URL' },
ctaText: { type: 'string', label: 'Button Text' },
ctaUrl: { type: 'string', label: 'Button URL' },
},
defaults: {
title: 'Welcome to Our Campaign',
subtitle: 'Join us in making a difference in your community.',
backgroundImage: '',
ctaText: 'Get Involved',
ctaUrl: '#',
},
}
```
#### Text Block
```typescript
{
id: 'default-text',
type: 'text',
label: 'Text Block',
category: 'Content',
sortOrder: 2,
schema: {
heading: { type: 'string', label: 'Heading' },
body: { type: 'text', label: 'Body Text' },
},
defaults: {
heading: 'About Us',
body: 'Tell your story here...',
},
}
```
#### Features Grid
```typescript
{
id: 'default-features',
type: 'features',
label: 'Features Grid',
category: 'Content',
sortOrder: 3,
schema: {
features: {
type: 'array',
label: 'Features',
items: { title: 'string', description: 'string', icon: 'string' }
},
},
defaults: {
features: [
{ title: 'Community Action', description: 'Organize local events...', icon: '' },
{ title: 'Advocacy', description: 'Email your representatives...', icon: '' },
{ title: 'Volunteer', description: 'Sign up for shifts...', icon: '' },
],
},
}
```
#### Call to Action
```typescript
{
id: 'default-cta',
type: 'cta',
label: 'Call to Action',
category: 'Actions',
sortOrder: 4,
// ... (see seed.ts for full schema)
}
```
#### Testimonials
```typescript
{
id: 'default-testimonials',
type: 'testimonials',
label: 'Testimonials',
category: 'Content',
sortOrder: 5,
// ... (see seed.ts for full schema)
}
```
#### Contact Form
```typescript
{
id: 'default-contact-form',
type: 'contact-form',
label: 'Contact Form',
category: 'Actions',
sortOrder: 6,
// ... (see seed.ts for full schema)
}
```
### 4. Email Templates (4 templates)
#### Campaign Email to Representative
**Key:** `campaign-email`
**Category:** `INFLUENCE`
**Variables:** CAMPAIGN_TITLE, MESSAGE, USER_NAME, USER_EMAIL, POSTAL_CODE, RECIPIENT_NAME, RECIPIENT_LEVEL, ORGANIZATION_NAME, TIMESTAMP
**File Locations:**
- HTML: `api/src/templates/email/campaign-email.html`
- Text: `api/src/templates/email/campaign-email.txt`
**Seeding Logic:**
```typescript
const templateDef = {
key: 'campaign-email',
name: 'Campaign Email to Representative',
description: 'Email sent when a constituent contacts their elected representative through a campaign',
category: EmailTemplateCategory.INFLUENCE,
subjectLine: '{{CAMPAIGN_TITLE}} - Message from {{USER_NAME}}',
isSystem: true,
variables: [
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', isRequired: true, sampleValue: 'Support Climate Action Bill C-12' },
{ key: 'MESSAGE', label: 'Message Body', isRequired: true, sampleValue: 'I urge you to support...' },
// ... 7 more variables
],
};
const htmlContent = fs.readFileSync(path.join(templatesDir, `${templateDef.key}.html`), 'utf-8');
const textContent = fs.readFileSync(path.join(templatesDir, `${templateDef.key}.txt`), 'utf-8');
const template = await prisma.emailTemplate.create({
data: {
...templateDef,
htmlContent,
textContent,
createdByUserId: admin.id,
variables: {
create: templateDef.variables,
},
},
});
```
#### Response Verification
**Key:** `response-verification`
**Category:** `INFLUENCE`
**Variables:** CAMPAIGN_TITLE, RESPONSE_TYPE, RESPONSE_TEXT, SUBMITTER_NAME, SUBMITTED_DATE, VERIFICATION_URL, REPORT_URL, ORGANIZATION_NAME, TIMESTAMP
#### Shift Signup Confirmation
**Key:** `shift-signup-confirmation`
**Category:** `MAP`
**Variables:** ORGANIZATION_NAME, USER_NAME, USER_EMAIL, SHIFT_TITLE, SHIFT_DATE, SHIFT_TIME, SHIFT_LOCATION, IS_NEW_USER, TEMP_PASSWORD, LOGIN_URL
#### Shift Details Reminder
**Key:** `shift-details`
**Category:** `MAP`
**Variables:** ORGANIZATION_NAME, USER_NAME, SHIFT_TITLE, SHIFT_DATE, SHIFT_START_TIME, SHIFT_END_TIME, SHIFT_LOCATION, SHIFT_DESCRIPTION, CURRENT_VOLUNTEERS, MAX_VOLUNTEERS, SHIFT_STATUS
---
## Seed Script Structure
### Main Function
```typescript
async function main() {
console.log('Seeding database...');
// 1. Create admin user
const admin = await createAdminUser();
// 2. Create map settings
await createMapSettings();
// 3. Create page blocks
await createPageBlocks();
// 4. Seed email templates
await seedEmailTemplates(admin);
console.log('Seed complete.');
}
```
### Upsert Pattern
All seed operations use `upsert` to be idempotent:
```typescript
await prisma.pageBlock.upsert({
where: { id: block.id },
update: {}, // Don't update if exists
create: block, // Create if doesn't exist
});
```
**Benefits:**
- Safe to run multiple times
- Won't duplicate data
- Won't overwrite user changes (empty `update` clause)
### Error Handling
```typescript
main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
```
---
## Customizing Seed Data
### Change Admin Credentials
Edit `api/prisma/seed.ts`:
```typescript
const hashedPassword = await bcrypt.hash('YourSecurePassword123!', 10);
const admin = await prisma.user.upsert({
where: { email: 'your-email@example.com' }, // Change email
update: {
password: hashedPassword,
emailVerified: true,
status: 'ACTIVE',
},
create: {
email: 'your-email@example.com', // Change email
password: hashedPassword,
name: 'Your Name', // Change name
role: UserRole.SUPER_ADMIN,
emailVerified: true,
},
});
```
### Change Map Default Location
Edit `api/prisma/seed.ts`:
```typescript
await prisma.mapSettings.upsert({
where: { id: 'default' },
update: {},
create: {
id: 'default',
latitude: 51.0447, // Calgary, AB
longitude: -114.0719,
zoom: 11,
walkSheetTitle: 'Calgary Canvass Walk Sheet',
walkSheetSubtitle: 'District Canvassing',
walkSheetFooter: 'Thank you for volunteering!',
},
});
```
### Add Custom Page Blocks
```typescript
const customBlocks = [
{
id: 'custom-video',
type: 'video',
label: 'Video Embed',
category: 'Media',
sortOrder: 7,
schema: {
videoUrl: { type: 'string', label: 'Video URL' },
caption: { type: 'string', label: 'Caption' },
},
defaults: {
videoUrl: 'https://www.youtube.com/embed/...',
caption: 'Watch our video',
},
},
];
for (const block of customBlocks) {
await prisma.pageBlock.upsert({
where: { id: block.id },
update: {},
create: block,
});
}
```
---
## Verifying Seed Data
### Check Admin User
```bash
docker compose exec api npx prisma studio
# Navigate to users table, filter by role = "SUPER_ADMIN"
```
### Check Map Settings
```bash
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT * FROM map_settings;"
```
### Check Page Blocks
```bash
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT id, type, label FROM page_blocks ORDER BY sort_order;"
```
### Check Email Templates
```bash
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT key, name, category FROM email_templates;"
```
---
## Troubleshooting
### Error: "Unique constraint failed on email"
**Cause:** Admin user already exists
**Solution:** Seed uses `upsert`, so this shouldn't happen. Check seed script for typos.
### Error: "Template files not found"
**Cause:** Email template `.html`/`.txt` files missing
**Solution:** Ensure `api/src/templates/email/` directory contains:
- `campaign-email.html`
- `campaign-email.txt`
- `response-verification.html`
- `response-verification.txt`
- `shift-signup-confirmation.html`
- `shift-signup-confirmation.txt`
- `shift-details.html`
- `shift-details.txt`
### Error: "Cannot find module 'bcryptjs'"
**Cause:** Dependencies not installed
**Solution:**
```bash
cd api && npm install
```
### Seed doesn't run after migration
**Cause:** `package.json` missing `prisma.seed` config
**Solution:** Add to `api/package.json`:
```json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
```
---
## Production Seeding
### Initial Deployment
```bash
# 1. Deploy migrations
docker compose exec api npx prisma migrate deploy
# 2. Run seed
docker compose exec api npx prisma db seed
# 3. Change admin password immediately
# Login at https://app.cmlite.org with admin@cmlite.org / ChangeMe2025!
# Navigate to /app/profile, update password
```
### Subsequent Deployments
**Don't re-run seed** unless adding new seed data (new page blocks, email templates, etc.). Existing seed data uses `upsert` with empty `update` clause, so it won't overwrite user changes.
---
## Related Documentation
- [Database Overview](./index.md) — Complete ER diagram
- [Schema Reference](./schema.md) — All model fields
- [Migration Workflow](./migrations.md) — Prisma migrations
- [Auth Models](./models/auth.md) — User model details
- [Settings Models](./models/settings.md) — MapSettings details
- [Landing Page Models](./models/pages.md) — PageBlock details
- [Email Template Models](./models/email-templates.md) — EmailTemplate details