39948-vm/backend/docs/modules/db-seeders.md
2026-07-03 16:11:24 +02:00

576 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DB Seeders Module
## Overview
The DB Seeders module provides initial data population for the database using
the typed **Umzug v3** runner in `backend/src/db/umzug.ts`. Seeders create
essential system data (users, roles, permissions) and optional sample data for
development and testing.
**Location:** `backend/src/db/seeders/`
**Files:** 3 TypeScript ESM seeder entries. There are no JavaScript seeder
bridges after the backend ESM migration.
---
## Architecture
```
backend/src/db/seeders/
├── 20200430130759-admin-user.ts # Initial admin users
├── 20200430130760-user-roles.ts # RBAC roles + permissions
└── 20231127130745-sample-data.ts # Sample data, opt-in
```
**Execution Order:** Seeders run in timestamp order (oldest first).
---
## Configuration
### NPM Scripts
```bash
# Run pending seeders
npm run db:seed
# Undo all seeders
npm run db:seed:undo
# Full database reset (includes seeding)
npm run db:reset
```
### Server Startup
Seeders run automatically via `npm start`:
```json
{
"scripts": {
"start": "npm run db:migrate && npm run db:seed && npm run watch"
}
}
```
---
## Seeder Files
### 1. Admin User Seeder (`20200430130759-admin-user.ts`)
**Purpose:** Create initial system users.
The implementation uses reusable contracts from
`backend/src/types/db-seeders.ts`. Umzug executes the TypeScript source in
development and the compiled JavaScript file from `dist/` in production builds.
**Users Created:**
| User | Email | Role Assignment |
|------|-------|-----------------|
| Admin | `config.admin_email` | Administrator |
| John | john@doe.com | Account Manager |
| Client | client@hello.com | Platform Owner |
**Key Features:**
- Uses bcrypt for password hashing with configured salt rounds
- Hardcoded UUIDs for consistent user IDs
- Reads credentials from `config.ts` (environment variables)
- Idempotent on repeated `db:seed`: existing seed user IDs are selected first,
and only missing rows are inserted. This prevents duplicate-key failures when
`SequelizeData` lacks the seeder entry but users already exist.
```typescript
const existingRows =
await queryInterface.sequelize.query<AdminUserSeedExistingIdRow>(
'SELECT "id" FROM "users" WHERE "id" IN (:ids)',
{
replacements: { ids: seedUserIds },
type: QueryTypes.SELECT,
},
);
const existingIds = new Set(existingRows.map((row) => row.id));
const rowsToInsert = createAdminUserRows().filter(
(row) => !existingIds.has(row.id),
);
if (rowsToInsert.length > 0) {
await queryInterface.bulkInsert('users', rowsToInsert);
}
```
---
### 2. User Roles Seeder (`20200430130760-user-roles.ts`)
**Purpose:** Create the complete RBAC (Role-Based Access Control) system.
The implementation uses reusable contracts from
`backend/src/types/db-seeders.ts`. Umzug stores stable seeder execution names
in `SequelizeData` so repeated startup seeding does not rerun completed seed
data.
**Data Created:**
| Data Type | Count | Description |
|-----------|-------|-------------|
| Roles | 7 | User role definitions |
| Permissions | 54 | CRUD permissions for 13 entities + special |
| Role-Permission Links | 200+ | M:N relationships |
| Join Table | 1 | `rolesPermissionsPermissions` table |
**Key Features:**
- Uses stable named role and permission definitions, then reuses existing DB IDs
when the same role/permission name is already present.
- Inserts only missing roles, permissions, and role-permission links. This keeps
repeated `db:seed` safe.
- Keeps the existing startup behavior that assigns system users to seeded roles
by email after RBAC data exists.
#### Roles
```javascript
const roles = [
'Administrator', // Full system access
'PlatformOwner', // Full project/content access
'AccountManager', // User and project management
'TourDesigner', // Content creation and editing
'ContentReviewer', // Read + limited update access
'AnalyticsViewer', // Read-only access
'Public', // Public/unauthenticated access
];
```
#### Permission Generation Pattern
```javascript
// Generates CREATE, READ, UPDATE, DELETE permissions per entity
function createPermissions(name) {
return [
{ name: `CREATE_${name.toUpperCase()}` },
{ name: `READ_${name.toUpperCase()}` },
{ name: `UPDATE_${name.toUpperCase()}` },
{ name: `DELETE_${name.toUpperCase()}` },
];
}
const entities = [
'users', 'roles', 'permissions', 'projects', 'project_memberships',
'assets', 'asset_variants', 'presigned_url_requests', 'tour_pages',
'project_audio_tracks', 'publish_events', 'pwa_caches', 'access_logs'
];
// Creates 52 permissions (13 entities × 4 CRUD operations)
await queryInterface.bulkInsert('permissions', entities.flatMap(createPermissions));
// Plus special permissions
await queryInterface.bulkInsert('permissions', [
{ name: 'READ_API_DOCS' },
{ name: 'CREATE_SEARCH' },
]);
```
#### ID Map Pattern
```javascript
// Consistent UUID generation using key-based map
const idMap = new Map();
function getId(key) {
if (idMap.has(key)) {
return idMap.get(key);
}
const id = uuid();
idMap.set(key, id);
return id;
}
// Usage - same key always returns same UUID within seeder run
getId('Administrator') // Returns consistent UUID
getId('CREATE_USERS') // Returns consistent UUID
```
#### Permission Matrix
| Role | Users | Projects | Assets | Tour Pages | Access Logs |
|------|-------|----------|--------|------------|-------------|
| **Administrator** | CRUD | CRUD | CRUD | CRUD | CRUD |
| **PlatformOwner** | CRUD | CRUD | CRUD | CRUD | CRUD |
| **AccountManager** | RU | CRU | CRU | CRU | R |
| **TourDesigner** | R | RU | CRU | CRU | R |
| **ContentReviewer** | R | RU | RU | RU | R |
| **AnalyticsViewer** | R | R | R | R | R |
| **Public** | - | - | - | - | - |
**Legend:** C=Create, R=Read, U=Update, D=Delete
#### Join Table Creation
```javascript
// Creates M:N relationship table directly in seeder
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS "rolesPermissionsPermissions" (
"createdAt" timestamp with time zone not null,
"updatedAt" timestamp with time zone not null,
"roles_permissionsId" uuid not null,
"permissionId" uuid not null,
primary key ("roles_permissionsId", "permissionId"),
constraint "rolesPermissionsPermissions_roles_permissions_fk"
foreign key ("roles_permissionsId") references "roles"("id")
on delete cascade on update cascade,
constraint "rolesPermissionsPermissions_permission_fk"
foreign key ("permissionId") references "permissions"("id")
on delete cascade on update cascade
);
`);
```
---
### 3. Sample Data Seeder (`20231127130745-sample-data.ts`)
**Purpose:** Create sample data for development and testing.
The implementation lives in `20231127130745-sample-data.ts`. Demo-only
Sequelize operations use reusable `SampleDataModel` and
`SampleDataAssociationRecord` contracts from
`backend/src/types/db-seeders.ts`; the production service model overloads stay
separate. Umzug records the seeder under its legacy `.js` name for storage
compatibility only.
**Opt-In Activation:**
```bash
# Enable sample data seeding
export ENABLE_SAMPLE_DATA=true
npm run db:seed
```
**Check in Code:**
```typescript
const sampleDataSeeder: SequelizeSeeder = {
async up() {
if (process.env.ENABLE_SAMPLE_DATA !== 'true') return;
// ... seed data
},
};
```
**Data Created:**
| Entity | Records | Description |
|--------|---------|-------------|
| Projects | 3 | Sample tour projects |
| Project Memberships | 3 | User-project associations |
| Assets | 3 | Images, videos, audio |
| Asset Variants | 3 | Thumbnail/preview variants |
| Presigned URL Requests | 3 | Upload/download requests |
| Tour Pages | 3 | Sample tour pages |
| Project Audio Tracks | 3 | Background audio |
| Publish Events | 3 | Deployment history |
| PWA Caches | 3 | Offline cache configs |
| Access Logs | 3 | Visitor tracking |
#### Sample Projects
```javascript
const ProjectsData = [
{
name: 'Cardiff Arena Tour',
slug: 'cardiff-arena',
description: 'Interactive arena tour for visitors and event planners.',
logo_url: 'https://cdn.platform.com/cardiff/logo.png',
favicon_url: 'https://cdn.platform.com/cardiff/favicon.ico',
og_image_url: 'https://cdn.platform.com/cardiff/og.jpg',
},
{
name: 'Riverside Park Walkthrough',
slug: 'riverside-park',
description: 'Offline-ready guided walkthrough for the city park.',
// ...
},
{
name: 'Mall Central Experience',
slug: 'mall-central',
description: 'Retail complex presentation with navigation and galleries.',
// ...
},
];
```
#### Association Helper Pattern
```javascript
// Associates records after bulk creation using Sequelize model methods
async function associateAssetWithProject() {
const relatedProject = await Projects.findOne({
offset: Math.floor(Math.random() * (await Projects.count())),
});
const asset = await Assets.findOne({
order: [['id', 'ASC']],
offset: 0,
});
if (asset?.setProject) {
await asset.setProject(relatedProject);
}
}
```
---
## Seeder Patterns
### 1. bulkInsert Pattern
**Purpose:** Insert multiple records efficiently.
```javascript
await queryInterface.bulkInsert('tableName', [
{
id: uuid(),
field1: 'value1',
createdAt: new Date(),
updatedAt: new Date(),
},
// ... more records
]);
```
### 2. bulkDelete Pattern
**Purpose:** Remove seeded data during rollback.
```javascript
async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete('users', {
id: { [Sequelize.Op.in]: ids }
});
}
```
### 3. Conditional Execution Pattern
**Purpose:** Enable/disable seeders based on environment.
Seeder-only environment gates are intentionally allowed to read `process.env`
directly. Runtime app configuration must go through `backend/src/config.ts`,
but seeders and scripts are bootstrap/data-loading entrypoints and are listed
as exceptions in `AGENTS.md`.
The resilience/config hardening work does not require seeder changes because it
does not introduce database schema, RBAC permissions, seed users, roles, default
records, or sample-data entities.
```javascript
up: async () => {
if (process.env.ENABLE_SAMPLE_DATA !== 'true') {
return; // Skip seeding
}
// ... proceed with seeding
}
```
### 4. ID Consistency Pattern
**Purpose:** Use deterministic IDs for reliable down() migrations.
```javascript
// Hardcoded IDs for rollback capability
const ids = [
'193bf4b5-9f07-4bd5-9a43-e7e41f3e96af',
'af5a87be-8f9c-4630-902a-37a60b7005ba',
];
// OR: Map-based consistent generation
const idMap = new Map();
function getId(key) {
if (!idMap.has(key)) {
idMap.set(key, uuid());
}
return idMap.get(key);
}
```
### 5. Model-Based Association Pattern
**Purpose:** Create relationships using Sequelize models after bulk insert.
```javascript
// Use model methods for associations (more readable)
const project = await Projects.findOne({ where: { slug: 'test' } });
const asset = await Assets.findOne({ order: [['id', 'ASC']] });
await asset.setProject(project);
```
### 6. Raw SQL Pattern
**Purpose:** Create structures not managed by Sequelize models.
```javascript
// Create join table directly via SQL
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS "rolesPermissionsPermissions" (
...
);
`);
// Create indexes
await queryInterface.sequelize.query(
'CREATE INDEX IF NOT EXISTS "index_name" ON "tableName" ("columnName");'
);
```
---
## Execution Flow
```
npm run db:seed
┌────────────────────────────────────┐
│ 20200430130759-admin-user.ts │
│ ├─ Typed ESM source │
│ ├─ Hash passwords │
│ └─ Insert missing seed users │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 20200430130760-user-roles.ts │
│ ├─ Typed ESM source │
│ ├─ Insert missing roles │
│ ├─ Insert missing permissions │
│ ├─ Create join table │
│ ├─ Insert missing RBAC links │
│ └─ Update user app_roleId │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 20231127130745-sample-data.ts │
│ ├─ Check ENABLE_SAMPLE_DATA │
│ ├─ Skip if not enabled │
│ ├─ Insert sample records │
│ └─ Create associations │
└────────────────────────────────────┘
```
---
## Best Practices
### 1. Use Consistent IDs
```javascript
// Good - allows rollback
const ids = ['uuid-1', 'uuid-2'];
await queryInterface.bulkInsert('table', records.map((r, i) => ({ id: ids[i], ...r })));
// Down migration can target specific IDs
await queryInterface.bulkDelete('table', { id: { [Op.in]: ids } });
```
### 2. Always Include Timestamps
```javascript
{
field: 'value',
createdAt: new Date(),
updatedAt: new Date(),
}
```
### 3. Handle Errors
```javascript
try {
await queryInterface.bulkInsert('users', [...]);
} catch (error) {
console.error('Error during bulkInsert:', error);
throw error;
}
```
### 4. Environment-Aware Seeding
```javascript
// Production - only essential data
// Development - include sample data
if (process.env.ENABLE_SAMPLE_DATA !== 'true') {
return;
}
```
### 5. Idempotent Where Possible
```javascript
// Use IF NOT EXISTS for table/index creation
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS "tableName" (...)
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS "indexName" ON "tableName" (...)
`);
```
---
## Data Dependencies
```
admin-user.ts
│ Creates missing users with stable IDs
user-roles.ts
│ Creates/reuses roles and permissions, then references user emails to assign roles
│ UPDATE users SET app_roleId = '...' WHERE email = '...'
sample-data.ts
│ Uses Sequelize models to find existing users/projects
│ Creates associations via model methods
```
---
## Running Seeders
### Development Setup
```bash
cd backend
npm run db:seed
```
### With Sample Data
```bash
export ENABLE_SAMPLE_DATA=true
npm run db:seed
```
### Fresh Database
```bash
npm run db:reset # drop, create, migrate, seed
```
### Undo Seeders
```bash
npm run db:seed:undo # Runs all down() methods in reverse order
```
---
## Seeder Inventory
| # | Timestamp | Name | Records | Required |
|---|-----------|------|---------|----------|
| 1 | 20200430130759 | admin-user | 3 users | Yes |
| 2 | 20200430130760 | user-roles | 7 roles, 54 permissions, 200+ links | Yes |
| 3 | 20231127130745 | sample-data | 30+ sample records | No (opt-in) |
---
## Environment Variables
| Variable | Purpose | Default |
|----------|---------|---------|
| `ENABLE_SAMPLE_DATA` | Enable sample data seeder | `false` |
| `ADMIN_EMAIL` | Admin user email | (from config) |
| `ADMIN_PASS` | Admin user password | (from config) |
| `USER_PASS` | Default user password | (from config) |
---
## Related Documentation
- [DB Migrations](./db-migrations.md) - Schema evolution
- [DB Models](./db-models.md) - Sequelize model definitions
- [RBAC System](../../../documentation/rbac-system.md) - Role-based access control details
- [Authentication](./auth.md) - User authentication