485 lines
11 KiB
Markdown
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
|