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 update clause)

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.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:

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.