837 lines
29 KiB
Markdown
837 lines
29 KiB
Markdown
# Database Documentation
|
|
|
|
## Overview
|
|
|
|
Changemaker Lite V2 uses a **dual ORM architecture** with PostgreSQL 16 as the backing database:
|
|
|
|
- **Prisma ORM** (Express API, port 4000) — 30 models for auth, influence, map, canvassing, email templates, landing pages, and tracking
|
|
- **Drizzle ORM** (Fastify Media API, port 4100) — 3 models for video library, compilations, and job queue
|
|
|
|
Both ORMs share the same PostgreSQL database but maintain separate schemas and migration workflows.
|
|
|
|
### Database Architecture
|
|
|
|
**Database:** PostgreSQL 16
|
|
**Connection:** `DATABASE_URL` environment variable
|
|
**Total Models:** 33 models organized into 9 groups
|
|
**Migration Tools:** Prisma Migrate (main API), Drizzle Kit (media API)
|
|
|
|
### Key Design Patterns
|
|
|
|
1. **Audit Fields** — Most models include:
|
|
- `createdAt` / `updatedAt` timestamps
|
|
- `createdByUserId` / `updatedByUserId` user references
|
|
- Automatic tracking via Prisma middleware
|
|
|
|
2. **Soft Deletes** — Some models use status fields instead of hard deletes:
|
|
- User: `status` (ACTIVE/INACTIVE/SUSPENDED/EXPIRED)
|
|
- Campaign: `status` (DRAFT/ACTIVE/PAUSED/ARCHIVED)
|
|
- Shift: `status` (OPEN/FULL/CANCELLED)
|
|
|
|
3. **JSON Fields** — Used for flexible schema:
|
|
- `permissions` (User) — granular per-app permissions
|
|
- `offices` (Representative) — array of office contact info
|
|
- `tags` (videos) — array of tag strings
|
|
- `geojson` (Cut) — GeoJSON polygon coordinates
|
|
- `blocks` (LandingPage) — GrapesJS editor output
|
|
|
|
4. **Enums** — 18 enums for type safety:
|
|
- UserRole, UserStatus, CampaignStatus, GovernmentLevel, EmailMethod, ResponseType, ResponseStatus, SupportLevel, GeocodeProvider, BuildingType, LocationHistoryAction, ShiftStatus, SignupStatus, SignupSource, CutCategory, VisitOutcome, CanvassSessionStatus, TrackPointEvent, EmailTemplateCategory, EditorMode, MkdocsExportMode
|
|
|
|
5. **Cascade Deletes** — Foreign keys with `onDelete: Cascade`:
|
|
- Deleting a Campaign deletes all CampaignEmail, RepresentativeResponse, CustomRecipient, Call records
|
|
- Deleting a Location deletes all Address and LocationHistory records
|
|
- Deleting a Shift deletes all ShiftSignup records
|
|
- Deleting a CanvassSession deletes all CanvassVisit records
|
|
|
|
6. **Indexes** — Strategic indexing for performance:
|
|
- All foreign keys indexed (userId, campaignId, locationId, etc.)
|
|
- Composite indexes for common queries (latitude+longitude, locationId+unitNumber, etc.)
|
|
- Unique constraints (email, slug, postalCode, token, etc.)
|
|
|
|
## Complete Entity Relationship Diagram
|
|
|
|
```mermaid
|
|
erDiagram
|
|
%% ============================================================================
|
|
%% AUTH & USERS
|
|
%% ============================================================================
|
|
|
|
User ||--o{ RefreshToken : has
|
|
User ||--o{ Campaign : creates
|
|
User ||--o{ CampaignEmail : sends
|
|
User ||--o{ RepresentativeResponse : submits
|
|
User ||--o{ ResponseUpvote : upvotes
|
|
User ||--o{ ShiftSignup : "signs up for"
|
|
User ||--o{ Location : creates
|
|
User ||--o{ Location : updates
|
|
User ||--o{ Address : "creates (addresses)"
|
|
User ||--o{ Address : "updates (addresses)"
|
|
User ||--o{ LocationHistory : edits
|
|
User ||--o{ Cut : "creates (cuts)"
|
|
User ||--o{ CanvassVisit : visits
|
|
User ||--o{ CanvassSession : "has (sessions)"
|
|
User ||--o{ TrackingSession : "tracks (gps)"
|
|
User ||--o{ EmailTemplate : "creates (templates)"
|
|
User ||--o{ EmailTemplate : "updates (templates)"
|
|
User ||--o{ EmailTemplateVersion : "versions (templates)"
|
|
User ||--o{ EmailTemplateTestLog : "tests (templates)"
|
|
|
|
User {
|
|
String id PK
|
|
String email UK "bcrypt hashed"
|
|
String password "bcrypt"
|
|
String name
|
|
String phone
|
|
UserRole role "SUPER_ADMIN | INFLUENCE_ADMIN | MAP_ADMIN | USER | TEMP"
|
|
UserStatus status "ACTIVE | INACTIVE | SUSPENDED | EXPIRED"
|
|
Json permissions "granular per-app"
|
|
UserCreatedVia createdVia "ADMIN | PUBLIC_SHIFT_SIGNUP | STANDARD"
|
|
DateTime expiresAt "for TEMP users"
|
|
Int expireDays
|
|
DateTime lastLoginAt
|
|
Boolean emailVerified
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
RefreshToken {
|
|
String id PK
|
|
String token UK "JWT refresh token"
|
|
String userId FK
|
|
DateTime expiresAt
|
|
DateTime createdAt
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% INFLUENCE — CAMPAIGNS
|
|
%% ============================================================================
|
|
|
|
Campaign ||--o{ CampaignEmail : sends
|
|
Campaign ||--o{ RepresentativeResponse : receives
|
|
Campaign ||--o{ CustomRecipient : targets
|
|
Campaign ||--o{ Call : tracks
|
|
|
|
Campaign {
|
|
String id PK
|
|
String slug UK
|
|
String title
|
|
String description
|
|
String emailSubject
|
|
String emailBody
|
|
String callToAction
|
|
String coverPhoto
|
|
CampaignStatus status "DRAFT | ACTIVE | PAUSED | ARCHIVED"
|
|
Boolean allowSmtpEmail "default: true"
|
|
Boolean allowMailtoLink "default: true"
|
|
Boolean collectUserInfo "default: true"
|
|
Boolean showEmailCount "default: true"
|
|
Boolean showCallCount "default: true"
|
|
Boolean allowEmailEditing "default: false"
|
|
Boolean allowCustomRecipients "default: false"
|
|
Boolean showResponseWall "default: false"
|
|
Boolean highlightCampaign "default: false"
|
|
GovernmentLevel[] targetGovernmentLevels
|
|
String createdByUserId FK
|
|
String createdByUserEmail
|
|
String createdByUserName
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
CampaignEmail {
|
|
String id PK
|
|
String campaignId FK
|
|
String campaignSlug
|
|
String userId FK
|
|
String userEmail
|
|
String userName
|
|
String userPostalCode
|
|
String recipientEmail
|
|
String recipientName
|
|
String recipientTitle
|
|
GovernmentLevel recipientLevel
|
|
EmailMethod emailMethod "SMTP | MAILTO"
|
|
String subject
|
|
String message
|
|
CampaignEmailStatus status "QUEUED | SENT | FAILED | CLICKED | USER_INFO_CAPTURED"
|
|
String senderIp
|
|
DateTime sentAt
|
|
}
|
|
|
|
Representative {
|
|
String id PK
|
|
String postalCode IDX
|
|
String name
|
|
String email
|
|
String districtName
|
|
String electedOffice
|
|
String partyName
|
|
String representativeSetName
|
|
String url
|
|
String photoUrl
|
|
Json offices "array of office contact info"
|
|
DateTime cachedAt
|
|
}
|
|
|
|
CustomRecipient {
|
|
String id PK
|
|
String campaignId FK
|
|
String campaignSlug
|
|
String recipientName
|
|
String recipientEmail
|
|
String recipientTitle
|
|
String recipientOrganization
|
|
String notes
|
|
Boolean isActive
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
PostalCodeCache {
|
|
String id PK
|
|
String postalCode UK
|
|
String city
|
|
String province
|
|
Decimal centroidLat
|
|
Decimal centroidLng
|
|
DateTime lastUpdated
|
|
}
|
|
|
|
Call {
|
|
String id PK
|
|
String representativeName
|
|
String representativeTitle
|
|
String phoneNumber
|
|
String officeType
|
|
String callerName
|
|
String callerEmail
|
|
String postalCode
|
|
String campaignId FK
|
|
String campaignSlug
|
|
String callerIp
|
|
DateTime calledAt
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% INFLUENCE — RESPONSE WALL
|
|
%% ============================================================================
|
|
|
|
RepresentativeResponse ||--o{ ResponseUpvote : gets
|
|
|
|
RepresentativeResponse {
|
|
String id PK
|
|
String campaignId FK
|
|
String campaignSlug
|
|
String representativeName
|
|
String representativeTitle
|
|
GovernmentLevel representativeLevel
|
|
String representativeEmail
|
|
ResponseType responseType "EMAIL | LETTER | PHONE_CALL | MEETING | SOCIAL_MEDIA | OTHER"
|
|
String responseText
|
|
String userComment
|
|
String screenshotUrl
|
|
String submittedByUserId FK
|
|
String submittedByName
|
|
String submittedByEmail
|
|
Boolean isAnonymous
|
|
ResponseStatus status "PENDING | APPROVED | REJECTED"
|
|
Boolean isVerified
|
|
String verificationToken
|
|
DateTime verificationSentAt
|
|
DateTime verifiedAt
|
|
String verifiedBy
|
|
Int upvoteCount
|
|
String submittedIp
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
ResponseUpvote {
|
|
String id PK
|
|
String responseId FK
|
|
String userId FK
|
|
String userEmail
|
|
String upvotedIp
|
|
}
|
|
|
|
EmailLog {
|
|
String id PK
|
|
String recipientEmail
|
|
String senderName
|
|
String senderEmail
|
|
String subject
|
|
String message
|
|
String postalCode
|
|
String status "sent | failed | previewed"
|
|
String senderIp
|
|
DateTime sentAt
|
|
}
|
|
|
|
EmailVerification {
|
|
String id PK
|
|
String token UK
|
|
String email
|
|
String tempCampaignData "JSON"
|
|
DateTime createdAt
|
|
DateTime expiresAt
|
|
Boolean used
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% MAP — LOCATIONS
|
|
%% ============================================================================
|
|
|
|
Location ||--o{ Address : contains
|
|
Location ||--o{ LocationHistory : logs
|
|
|
|
Location {
|
|
String id PK
|
|
Decimal latitude "required, precision: 10,8"
|
|
Decimal longitude "required, precision: 11,8"
|
|
String address "base street address, no unit"
|
|
String postalCode
|
|
String province
|
|
String federalDistrict
|
|
Int buildingUse "NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown"
|
|
String locGuid UK "NAR LOC_GUID"
|
|
BuildingType buildingType "SINGLE_FAMILY | MULTI_UNIT | MIXED_USE | COMMERCIAL"
|
|
Int totalUnits
|
|
String buildingNotes "access codes, manager contact"
|
|
Int geocodeConfidence "0-100"
|
|
GeocodeProvider geocodeProvider
|
|
String createdByUserId FK
|
|
String updatedByUserId FK
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
Address {
|
|
String id PK
|
|
String locationId FK
|
|
String unitNumber
|
|
String addrGuid UK "NAR ADDR_GUID"
|
|
String firstName
|
|
String lastName
|
|
String email
|
|
String phone
|
|
SupportLevel supportLevel "1 | 2 | 3 | 4"
|
|
Boolean sign
|
|
String signSize
|
|
String notes
|
|
String createdByUserId FK
|
|
String updatedByUserId FK
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
LocationHistory {
|
|
String id PK
|
|
String locationId FK
|
|
String userId FK
|
|
LocationHistoryAction action "CREATED | UPDATED | GEOCODED | BULK_GEOCODED | MOVED_ON_MAP | IMPORTED_CSV | IMPORTED_NAR"
|
|
String field "which field changed"
|
|
String oldValue
|
|
String newValue
|
|
Json metadata "provider, confidence, etc"
|
|
DateTime createdAt
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% MAP — SHIFTS & CUTS
|
|
%% ============================================================================
|
|
|
|
Cut ||--o{ Shift : schedules
|
|
Shift ||--o{ ShiftSignup : has
|
|
Shift ||--o{ CanvassVisit : "visits (shift)"
|
|
Shift ||--o{ CanvassSession : "sessions (shift)"
|
|
|
|
Shift {
|
|
String id PK
|
|
String title
|
|
String description
|
|
DateTime date
|
|
String startTime "HH:MM"
|
|
String endTime "HH:MM"
|
|
String location
|
|
Int maxVolunteers
|
|
Int currentVolunteers
|
|
ShiftStatus status "OPEN | FULL | CANCELLED"
|
|
Boolean isPublic
|
|
String cutId FK
|
|
String createdBy
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
ShiftSignup {
|
|
String id PK
|
|
String shiftId FK
|
|
String shiftTitle
|
|
String userId FK
|
|
String userEmail
|
|
String userName
|
|
String userPhone
|
|
DateTime signupDate
|
|
SignupStatus status "CONFIRMED | CANCELLED"
|
|
SignupSource signupSource "AUTHENTICATED | PUBLIC | ADMIN"
|
|
}
|
|
|
|
Cut {
|
|
String id PK
|
|
String name
|
|
String description
|
|
String color
|
|
Decimal opacity
|
|
CutCategory category "CUSTOM | WARD | NEIGHBORHOOD | DISTRICT"
|
|
Boolean isPublic
|
|
Boolean isOfficial
|
|
String geojson "GeoJSON polygon data"
|
|
String bounds "bounding box JSON"
|
|
Boolean showLocations
|
|
Boolean exportEnabled
|
|
String assignedTo
|
|
Json filterSettings
|
|
DateTime lastCanvassed
|
|
Int completionPercentage
|
|
String createdByUserId FK
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
MapSettings {
|
|
String id PK
|
|
Decimal latitude
|
|
Decimal longitude
|
|
Int zoom
|
|
String walkSheetTitle
|
|
String walkSheetSubtitle
|
|
String walkSheetFooter
|
|
String qrCode1Url
|
|
String qrCode1Label
|
|
String qrCode2Url
|
|
String qrCode2Label
|
|
String qrCode3Url
|
|
String qrCode3Label
|
|
String createdBy
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% CANVASSING
|
|
%% ============================================================================
|
|
|
|
Cut ||--o{ CanvassSession : "sessions (cut)"
|
|
CanvassSession ||--o{ CanvassVisit : records
|
|
CanvassSession ||--|| TrackingSession : tracks
|
|
Address ||--o{ CanvassVisit : "visited (address)"
|
|
|
|
CanvassSession {
|
|
String id PK
|
|
String userId FK
|
|
String cutId FK
|
|
String shiftId FK
|
|
CanvassSessionStatus status "ACTIVE | COMPLETED | ABANDONED"
|
|
DateTime startedAt
|
|
DateTime endedAt
|
|
Decimal startLatitude
|
|
Decimal startLongitude
|
|
}
|
|
|
|
CanvassVisit {
|
|
String id PK
|
|
String addressId FK
|
|
String userId FK
|
|
String shiftId FK
|
|
String sessionId FK
|
|
VisitOutcome outcome "NOT_HOME | REFUSED | MOVED | ALREADY_VOTED | SPOKE_WITH | LEFT_LITERATURE | COME_BACK_LATER"
|
|
SupportLevel supportLevel
|
|
Boolean signRequested
|
|
String signSize
|
|
String notes
|
|
Int durationSeconds
|
|
DateTime visitedAt
|
|
}
|
|
|
|
TrackingSession {
|
|
String id PK
|
|
String userId FK
|
|
String canvassSessionId UK
|
|
DateTime startedAt
|
|
DateTime endedAt
|
|
Boolean isActive
|
|
Int totalPoints
|
|
Float totalDistanceM
|
|
Decimal lastLatitude
|
|
Decimal lastLongitude
|
|
DateTime lastRecordedAt
|
|
}
|
|
|
|
TrackingSession ||--o{ TrackPoint : logs
|
|
|
|
TrackPoint {
|
|
String id PK
|
|
String trackingSessionId FK
|
|
Decimal latitude
|
|
Decimal longitude
|
|
Float accuracy
|
|
DateTime recordedAt
|
|
TrackPointEvent eventType "LOCATION_ADDED | VISIT_RECORDED | SESSION_STARTED | SESSION_ENDED"
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% EMAIL TEMPLATES
|
|
%% ============================================================================
|
|
|
|
EmailTemplate ||--o{ EmailTemplateVariable : defines
|
|
EmailTemplate ||--o{ EmailTemplateVersion : versions
|
|
EmailTemplate ||--o{ EmailTemplateTestLog : tests
|
|
|
|
EmailTemplate {
|
|
String id PK
|
|
String key UK "e.g., campaign-email"
|
|
String name "display name"
|
|
String description
|
|
EmailTemplateCategory category "INFLUENCE | MAP | SYSTEM"
|
|
String subjectLine "with {{VAR}} support"
|
|
String htmlContent
|
|
String textContent
|
|
Boolean isSystem "prevent deletion"
|
|
Boolean isActive
|
|
String createdByUserId FK
|
|
String updatedByUserId FK
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
EmailTemplateVariable {
|
|
String id PK
|
|
String templateId FK
|
|
String key "e.g., USER_NAME"
|
|
String label "e.g., User Name"
|
|
String description
|
|
Boolean isRequired
|
|
Boolean isConditional "used in {{#if}} blocks"
|
|
String sampleValue
|
|
Int sortOrder
|
|
}
|
|
|
|
EmailTemplateVersion {
|
|
String id PK
|
|
String templateId FK
|
|
Int versionNumber "auto-increment per template"
|
|
String subjectLine
|
|
String htmlContent
|
|
String textContent
|
|
String changeNotes
|
|
String createdByUserId FK
|
|
DateTime createdAt
|
|
}
|
|
|
|
EmailTemplateTestLog {
|
|
String id PK
|
|
String templateId FK
|
|
String recipientEmail
|
|
Json testData "sample variable values"
|
|
Boolean success
|
|
String errorMessage
|
|
String messageId "nodemailer message ID"
|
|
String sentByUserId FK
|
|
DateTime sentAt
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% LANDING PAGES
|
|
%% ============================================================================
|
|
|
|
LandingPage {
|
|
String id PK
|
|
String slug UK
|
|
String title
|
|
String description
|
|
Json blocks "GrapesJS editor JSON"
|
|
String htmlOutput
|
|
String cssOutput
|
|
EditorMode editorMode "VISUAL | CODE"
|
|
String mkdocsPath "path in mkdocs/overrides/"
|
|
String mkdocsStubPath "path to .md stub"
|
|
MkdocsExportMode mkdocsExportMode "THEMED | STANDALONE"
|
|
Boolean mkdocsHideNav
|
|
Boolean mkdocsHideToc
|
|
Boolean mkdocsSkipExport
|
|
Boolean published
|
|
String seoTitle
|
|
String seoDescription
|
|
String seoImage
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
PageBlock {
|
|
String id PK
|
|
String type "hero | text | image | cta | features | testimonials | form"
|
|
String label
|
|
Json schema "block configuration schema"
|
|
Json defaults "default values"
|
|
String thumbnail
|
|
String category
|
|
Int sortOrder
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
|
|
%% ============================================================================
|
|
%% SITE SETTINGS
|
|
%% ============================================================================
|
|
|
|
SiteSettings {
|
|
String id PK
|
|
String organizationName
|
|
String organizationShortName
|
|
String organizationLogoUrl
|
|
String organizationFaviconUrl
|
|
String adminColorPrimary
|
|
String adminColorBgBase
|
|
String publicColorPrimary
|
|
String publicColorBgBase
|
|
String publicColorBgContainer
|
|
String publicHeaderGradient
|
|
String footerText
|
|
String loginSubtitle
|
|
String emailFromName
|
|
String smtpHost
|
|
Int smtpPort
|
|
String smtpUser
|
|
String smtpPass
|
|
String smtpFromAddress
|
|
String smtpActiveProvider "mailhog | production"
|
|
Boolean emailTestMode
|
|
String testEmailRecipient
|
|
Boolean enableInfluence
|
|
Boolean enableMap
|
|
Boolean enableNewsletter
|
|
Boolean enableLandingPages
|
|
DateTime createdAt
|
|
DateTime updatedAt
|
|
}
|
|
```
|
|
|
|
## Model Groups
|
|
|
|
The database is organized into 9 logical groups:
|
|
|
|
### 1. [Auth & Users](./models/auth.md)
|
|
- **User** — User accounts with roles (SUPER_ADMIN, INFLUENCE_ADMIN, MAP_ADMIN, USER, TEMP)
|
|
- **RefreshToken** — JWT refresh token storage with rotation
|
|
|
|
**Key Features:** bcrypt passwords (12+ chars policy), role-based access control, temp user expiration, email verification
|
|
|
|
### 2. [Influence](./models/influence.md)
|
|
- **Campaign** — Advocacy campaigns with 12 feature flags
|
|
- **Representative** — Cached representative data from Represent API
|
|
- **CampaignEmail** — Email tracking (SMTP vs MAILTO)
|
|
- **RepresentativeResponse** — Response wall with moderation
|
|
- **ResponseUpvote** — Upvote tracking with IP + user uniqueness
|
|
- **CustomRecipient** — Custom email targets
|
|
- **PostalCodeCache** — Postal code geocoding cache
|
|
- **EmailLog** — Email audit trail
|
|
- **EmailVerification** — Verification token storage
|
|
- **Call** — Phone call tracking
|
|
|
|
**Key Features:** Multi-government-level targeting, response moderation workflow (PENDING → APPROVED/REJECTED), BullMQ integration for email queue, upvote deduplication
|
|
|
|
### 3. [Map — Locations](./models/map.md)
|
|
- **Location** — Building-level data with lat/lng, NAR integration
|
|
- **Address** — Unit-level data with support levels
|
|
- **LocationHistory** — Audit trail with 7 action types
|
|
- **Shift** — Volunteer shifts with cut relation
|
|
- **ShiftSignup** — Signup tracking
|
|
- **Cut** — GeoJSON polygon overlays
|
|
- **MapSettings** — Singleton for map center/zoom + walk sheet config
|
|
|
|
**Key Features:** Building vs unit architecture, multi-provider geocoding (6 providers), NAR 2025 import support, spatial indexing, GeoJSON storage
|
|
|
|
### 4. [Canvassing](./models/canvass.md)
|
|
- **CanvassSession** — Session lifecycle (ACTIVE → COMPLETED/ABANDONED)
|
|
- **CanvassVisit** — Visit recording with 7 outcome types
|
|
- **TrackingSession** — GPS tracking integration
|
|
- **TrackPoint** — GPS breadcrumb trail
|
|
|
|
**Key Features:** Walking route algorithm, session abandonment logic (12h timeout), distance calculation, support level tracking
|
|
|
|
### 5. [Email Templates](./models/email-templates.md)
|
|
- **EmailTemplate** — Template master with categories
|
|
- **EmailTemplateVariable** — Variable definitions with validation
|
|
- **EmailTemplateVersion** — Version history
|
|
- **EmailTemplateTestLog** — Test email audit
|
|
|
|
**Key Features:** Handlebars-style variable interpolation ({{VAR}}), conditional variables, system template protection, version auto-increment
|
|
|
|
### 6. [Landing Pages](./models/pages.md)
|
|
- **LandingPage** — GrapesJS editor output with MkDocs export
|
|
- **PageBlock** — Reusable block library
|
|
|
|
**Key Features:** GrapesJS JSON storage, MkDocs export modes (THEMED vs STANDALONE), SEO metadata, slug-based routing
|
|
|
|
### 7. [Settings](./models/settings.md)
|
|
- **SiteSettings** — Org branding + theme + SMTP + feature toggles
|
|
- **MapSettings** — Map center/zoom + walk sheet config
|
|
|
|
**Key Features:** Singleton pattern, SMTP override hierarchy (SiteSettings → .env), feature flags
|
|
|
|
### 8. [Media (Drizzle ORM)](./models/media.md)
|
|
- **videos** — Video library with metadata, directory types, engagement stats
|
|
- **compilations** — Video compilation tracking
|
|
- **jobs** — Job queue with resource categories
|
|
|
|
**Key Features:** Dual ORM architecture, FFprobe metadata extraction, directory type enum (9 types), job queue with GPU/CPU resource tracking
|
|
|
|
### 9. [Shared/Standalone Models](#)
|
|
- **Representative** — Shared across campaigns
|
|
- **PostalCodeCache** — Shared geocoding cache
|
|
- **EmailLog** — Audit trail (no relations)
|
|
- **EmailVerification** — Standalone verification tokens
|
|
|
|
## Field Types Reference
|
|
|
|
| Prisma Type | PostgreSQL Type | Description | Example |
|
|
|-------------|----------------|-------------|---------|
|
|
| `String` | `text` | Variable-length text | `"admin@cmlite.org"` |
|
|
| `String @db.Text` | `text` | Long-form text (no char limit) | Campaign descriptions |
|
|
| `Int` | `integer` | 32-bit integer | `42` |
|
|
| `BigInt` | `bigint` | 64-bit integer (Node: `number` mode) | File sizes |
|
|
| `Boolean` | `boolean` | True/false | `true` |
|
|
| `Decimal` | `numeric` | Arbitrary precision decimal | Lat/lng coordinates |
|
|
| `Decimal @db.Decimal(10, 8)` | `numeric(10, 8)` | 10 digits, 8 after decimal | `53.54612345` |
|
|
| `DateTime` | `timestamp with time zone` | Timestamp | `2025-02-11T10:30:00Z` |
|
|
| `DateTime @db.Date` | `date` | Date only (no time) | Shift dates |
|
|
| `Json` | `jsonb` | JSON data (binary storage) | Arrays, objects |
|
|
| `Enum` | `enum` | Enumerated type | `UserRole.SUPER_ADMIN` |
|
|
|
|
## Enum Definitions
|
|
|
|
### Auth & Users
|
|
- **UserRole:** `SUPER_ADMIN`, `INFLUENCE_ADMIN`, `MAP_ADMIN`, `USER`, `TEMP`
|
|
- **UserStatus:** `ACTIVE`, `INACTIVE`, `SUSPENDED`, `EXPIRED`
|
|
- **UserCreatedVia:** `ADMIN`, `PUBLIC_SHIFT_SIGNUP`, `STANDARD`
|
|
|
|
### Influence
|
|
- **CampaignStatus:** `DRAFT`, `ACTIVE`, `PAUSED`, `ARCHIVED`
|
|
- **GovernmentLevel:** `FEDERAL`, `PROVINCIAL`, `MUNICIPAL`, `SCHOOL_BOARD`
|
|
- **EmailMethod:** `SMTP`, `MAILTO`
|
|
- **CampaignEmailStatus:** `QUEUED`, `SENT`, `FAILED`, `CLICKED`, `USER_INFO_CAPTURED`
|
|
- **ResponseType:** `EMAIL`, `LETTER`, `PHONE_CALL`, `MEETING`, `SOCIAL_MEDIA`, `OTHER`
|
|
- **ResponseStatus:** `PENDING`, `APPROVED`, `REJECTED`
|
|
|
|
### Map
|
|
- **SupportLevel:** `LEVEL_1` (mapped to `"1"`), `LEVEL_2`, `LEVEL_3`, `LEVEL_4`
|
|
- **GeocodeProvider:** `GOOGLE`, `MAPBOX`, `NOMINATIM`, `PHOTON`, `LOCATIONIQ`, `ARCGIS`, `UNKNOWN`
|
|
- **BuildingType:** `SINGLE_FAMILY`, `MULTI_UNIT`, `MIXED_USE`, `COMMERCIAL`
|
|
- **LocationHistoryAction:** `CREATED`, `UPDATED`, `GEOCODED`, `BULK_GEOCODED`, `MOVED_ON_MAP`, `IMPORTED_CSV`, `IMPORTED_NAR`
|
|
- **ShiftStatus:** `OPEN`, `FULL`, `CANCELLED`
|
|
- **SignupStatus:** `CONFIRMED`, `CANCELLED`
|
|
- **SignupSource:** `AUTHENTICATED`, `PUBLIC`, `ADMIN`
|
|
- **CutCategory:** `CUSTOM`, `WARD`, `NEIGHBORHOOD`, `DISTRICT`
|
|
|
|
### Canvassing
|
|
- **VisitOutcome:** `NOT_HOME`, `REFUSED`, `MOVED`, `ALREADY_VOTED`, `SPOKE_WITH`, `LEFT_LITERATURE`, `COME_BACK_LATER`
|
|
- **CanvassSessionStatus:** `ACTIVE`, `COMPLETED`, `ABANDONED`
|
|
- **TrackPointEvent:** `LOCATION_ADDED`, `VISIT_RECORDED`, `SESSION_STARTED`, `SESSION_ENDED`
|
|
|
|
### Email Templates
|
|
- **EmailTemplateCategory:** `INFLUENCE`, `MAP`, `SYSTEM`
|
|
|
|
### Landing Pages
|
|
- **EditorMode:** `VISUAL`, `CODE`
|
|
- **MkdocsExportMode:** `THEMED`, `STANDALONE`
|
|
|
|
### Media (Drizzle)
|
|
- **DirectoryType** (TypeScript literal): `'studios'`, `'gifs'`, `'private'`, `'inbox'`, `'curated'`, `'playback'`, `'compilations'`, `'videos'`, `'highlights'`
|
|
- **ResourceCategory** (TypeScript literal): `'gpu_ai'`, `'gpu_encode'`, `'cpu'`
|
|
- **JobStatus** (TypeScript literal): `'pending'`, `'queued'`, `'running'`, `'completed'`, `'failed'`, `'cancelled'`
|
|
|
|
## Index Strategy Overview
|
|
|
|
### Foreign Key Indexes
|
|
All foreign key fields are indexed for join performance:
|
|
- `userId`, `campaignId`, `locationId`, `addressId`, `shiftId`, `cutId`, `sessionId`, `templateId`, `trackingSessionId`
|
|
|
|
### Composite Indexes
|
|
Strategic multi-column indexes for common query patterns:
|
|
- `[latitude, longitude]` (Location) — spatial queries
|
|
- `[locationId, unitNumber]` (Address) — unit lookups
|
|
- `[campaignId, status]` (RepresentativeResponse) — filtered response lists
|
|
- `[isActive, lastRecordedAt]` (TrackingSession) — active session cleanup
|
|
- `[templateId, createdAt(sort: Desc)]` (EmailTemplateVersion) — version history
|
|
- `[directoryType, isValid, orientation]` (videos) — media library filtering
|
|
|
|
### Unique Constraints
|
|
Enforce data integrity:
|
|
- `email` (User)
|
|
- `slug` (Campaign, LandingPage)
|
|
- `postalCode` (PostalCodeCache)
|
|
- `token` (RefreshToken, EmailVerification)
|
|
- `key` (EmailTemplate)
|
|
- `[responseId, userId]` (ResponseUpvote) — prevent duplicate upvotes from logged-in users
|
|
- `[responseId, upvotedIp]` (ResponseUpvote) — prevent duplicate upvotes from same IP
|
|
- `[shiftId, userEmail]` (ShiftSignup) — prevent duplicate shift signups
|
|
- `[templateId, key]` (EmailTemplateVariable) — unique variable keys per template
|
|
- `[templateId, versionNumber]` (EmailTemplateVersion) — sequential version numbers
|
|
|
|
## Foreign Key Conventions
|
|
|
|
### Cascade Deletes
|
|
```prisma
|
|
onDelete: Cascade
|
|
```
|
|
Used when child records should be deleted with parent:
|
|
- RefreshToken → User
|
|
- CampaignEmail → Campaign
|
|
- RepresentativeResponse → Campaign
|
|
- CustomRecipient → Campaign
|
|
- Call → Campaign (SetNull)
|
|
- Address → Location
|
|
- LocationHistory → Location
|
|
- ShiftSignup → Shift
|
|
- CanvassVisit → Address, CanvassSession
|
|
- TrackPoint → TrackingSession
|
|
- EmailTemplateVariable → EmailTemplate
|
|
- EmailTemplateVersion → EmailTemplate
|
|
- EmailTemplateTestLog → EmailTemplate
|
|
|
|
### Set Null
|
|
```prisma
|
|
onDelete: SetNull
|
|
```
|
|
Used when child records should remain but orphan the reference:
|
|
- Campaign.createdByUserId → User
|
|
- CampaignEmail.userId → User
|
|
- RepresentativeResponse.submittedByUserId → User
|
|
- Location.createdByUserId/updatedByUserId → User
|
|
- Shift.cutId → Cut
|
|
- CanvassSession.shiftId → Shift
|
|
- TrackingSession.canvassSessionId → CanvassSession
|
|
|
|
## Related Documentation
|
|
|
|
- [Schema Reference](./schema.md) — Complete table and field listing
|
|
- [Migration Workflow](./migrations.md) — Prisma and Drizzle migration processes
|
|
- [Seeding](./seeding.md) — Default data and seed script
|
|
- [Indexes](./indexes.md) — Detailed index strategy and performance notes
|
|
- [Auth Models](./models/auth.md) — User and authentication tables
|
|
- [Influence Models](./models/influence.md) — Campaign and advocacy tables
|
|
- [Map Models](./models/map.md) — Location, shift, and cut tables
|
|
- [Canvassing Models](./models/canvass.md) — Session and visit tracking
|
|
- [Email Template Models](./models/email-templates.md) — Template system
|
|
- [Landing Page Models](./models/pages.md) — Page builder and blocks
|
|
- [Settings Models](./models/settings.md) — Site and map settings
|
|
- [Media Models](./models/media.md) — Video library (Drizzle ORM)
|
|
|
|
## Quick Links
|
|
|
|
- [Prisma Schema File](https://github.com/changemaker-lite/api/blob/v2/prisma/schema.prisma)
|
|
- [Drizzle Schema File](https://github.com/changemaker-lite/api/blob/v2/src/modules/media/db/schema.ts)
|
|
- [API Documentation](../api/index.md)
|
|
- [Admin GUI Documentation](../admin/index.md)
|