765 lines
23 KiB
Markdown
765 lines
23 KiB
Markdown
# DB Migrations Module
|
|
|
|
## Overview
|
|
|
|
The DB Migrations module manages database schema evolution using a typed
|
|
**Umzug v3** runner. It provides version-controlled, reversible database
|
|
changes that run automatically on server startup.
|
|
|
|
**Location:** `backend/src/db/migrations/`
|
|
|
|
**Files:** 34+ migration files (as of June 2026)
|
|
|
|
**Migration safety policy:** Do not rewrite, rename, or reformat already
|
|
applied migration files. Production databases track migration names in
|
|
`SequelizeMeta`, and fresh databases must replay the same schema history.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
backend/
|
|
├── package.json # Umzug-backed migration scripts
|
|
└── src/db/
|
|
├── db-config.ts # Database connection settings
|
|
├── umzug.ts # Typed Umzug runner
|
|
└── migrations/ # Migration files
|
|
├── package.json # CommonJS boundary for legacy migrations
|
|
├── 20260319000001-*.js # Foreign key constraints
|
|
├── 20260319000002-*.js # Column cleanup
|
|
├── 20260326000001-*.js # Table rename
|
|
├── 20260326000002-*.js # ENUM to TEXT conversion
|
|
├── 20260326000003-*.js # Create table
|
|
├── 20260326000004-*.js # Data backfill
|
|
├── 20260326000005-*.js # Environment fix
|
|
├── 20260326000006-*.js # Cross-environment copy
|
|
├── 20260326043002-*.js # NOT NULL enforcement
|
|
├── 20260326050442-*.js # Column removal
|
|
├── 20260326054410-*.js # Column removal
|
|
├── 20260326060000-*.js # JSON data transformation
|
|
├── 20260326060001-*.js # Table drop (page_elements)
|
|
├── 20260326060002-*.js # Table drop (page_links)
|
|
├── 20260326060003-*.js # Table drop (transitions)
|
|
├── 20260326171017-*.js # Missing data insertion
|
|
├── 20260327000001-*.js # Full data sync
|
|
├── 20260331024423-*.js # Column removal (theme/css)
|
|
├── 20260331054340-*.js # Duplicate cleanup
|
|
├── 20260331063424-*.js # Invalid data cleanup
|
|
├── 20260403000001-*.js # Background video settings
|
|
├── 20260409000001-*.js # Design dimensions (projects)
|
|
├── 20260409111309-*.js # Design dimensions (tour_pages)
|
|
├── 20260605000001-*.js # Background audio settings
|
|
├── 20260626000001-*.js # Private production presentation access
|
|
├── 20260626000002-*.js # Account manager user creation permission
|
|
├── 20260628000001-*.js # Global UI-control settings tables
|
|
└── 20260628000005-*.js # Existing-project UI-control snapshots
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Umzug Runner
|
|
|
|
`backend/src/db/umzug.ts` owns migration and seeder execution. It uses official
|
|
Umzug types, `SequelizeStorage`, and the existing storage tables:
|
|
|
|
| Flow | Files | Storage Table | Stored Names |
|
|
|------|-------|---------------|--------------|
|
|
| Migrations | `src/db/migrations/*.js` | `SequelizeMeta` | `*.js` |
|
|
| Seeders | `src/db/seeders/*.ts` in source, `dist/src/db/seeders/*.js` in build | `SequelizeData` | stable `*.js` names |
|
|
|
|
Seeder files are typed ESM source, and the runner stores stable execution names
|
|
so already executed seeders are not treated as pending.
|
|
|
|
### NPM Scripts
|
|
```bash
|
|
# Run pending migrations
|
|
npm run db:migrate
|
|
|
|
# Undo last migration
|
|
npm run db:migrate:undo
|
|
|
|
# Undo all migrations
|
|
npm run db:migrate:undo:all
|
|
|
|
# Check migration status
|
|
npm run db:migrate:status
|
|
|
|
# Full database reset (drop, create, migrate, seed)
|
|
npm run db:reset
|
|
|
|
# Seed data
|
|
npm run db:seed
|
|
```
|
|
|
|
---
|
|
|
|
## Migration File Structure
|
|
|
|
### Standard Template
|
|
```javascript
|
|
'use strict';
|
|
|
|
/**
|
|
* Migration: [Description]
|
|
*
|
|
* [Explanation of what this migration does and why]
|
|
*/
|
|
module.exports = {
|
|
async up(queryInterface, Sequelize) {
|
|
// Apply changes
|
|
},
|
|
|
|
async down(queryInterface, Sequelize) {
|
|
// Revert changes
|
|
},
|
|
};
|
|
```
|
|
|
|
Use one project-wide migration template for new schema changes. Do not modify
|
|
already applied migration files to match newer style choices.
|
|
|
|
### Naming Convention
|
|
```
|
|
YYYYMMDDHHMMSS-descriptive-name.js
|
|
|
|
Examples:
|
|
- 20260319000001-add-foreign-key-constraints.js
|
|
- 20260326060000-convert-targetpageid-to-slug.js
|
|
- 20260326060001-drop-page-elements-table.js
|
|
- 20260628000001-create-ui-control-settings.js
|
|
- 20260628000005-snapshot-existing-project-ui-controls.js
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Patterns
|
|
|
|
### 1. Transaction Wrapper Pattern
|
|
**Purpose:** Ensure atomic operations - all changes succeed or all fail.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface) {
|
|
const transaction = await queryInterface.sequelize.transaction();
|
|
|
|
try {
|
|
// Multiple operations...
|
|
await queryInterface.addColumn('table', 'column', { ... }, { transaction });
|
|
await queryInterface.addIndex('table', ['column'], { transaction });
|
|
|
|
await transaction.commit();
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** Foreign key constraints, ENUM conversions, data transformations
|
|
|
|
---
|
|
|
|
### 2. Idempotent Check Pattern
|
|
**Purpose:** Safely re-run migrations without errors.
|
|
|
|
```javascript
|
|
// Check if table/column/constraint exists before modifying
|
|
const [results] = await queryInterface.sequelize.query(
|
|
`SELECT column_name FROM information_schema.columns
|
|
WHERE table_name = 'tableName' AND column_name = 'columnName'`,
|
|
{ transaction }
|
|
);
|
|
|
|
if (results.length > 0) {
|
|
console.log('Column already exists, skipping');
|
|
return;
|
|
}
|
|
|
|
// Proceed with modification
|
|
await queryInterface.addColumn('tableName', 'columnName', { ... });
|
|
```
|
|
|
|
**Used in:** All migrations that add columns, constraints, or tables
|
|
|
|
---
|
|
|
|
### 3. Helper Function Pattern
|
|
**Purpose:** Reduce repetition for bulk operations.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface) {
|
|
const transaction = await queryInterface.sequelize.transaction();
|
|
|
|
// Define reusable helper
|
|
const addForeignKey = async (tableName, columnName, references, onDelete) => {
|
|
const constraintName = `${tableName}_${columnName}_fkey`;
|
|
|
|
// Check existence
|
|
const [results] = await queryInterface.sequelize.query(
|
|
`SELECT constraint_name FROM information_schema.table_constraints
|
|
WHERE table_name = '${tableName}' AND constraint_name = '${constraintName}'`,
|
|
{ transaction }
|
|
);
|
|
|
|
if (results.length === 0) {
|
|
await queryInterface.addConstraint(tableName, {
|
|
fields: [columnName],
|
|
type: 'foreign key',
|
|
name: constraintName,
|
|
references,
|
|
onDelete,
|
|
onUpdate: 'CASCADE',
|
|
transaction,
|
|
});
|
|
console.log(`Added FK: ${constraintName}`);
|
|
}
|
|
};
|
|
|
|
// Use helper multiple times
|
|
await addForeignKey('assets', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE');
|
|
await addForeignKey('tour_pages', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE');
|
|
// ... more FKs
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** Foreign key constraints, column removal, index management
|
|
|
|
---
|
|
|
|
### 4. Safe Table Drop Pattern
|
|
**Purpose:** Prevent accidental data loss when dropping tables.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface) {
|
|
// Verify table is empty before dropping
|
|
const [results] = await queryInterface.sequelize.query(
|
|
'SELECT COUNT(*) as count FROM table_name'
|
|
);
|
|
const count = parseInt(results[0].count, 10);
|
|
|
|
if (count > 0) {
|
|
throw new Error(
|
|
`Cannot drop table_name: it contains ${count} records. ` +
|
|
`Please migrate or delete them first.`
|
|
);
|
|
}
|
|
|
|
await queryInterface.dropTable('table_name');
|
|
console.log('Dropped table_name (was empty)');
|
|
},
|
|
|
|
async down(queryInterface, Sequelize) {
|
|
// Full table recreation with all columns, indexes, and constraints
|
|
await queryInterface.createTable('table_name', {
|
|
id: { type: Sequelize.UUID, ... },
|
|
// ... all columns
|
|
});
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `drop-page-elements-table`, `drop-page-links-table`, `drop-transitions-table`
|
|
|
|
---
|
|
|
|
### 5. ENUM to TEXT Conversion Pattern
|
|
**Purpose:** Convert restrictive ENUMs to flexible TEXT while preserving data.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface, Sequelize) {
|
|
const transaction = await queryInterface.sequelize.transaction();
|
|
|
|
try {
|
|
// 1. Create temporary TEXT column
|
|
await queryInterface.addColumn('table', 'column_text', {
|
|
type: Sequelize.TEXT,
|
|
allowNull: true,
|
|
}, { transaction });
|
|
|
|
// 2. Copy ENUM values to TEXT
|
|
await queryInterface.sequelize.query(
|
|
`UPDATE table SET column_text = column::TEXT`,
|
|
{ transaction }
|
|
);
|
|
|
|
// 3. Drop old ENUM column
|
|
await queryInterface.removeColumn('table', 'column', { transaction });
|
|
|
|
// 4. Rename TEXT column
|
|
await queryInterface.renameColumn('table', 'column_text', 'column', { transaction });
|
|
|
|
// 5. Add NOT NULL constraint
|
|
await queryInterface.changeColumn('table', 'column', {
|
|
type: Sequelize.TEXT,
|
|
allowNull: false,
|
|
}, { transaction });
|
|
|
|
// 6. Drop ENUM type
|
|
await queryInterface.sequelize.query(
|
|
`DROP TYPE IF EXISTS "enum_table_column"`,
|
|
{ transaction }
|
|
);
|
|
|
|
await transaction.commit();
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async down(queryInterface, Sequelize) {
|
|
// Recreate ENUM type
|
|
await queryInterface.sequelize.query(`
|
|
CREATE TYPE "enum_table_column" AS ENUM ('value1', 'value2', 'value3')
|
|
`);
|
|
// ... reverse the process
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `convert-element-type-enum-to-text`
|
|
|
|
---
|
|
|
|
### 6. Data Backfill Pattern
|
|
**Purpose:** Populate new tables/columns with data from existing records.
|
|
|
|
```javascript
|
|
// Define default data
|
|
const DEFAULT_ELEMENT_TYPES = [
|
|
{ element_type: 'navigation_next', name: 'Forward Button', sort_order: 1, settings_json: {...} },
|
|
{ element_type: 'navigation_prev', name: 'Back Button', sort_order: 2, settings_json: {...} },
|
|
// ... more defaults
|
|
];
|
|
|
|
module.exports = {
|
|
async up(queryInterface, Sequelize) {
|
|
// Get existing records
|
|
const [projects] = await queryInterface.sequelize.query(
|
|
`SELECT id FROM projects WHERE "deletedAt" IS NULL`,
|
|
{ type: Sequelize.QueryTypes.SELECT }
|
|
);
|
|
|
|
// For each project, check and insert missing records
|
|
for (const project of projects) {
|
|
const [existing] = await queryInterface.sequelize.query(
|
|
`SELECT element_type FROM project_element_defaults
|
|
WHERE "projectId" = :projectId AND "deletedAt" IS NULL`,
|
|
{ replacements: { projectId: project.id }, type: Sequelize.QueryTypes.SELECT }
|
|
);
|
|
|
|
const existingTypes = new Set(existing.map(d => d.element_type));
|
|
|
|
for (const defaultType of DEFAULT_ELEMENT_TYPES) {
|
|
if (!existingTypes.has(defaultType.element_type)) {
|
|
await queryInterface.sequelize.query(`
|
|
INSERT INTO project_element_defaults (...)
|
|
VALUES (gen_random_uuid(), :element_type, :name, ...)
|
|
`, { replacements: { ... } });
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
async down(queryInterface) {
|
|
// Delete only records created by this migration
|
|
await queryInterface.sequelize.query(
|
|
`DELETE FROM project_element_defaults WHERE snapshot_version = 1`
|
|
);
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `backfill-project-element-defaults`, `sync-all-element-type-defaults`
|
|
|
|
---
|
|
|
|
### 7. Cross-Environment Data Copy Pattern
|
|
**Purpose:** Copy content between environments (dev → stage → production).
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface, Sequelize) {
|
|
const projects = await queryInterface.sequelize.query(
|
|
`SELECT id FROM projects WHERE "deletedAt" IS NULL`,
|
|
{ type: Sequelize.QueryTypes.SELECT }
|
|
);
|
|
|
|
for (const project of projects) {
|
|
// Check if target environment already has content
|
|
const [stageCheck] = await queryInterface.sequelize.query(
|
|
`SELECT COUNT(*)::int as count FROM tour_pages
|
|
WHERE "projectId" = '${project.id}' AND environment = 'stage'`
|
|
);
|
|
|
|
if (stageCheck?.count > 0) continue;
|
|
|
|
// Copy with INSERT...SELECT, generating new UUIDs
|
|
await queryInterface.sequelize.query(`
|
|
INSERT INTO tour_pages (id, slug, name, ..., environment, source_key, ...)
|
|
SELECT
|
|
gen_random_uuid(),
|
|
slug, name, ...,
|
|
'stage', -- New environment
|
|
id::text, -- Track source record for rollback
|
|
...
|
|
FROM tour_pages
|
|
WHERE "projectId" = '${project.id}' AND environment = 'dev'
|
|
`);
|
|
|
|
// Copy related records using source_key for ID mapping
|
|
await queryInterface.sequelize.query(`
|
|
INSERT INTO page_elements (id, ..., "pageId", ...)
|
|
SELECT
|
|
gen_random_uuid(), ...,
|
|
stage_page.id, -- Map to new page ID
|
|
...
|
|
FROM page_elements pe
|
|
INNER JOIN tour_pages dev_page ON pe."pageId" = dev_page.id
|
|
INNER JOIN tour_pages stage_page ON stage_page.source_key = dev_page.id::text
|
|
WHERE dev_page.environment = 'dev'
|
|
`);
|
|
}
|
|
},
|
|
|
|
async down(queryInterface) {
|
|
// Delete records with source_key (created by migration)
|
|
await queryInterface.sequelize.query(
|
|
`DELETE FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL`
|
|
);
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `copy-dev-to-stage`
|
|
|
|
---
|
|
|
|
### 8. JSON Field Transformation Pattern
|
|
**Purpose:** Transform data stored in JSON columns.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface, Sequelize) {
|
|
const transaction = await queryInterface.sequelize.transaction();
|
|
|
|
try {
|
|
// Get all records with JSON data
|
|
const [records] = await queryInterface.sequelize.query(
|
|
`SELECT id, "projectId", environment, slug, json_column
|
|
FROM table_name WHERE json_column IS NOT NULL`,
|
|
{ transaction }
|
|
);
|
|
|
|
// Build lookup maps for ID → slug transformations
|
|
const slugById = new Map();
|
|
records.forEach(r => slugById.set(r.id, { projectId: r.projectId, slug: r.slug }));
|
|
|
|
// Transform each record
|
|
for (const record of records) {
|
|
const jsonData = typeof record.json_column === 'string'
|
|
? JSON.parse(record.json_column)
|
|
: record.json_column;
|
|
|
|
let hasChanges = false;
|
|
|
|
// Transform JSON structure
|
|
if (jsonData.elements) {
|
|
jsonData.elements.forEach(element => {
|
|
if (element.targetPageId) {
|
|
const target = slugById.get(element.targetPageId);
|
|
if (target) {
|
|
element.targetPageSlug = target.slug;
|
|
delete element.targetPageId;
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (hasChanges) {
|
|
await queryInterface.sequelize.query(
|
|
`UPDATE table_name SET json_column = :json WHERE id = :id`,
|
|
{
|
|
replacements: { json: JSON.stringify(jsonData), id: record.id },
|
|
transaction
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
await transaction.commit();
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `convert-targetpageid-to-slug`
|
|
|
|
---
|
|
|
|
### 9. Constraint Enforcement Pattern
|
|
**Purpose:** Add NOT NULL constraints after fixing existing NULL values.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface) {
|
|
// First, fix any NULL values
|
|
await queryInterface.sequelize.query(
|
|
`UPDATE table_name SET column = 'default' WHERE column IS NULL`
|
|
);
|
|
|
|
// Then add NOT NULL constraint with default
|
|
await queryInterface.sequelize.query(`
|
|
ALTER TABLE table_name
|
|
ALTER COLUMN column SET NOT NULL,
|
|
ALTER COLUMN column SET DEFAULT 'default'
|
|
`);
|
|
},
|
|
|
|
async down(queryInterface) {
|
|
await queryInterface.sequelize.query(`
|
|
ALTER TABLE table_name
|
|
ALTER COLUMN column DROP NOT NULL,
|
|
ALTER COLUMN column DROP DEFAULT
|
|
`);
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `enforce-environment-not-null`
|
|
|
|
---
|
|
|
|
### 10. Safe Down Migration Pattern
|
|
**Purpose:** Handle cases where down migration isn't meaningful.
|
|
|
|
```javascript
|
|
module.exports = {
|
|
async up(queryInterface, Sequelize) {
|
|
// Add/update missing data
|
|
// ...
|
|
},
|
|
|
|
async down(_queryInterface, _Sequelize) {
|
|
// This migration only adds missing data, not destructive
|
|
console.log('No down migration needed - this migration only adds missing data.');
|
|
},
|
|
};
|
|
```
|
|
|
|
**Used in:** `sync-all-element-type-defaults`
|
|
|
|
---
|
|
|
|
## Migration Categories
|
|
|
|
### Schema Changes
|
|
| Migration | Description |
|
|
|-----------|-------------|
|
|
| `add-foreign-key-constraints` | Add FK constraints to all model associations |
|
|
| `create-project-element-defaults` | Create new table with indexes |
|
|
| `drop-page-elements-table` | Drop unused table |
|
|
| `drop-page-links-table` | Drop unused table |
|
|
| `drop-transitions-table` | Drop unused table |
|
|
|
|
### Column Modifications
|
|
| Migration | Description |
|
|
|-----------|-------------|
|
|
| `remove-redundant-deletion-columns` | Remove `is_deleted`, `deleted_at_time` |
|
|
| `remove-project-phase-column` | Remove redundant `phase` column |
|
|
| `remove-entry-page-slug-column` | Remove unused column |
|
|
| `convert-element-type-enum-to-text` | ENUM → TEXT for flexibility |
|
|
| `enforce-environment-not-null` | Add NOT NULL constraint |
|
|
| `remove-unused-theme-columns-from-projects` | Remove `theme_config_json`, `custom_css_json`, `cdn_base_url` |
|
|
| `add-background-video-settings` | Add video playback settings (autoplay, loop, muted, start/end time) to tour_pages |
|
|
| `add-design-dimensions-to-projects` | Add `design_width`, `design_height` to projects table |
|
|
| `add-design-dimensions-to-tour-pages` | Add `design_width`, `design_height` to tour_pages table |
|
|
|
|
### Table Renames
|
|
| Migration | Description |
|
|
|-----------|-------------|
|
|
| `rename-ui-elements-to-element-type-defaults` | Rename for clarity |
|
|
|
|
### Data Migrations
|
|
| Migration | Description |
|
|
|-----------|-------------|
|
|
| `backfill-project-element-defaults` | Populate new table for existing projects |
|
|
| `copy-dev-to-stage` | Initialize stage environment |
|
|
| `convert-targetpageid-to-slug` | Transform JSON navigation references |
|
|
| `fix-project-audio-tracks-environment` | Fix environment values |
|
|
| `add-missing-element-type-defaults` | Insert missing default rows |
|
|
| `sync-all-element-type-defaults` | Full sync of all 11 element types |
|
|
| `remove-duplicate-element-type-defaults` | Remove duplicate records created during earlier migrations |
|
|
| `cleanup-invalid-element-type-defaults` | Clean up invalid entries and ensure data integrity |
|
|
|
|
---
|
|
|
|
## Foreign Key Strategies
|
|
|
|
| Strategy | When to Use | Example |
|
|
|----------|-------------|---------|
|
|
| `CASCADE` | Delete child when parent deleted | `assets.projectId → projects.id` |
|
|
| `SET NULL` | Preserve record, nullify FK | `publish_events.userId → users.id` (audit trail) |
|
|
| `SET NULL` + `allowNull: true` | Optional FK | `users.app_roleId → roles.id` |
|
|
|
|
```javascript
|
|
// CASCADE - delete assets when project is deleted
|
|
await addForeignKey('assets', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE');
|
|
|
|
// SET NULL - preserve audit log when user is deleted
|
|
await addForeignKey('access_logs', 'userId', { table: 'users', field: 'id' }, 'SET NULL');
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Use Transactions
|
|
```javascript
|
|
const transaction = await queryInterface.sequelize.transaction();
|
|
try {
|
|
// ... operations
|
|
await transaction.commit();
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
throw error;
|
|
}
|
|
```
|
|
|
|
### 2. Check Before Modify
|
|
```javascript
|
|
// Always check existence before adding/removing
|
|
const tableExists = await queryInterface.sequelize.query(
|
|
`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'name')`
|
|
);
|
|
```
|
|
|
|
### 3. Log Progress
|
|
```javascript
|
|
console.log(`Migrating project ${projectId}: ${addedCount} records added`);
|
|
console.log('Migration complete: All foreign keys added');
|
|
```
|
|
|
|
### 4. Safe Drops
|
|
```javascript
|
|
// Never drop non-empty tables silently
|
|
if (count > 0) {
|
|
throw new Error(`Cannot drop table: contains ${count} records`);
|
|
}
|
|
```
|
|
|
|
### 5. Reversible Operations
|
|
```javascript
|
|
// Down migration should restore previous state
|
|
async down(queryInterface, Sequelize) {
|
|
// Recreate everything that was dropped
|
|
await queryInterface.createTable('table_name', { ... });
|
|
await queryInterface.addIndex('table_name', [...]);
|
|
}
|
|
```
|
|
|
|
### 6. Use Parameterized Queries
|
|
```javascript
|
|
// Good - prevents SQL injection
|
|
await queryInterface.sequelize.query(
|
|
`UPDATE table SET column = :value WHERE id = :id`,
|
|
{ replacements: { value: 'safe', id: record.id } }
|
|
);
|
|
|
|
// Avoid - SQL injection risk
|
|
await queryInterface.sequelize.query(
|
|
`UPDATE table SET column = '${unsafeValue}' WHERE id = '${unsafeId}'`
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Running Migrations
|
|
|
|
### Development
|
|
```bash
|
|
cd backend
|
|
npm run db:migrate
|
|
```
|
|
|
|
### Server Startup
|
|
Migrations run automatically via `npm start`:
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"start": "npm run db:migrate && npm run db:seed && npm run watch"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Migration Status
|
|
```bash
|
|
npm run db:migrate:status
|
|
```
|
|
|
|
### Undo Migrations
|
|
```bash
|
|
# Undo last migration
|
|
npm run db:migrate:undo
|
|
|
|
# Undo all migrations (dangerous!)
|
|
npm run db:migrate:undo:all
|
|
```
|
|
|
|
### Create New Migration
|
|
|
|
Create new migration files manually under `backend/src/db/migrations/` only
|
|
when a schema change is required. Keep every migration reversible or document an
|
|
explicit rollback/backup plan.
|
|
|
|
---
|
|
|
|
## Current Migration Inventory
|
|
|
|
| # | Timestamp | Name | Type |
|
|
|---|-----------|------|------|
|
|
| 1 | 20260319000001 | add-foreign-key-constraints | Schema |
|
|
| 2 | 20260319000002 | remove-redundant-deletion-columns | Column |
|
|
| 3 | 20260326000001 | rename-ui-elements-to-element-type-defaults | Rename |
|
|
| 4 | 20260326000002 | convert-element-type-enum-to-text | Column |
|
|
| 5 | 20260326000003 | create-project-element-defaults | Schema |
|
|
| 6 | 20260326000004 | backfill-project-element-defaults | Data |
|
|
| 7 | 20260326000005 | fix-project-audio-tracks-environment | Data |
|
|
| 8 | 20260326000006 | copy-dev-to-stage | Data |
|
|
| 9 | 20260326043002 | enforce-environment-not-null | Column |
|
|
| 10 | 20260326050442 | remove-project-phase-column | Column |
|
|
| 11 | 20260326054410 | remove-entry-page-slug-column | Column |
|
|
| 12 | 20260326060000 | convert-targetpageid-to-slug | Data |
|
|
| 13 | 20260326060001 | drop-page-elements-table | Schema |
|
|
| 14 | 20260326060002 | drop-page-links-table | Schema |
|
|
| 15 | 20260326060003 | drop-transitions-table | Schema |
|
|
| 16 | 20260326171017 | add-missing-element-type-defaults | Data |
|
|
| 17 | 20260327000001 | sync-all-element-type-defaults | Data |
|
|
| 18 | 20260331024423 | remove-unused-theme-columns-from-projects | Column |
|
|
| 19 | 20260331054340 | remove-duplicate-element-type-defaults | Data |
|
|
| 20 | 20260331063424 | cleanup-invalid-element-type-defaults | Data |
|
|
| 21 | 20260403000001 | add-background-video-settings | Column |
|
|
| 22 | 20260409000001 | add-design-dimensions-to-projects | Column |
|
|
| 23 | 20260409111309 | add-design-dimensions-to-tour-pages | Column |
|
|
| 24 | 20260605000001 | add-background-audio-settings | Column |
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [DB Models](./db-models.md) - Sequelize model definitions
|
|
- [DB API](./db-api.md) - Database access layer
|
|
- [Database Schema](../database-schema.md) - Complete schema reference
|