@bernierllc/contentful-migration-service
v1.0.2
Published
Content model migration management with version control, rollback support, and migration history tracking
Readme
@bernierllc/contentful-migration-service
Content model migration management with version control, rollback support, and migration history tracking for Contentful.
Features
- Migration Execution: Run content model migrations using Contentful's official migration library
- History Tracking: Track migration execution history with flexible storage backends
- Dry-Run Mode: Validate migrations before executing them
- Abstract Storage: Plug in your own storage implementation (PostgreSQL, MongoDB, etc.)
- NeverHub Integration: Optional integration with NeverHub for event tracking
- Type Safety: Full TypeScript support with strict typing
Installation
npm install @bernierllc/contentful-migration-serviceUsage
Quick Start
import {
ContentfulMigrationService,
InMemoryMigrationStorage
} from '@bernierllc/contentful-migration-service';
// Create storage (in-memory for testing, use persistent storage in production)
const storage = new InMemoryMigrationStorage();
// Initialize service
const service = new ContentfulMigrationService({
spaceId: 'your-space-id',
environmentId: 'master',
accessToken: 'your-cma-token',
storage,
executedBy: '[email protected]',
});
// Define migration
const migrationFn = (migration) => {
const blogPost = migration.createContentType('blogPost', {
name: 'Blog Post',
description: 'A blog post content type',
});
blogPost.createField('title', {
name: 'Title',
type: 'Symbol',
required: true,
});
blogPost.createField('content', {
name: 'Content',
type: 'Text',
});
};
// Run migration
const result = await service.runMigration(migrationFn, {
id: 'create-blog-post-001',
name: 'Create Blog Post Content Type',
description: 'Initial blog post content type with title and content fields',
});
if (result.success) {
console.log(`Migration completed in ${result.duration}ms`);
} else {
console.error(`Migration failed: ${result.error}`);
}Core Concepts
Migration Storage
The service uses an abstract MigrationStorage interface, allowing you to plug in any storage backend:
interface MigrationStorage {
saveMigration(record: MigrationRecord): Promise<void>;
getMigration(id: string): Promise<MigrationRecord | null>;
getMigrations(spaceId: string, environmentId: string): Promise<MigrationRecord[]>;
hasMigration(id: string, spaceId: string, environmentId: string): Promise<boolean>;
updateMigrationStatus(
id: string,
status: MigrationRecord['status'],
error?: string,
duration?: number
): Promise<void>;
}Built-in Storage
InMemoryMigrationStorage: For testing and development
import { InMemoryMigrationStorage } from '@bernierllc/contentful-migration-service';
const storage = new InMemoryMigrationStorage();Custom Storage
Implement your own storage for production use:
import { MigrationStorage, MigrationRecord } from '@bernierllc/contentful-migration-service';
import { Pool } from 'pg';
class PostgresMigrationStorage implements MigrationStorage {
constructor(private pool: Pool) {}
async saveMigration(record: MigrationRecord): Promise<void> {
await this.pool.query(
`INSERT INTO migrations (id, name, description, executed_at, status, space_id, environment_id, dry_run)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status,
duration = EXCLUDED.duration,
error = EXCLUDED.error`,
[record.id, record.name, record.description, record.executedAt,
record.status, record.spaceId, record.environmentId, record.dryRun]
);
}
// Implement other methods...
}API Reference
ContentfulMigrationService
Constructor
constructor(config: MigrationServiceConfig)Config Options:
spaceId(string): Contentful space IDenvironmentId(string, optional): Environment ID (default: 'master')accessToken(string): Contentful CMA access tokenstorage(MigrationStorage): Storage implementation for migration historyexecutedBy(string, optional): Identifier for who is executing migrations
runMigration
Run a migration function:
async runMigration(
migrationFn: MigrationFunction,
options: MigrationOptions
): Promise<MigrationResult>Options:
id(string): Unique migration IDname(string): Human-readable namedescription(string, optional): Description of what the migration doesdryRun(boolean, optional): Run in validation-only modetimeout(number, optional): Timeout in milliseconds
Returns: MigrationResult with success status, duration, and any errors
getMigrationHistory
Get all migrations for the configured space/environment:
async getMigrationHistory(): Promise<MigrationRecord[]>getMigration
Get a specific migration by ID:
async getMigration(id: string): Promise<MigrationRecord | null>hasExecutedMigration
Check if a migration has been successfully executed:
async hasExecutedMigration(id: string): Promise<boolean>validateMigration
Validate a migration without executing it:
async validateMigration(
migrationFn: MigrationFunction,
options: Pick<MigrationOptions, 'id' | 'name' | 'description'>
): Promise<ValidationResult>getPendingMigrations
Get migrations that haven't been executed yet:
async getPendingMigrations(migrationIds: string[]): Promise<string[]>getCMAClient
Get the underlying Contentful CMA client for advanced operations:
getCMAClient(): ContentfulCMAClientMigration Examples
Creating a Content Type
const migrationFn = (migration) => {
const author = migration.createContentType('author', {
name: 'Author',
description: 'Author of blog posts',
});
author.createField('name', {
name: 'Name',
type: 'Symbol',
required: true,
});
author.createField('bio', {
name: 'Biography',
type: 'Text',
});
author.displayField('name');
};
await service.runMigration(migrationFn, {
id: 'create-author-001',
name: 'Create Author Content Type',
});Modifying an Existing Content Type
const migrationFn = (migration) => {
const blogPost = migration.editContentType('blogPost');
blogPost.createField('publishDate', {
name: 'Publish Date',
type: 'Date',
required: false,
});
blogPost.createField('author', {
name: 'Author',
type: 'Link',
linkType: 'Entry',
validations: [{
linkContentType: ['author'],
}],
});
};
await service.runMigration(migrationFn, {
id: 'add-author-to-blog-post-001',
name: 'Add Author and Publish Date to Blog Post',
});Dry-Run Mode
Test migrations before executing:
const result = await service.runMigration(migrationFn, {
id: 'risky-migration-001',
name: 'Risky Migration',
dryRun: true,
});
if (result.success) {
console.log('Migration is valid!');
// Now run it for real
await service.runMigration(migrationFn, {
id: 'risky-migration-001',
name: 'Risky Migration',
dryRun: false,
});
}Migration History
// Get all migrations
const history = await service.getMigrationHistory();
console.log(`Total migrations: ${history.length}`);
history.forEach(migration => {
console.log(`${migration.name}: ${migration.status} (${migration.executedAt})`);
});
// Check if specific migration was executed
const hasRun = await service.hasExecutedMigration('create-blog-post-001');
if (hasRun) {
console.log('Migration already executed, skipping...');
}Pending Migrations
const allMigrationIds = [
'create-blog-post-001',
'create-author-001',
'add-author-to-blog-post-001',
];
const pending = await service.getPendingMigrations(allMigrationIds);
console.log(`Pending migrations: ${pending.join(', ')}`);
// Run all pending migrations
for (const id of pending) {
const migrationFn = getMigrationFunction(id); // Your function to load migration
await service.runMigration(migrationFn, {
id,
name: getMigrationName(id),
});
}Integration Status
Logger Integration: ✅ Implemented
Uses @bernierllc/logger for structured logging throughout the service.
NeverHub Integration: ✅ Implemented Automatically detects and integrates with NeverHub if available.
NeverHub Integration
The service automatically detects and integrates with NeverHub if available:
// Events emitted:
// - migration.completed: When migration succeeds
// - migration.failed: When migration fails
const service = new ContentfulMigrationService(config);
// NeverHub automatically initialized if availableBest Practices
1. Use Unique Migration IDs
Always use unique, descriptive IDs for migrations:
// Good
id: 'create-blog-post-content-type-2025-01-15'
id: 'add-seo-fields-to-blog-post-001'
// Bad
id: '1'
id: 'migration'2. Test with Dry-Run First
Always validate migrations in dry-run mode before executing:
// Validate first
await service.runMigration(migrationFn, { id, name, dryRun: true });
// Then execute
await service.runMigration(migrationFn, { id, name, dryRun: false });3. Use Persistent Storage in Production
Never use InMemoryMigrationStorage in production:
// Development
const storage = new InMemoryMigrationStorage();
// Production
const storage = new PostgresMigrationStorage(pgPool);4. Handle Failures Gracefully
Always check migration results and handle failures:
const result = await service.runMigration(migrationFn, options);
if (!result.success) {
console.error(`Migration failed: ${result.error}`);
// Send alert, log to monitoring system, etc.
throw new Error(`Migration ${options.id} failed: ${result.error}`);
}5. Keep Migrations Small
Break large changes into smaller, atomic migrations:
// Instead of one large migration:
// ❌ 'update-entire-content-model-001'
// Use multiple smaller migrations:
// ✅ 'add-seo-fields-to-blog-post-001'
// ✅ 'add-author-relationship-to-blog-post-002'
// ✅ 'add-categories-to-blog-post-003'Error Handling
The service handles errors gracefully and tracks them in storage:
try {
const result = await service.runMigration(migrationFn, options);
if (!result.success) {
// Migration failed but error was caught
console.error(result.error);
}
} catch (error) {
// Unexpected error (storage failure, etc.)
console.error('Unexpected error:', error);
}Dependencies
@bernierllc/contentful-types- Type definitions@bernierllc/contentful-cma-client- CMA client wrapper@bernierllc/logger- Logging@bernierllc/neverhub-adapter- Optional event trackingcontentful-migration- Official Contentful migration library
License
Copyright (c) 2025 Bernier LLC - See LICENSE.md for details
Support
For issues and questions, please contact: [email protected]
