npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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-service

Usage

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 ID
  • environmentId (string, optional): Environment ID (default: 'master')
  • accessToken (string): Contentful CMA access token
  • storage (MigrationStorage): Storage implementation for migration history
  • executedBy (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 ID
  • name (string): Human-readable name
  • description (string, optional): Description of what the migration does
  • dryRun (boolean, optional): Run in validation-only mode
  • timeout (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(): ContentfulCMAClient

Migration 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 available

Best 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 tracking
  • contentful-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]