Content model migration management with version control, rollback support, and migration history tracking
npm install @bernierllc/contentful-migration-serviceContent model migration management with version control, rollback support, and migration history tracking for Contentful.
- 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
``bash`
npm install @bernierllc/contentful-migration-service
`typescript
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: 'john@example.com',
});
// 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);Migration failed: ${result.error}
} else {
console.error();`
}
The service uses an abstract MigrationStorage interface, allowing you to plug in any storage backend:
`typescript`
interface MigrationStorage {
saveMigration(record: MigrationRecord): Promise
getMigration(id: string): Promise
getMigrations(spaceId: string, environmentId: string): Promise
hasMigration(id: string, spaceId: string, environmentId: string): Promise
updateMigrationStatus(
id: string,
status: MigrationRecord['status'],
error?: string,
duration?: number
): Promise
}
InMemoryMigrationStorage: For testing and development
`typescript
import { InMemoryMigrationStorage } from '@bernierllc/contentful-migration-service';
const storage = new InMemoryMigrationStorage();
`
Implement your own storage for production use:
`typescript
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
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...
}
`
#### Constructor
`typescript`
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:
`typescript`
async runMigration(
migrationFn: MigrationFunction,
options: MigrationOptions
): Promise
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:
`typescript`
async getMigrationHistory(): Promise
#### getMigration
Get a specific migration by ID:
`typescript`
async getMigration(id: string): Promise
#### hasExecutedMigration
Check if a migration has been successfully executed:
`typescript`
async hasExecutedMigration(id: string): Promise
#### validateMigration
Validate a migration without executing it:
`typescript`
async validateMigration(
migrationFn: MigrationFunction,
options: Pick
): Promise
#### getPendingMigrations
Get migrations that haven't been executed yet:
`typescript`
async getPendingMigrations(migrationIds: string[]): Promise
#### getCMAClient
Get the underlying Contentful CMA client for advanced operations:
`typescript`
getCMAClient(): ContentfulCMAClient
`typescript
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',
});
`
`typescript
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',
});
`
Test migrations before executing:
`typescript
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,
});
}
`
`typescriptTotal migrations: ${history.length}
// Get all migrations
const history = await service.getMigrationHistory();
console.log();
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...');
}
`
`typescript
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),
});
}
`
Logger Integration: ✅ Implemented
Uses @bernierllc/logger for structured logging throughout the service.
NeverHub Integration: ✅ Implemented
Automatically detects and integrates with NeverHub if available.
The service automatically detects and integrates with NeverHub if available:
`typescript
// Events emitted:
// - migration.completed: When migration succeeds
// - migration.failed: When migration fails
const service = new ContentfulMigrationService(config);
// NeverHub automatically initialized if available
`
Always use unique, descriptive IDs for migrations:
`typescript
// Good
id: 'create-blog-post-content-type-2025-01-15'
id: 'add-seo-fields-to-blog-post-001'
// Bad
id: '1'
id: 'migration'
`
Always validate migrations in dry-run mode before executing:
`typescript
// Validate first
await service.runMigration(migrationFn, { id, name, dryRun: true });
// Then execute
await service.runMigration(migrationFn, { id, name, dryRun: false });
`
Never use InMemoryMigrationStorage in production:
`typescript
// Development
const storage = new InMemoryMigrationStorage();
// Production
const storage = new PostgresMigrationStorage(pgPool);
`
Always check migration results and handle failures:
`typescript
const result = await service.runMigration(migrationFn, options);
if (!result.success) {
console.error(Migration failed: ${result.error});Migration ${options.id} failed: ${result.error}
// Send alert, log to monitoring system, etc.
throw new Error();`
}
Break large changes into smaller, atomic migrations:
`typescript
// 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'
`
The service handles errors gracefully and tracks them in storage:
`typescript
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);
}
`
- @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
-
Copyright (c) 2025 Bernier LLC - See LICENSE.md for details
For issues and questions, please contact: support@bernierllc.com