11 KiB
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
cd api
npm run seed
# OR
npx prisma db seed
Production (Docker)
docker compose exec api npx prisma db seed
CI/CD
Seed runs automatically after prisma migrate deploy if configured in package.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:
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:
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
{
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
{
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
{
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
{
id: 'default-cta',
type: 'cta',
label: 'Call to Action',
category: 'Actions',
sortOrder: 4,
// ... (see seed.ts for full schema)
}
Testimonials
{
id: 'default-testimonials',
type: 'testimonials',
label: 'Testimonials',
category: 'Content',
sortOrder: 5,
// ... (see seed.ts for full schema)
}
Contact Form
{
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:
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
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:
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
updateclause)
Error Handling
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:
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:
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
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
docker compose exec api npx prisma studio
# Navigate to users table, filter by role = "SUPER_ADMIN"
Check Map Settings
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT * FROM map_settings;"
Check Page Blocks
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
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.htmlcampaign-email.txtresponse-verification.htmlresponse-verification.txtshift-signup-confirmation.htmlshift-signup-confirmation.txtshift-details.htmlshift-details.txt
Error: "Cannot find module 'bcryptjs'"
Cause: Dependencies not installed Solution:
cd api && npm install
Seed doesn't run after migration
Cause: package.json missing prisma.seed config
Solution: Add to api/package.json:
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
Production Seeding
Initial Deployment
# 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 — Complete ER diagram
- Schema Reference — All model fields
- Migration Workflow — Prisma migrations
- Auth Models — User model details
- Settings Models — MapSettings details
- Landing Page Models — PageBlock details
- Email Template Models — EmailTemplate details