Schema Migration

As your editor evolves, the schema may change. Migrating existing documents to new schemas requires careful planning and implementation.

Overview

Schema migration is the process of updating documents from an older schema version to a newer one. This is essential when you need to add new features, change node structures, or fix design issues.

Without proper migration, users with old documents won't be able to use new features, and the editor may fail to parse or render old documents correctly.

Version Management

Schema Registry

Track schema versions and provide migration paths:

// Schema version definition
const schemaVersion = {
  version: '1.0.0',
  nodes: {},
  marks: {},
  migrations: []
};

class SchemaRegistry {
  #schemas = new Map();
  #migrations = new Map();
  
  register(version, schema) {
    this.#schemas.set(version, schema);
  }
  
  addMigration(fromVersion, toVersion, migration) {
    const key = fromVersion + '->' + toVersion;
    if (!this.#migrations.has(key)) {
      this.#migrations.set(key, []);
    }
    const migrations = this.#migrations.get(key);
    if (migrations) {
      migrations.push(migration);
    }
  }
  
  migrate(document, fromVersion, toVersion) {
    if (fromVersion === toVersion) {
      return document;
    }
    
    // Find migration path
    const path = this.#findMigrationPath(fromVersion, toVersion);
    
    // Apply migrations in sequence
    let currentDoc = document;
    for (const migration of path) {
      currentDoc = migration.apply(currentDoc);
    }
    
    return currentDoc;
  }
  
  #findMigrationPath(from, to) {
    // Use graph search to find shortest path
    // Handle direct migrations and multi-step paths
    const migrations = [];
    // ... path finding logic
    return migrations;
  }
}

Version Tracking

Store version information with documents:

// Document with version metadata
const document = {
  schemaVersion: '1.0.0',
  document: {
    type: 'document',
    children: [...]
  }
};

// When loading document
function loadDocument(data) {
  const version = data.schemaVersion || '1.0.0';
  const currentVersion = '1.2.0';
  
  if (version !== currentVersion) {
    // Migrate to current version
    return schemaRegistry.migrate(
      data.document,
      version,
      currentVersion
    );
  }
  
  return data.document;
}

Migration Strategies

Transform on Load

Migrate documents when they are loaded:

class DocumentLoader {
  async load(data) {
    const parsed = JSON.parse(data);
    
    // Check version
    if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
      // Migrate to current version
      return schemaRegistry.migrate(
        parsed.document,
        parsed.schemaVersion,
        CURRENT_SCHEMA_VERSION
      );
    }
    
    return parsed.document;
  }
}

Lazy Migration

Migrate on access rather than on load:

class LazyDocument {
  #raw = null;
  #migrated = null;
  
  constructor(raw) {
    this.#raw = raw;
  }
  
  get document() {
    if (!this.#migrated) {
      this.#migrated = schemaRegistry.migrate(
        this.#raw.document,
        this.#raw.schemaVersion,
        CURRENT_SCHEMA_VERSION
      );
    }
    return this.#migrated;
  }
}

Background Migration

Migrate documents in the background:

class BackgroundMigrator {
  async migrateInBackground(documents) {
    // Migrate documents in background
    const promises = documents.map(doc => {
      return this.#migrateDocument(doc);
    });
    
    await Promise.all(promises);
  }
  
  async #migrateDocument(doc) {
    // Perform migration
    const migrated = schemaRegistry.migrate(
      doc.document,
      doc.schemaVersion,
      CURRENT_SCHEMA_VERSION
    );
    
    // Save migrated version
    await this.#saveDocument(migrated);
  }
}

Backward Compatibility

Multi-Version Support

Support multiple schema versions simultaneously:

// Support multiple schema versions simultaneously
class MultiVersionSchema {
  #versions = new Map();
  
  parse(data) {
    const version = data.schemaVersion || '1.0.0';
    const schema = this.#versions.get(version);
    
    if (!schema) {
      // Try to migrate
      return this.migrate(data, version);
    }
    
    return schema.parse(data.document);
  }
}

Node Type Mapping

Map old node types to new ones:

// Support old node types by mapping to new ones
function mapOldNodeType(oldType) {
  const mapping = {
    'oldHeading': 'heading',
    'oldParagraph': 'paragraph',
    'oldList': 'list'
  };
  return mapping[oldType] || oldType;
}

// Use in migration
const migration = {
  fromVersion: '1.0.0',
  toVersion: '1.1.0',
  apply(doc) {
    return transformDocument(doc, (node) => {
      const newType = mapOldNodeType(node.type);
      if (newType !== node.type) {
        return { ...node, type: newType };
      }
      return node;
    });
  }
};

Migration Examples

Common migration patterns:

// Example: Migrate old heading format to new
const headingMigration = {
  fromVersion: '1.0.0',
  toVersion: '1.1.0',
  apply(doc) {
    return transformDocument(doc, (node) => {
      if (node.type === 'heading' && typeof node.level === 'string') {
        // Old format: level as string "h1", "h2"
        // New format: level as number 1, 2
        const newNode = Object.assign({}, node);
        newNode.attrs = Object.assign({}, node.attrs);
        newNode.attrs.level = parseInt(node.level.replace('h', ''));
        return newNode;
      }
      return node;
    });
  }
};

// Example: Add default attributes
const addDefaultsMigration = {
  fromVersion: '1.1.0',
  toVersion: '1.2.0',
  apply(doc) {
    return transformDocument(doc, (node) => {
      if (node.type === 'link' && !node.attrs.target) {
        return {
          ...node,
          attrs: {
            ...node.attrs,
            target: '_blank'
          }
        };
      }
      return node;
    });
  }
};

// Example: Remove deprecated node type
const removeDeprecatedMigration = {
  fromVersion: '1.2.0',
  toVersion: '1.3.0',
  apply(doc) {
    return transformDocument(doc, (node) => {
      if (node.type === 'deprecatedNode') {
        // Convert to paragraph
        return {
          type: 'paragraph',
          children: node.children
        };
      }
      return node;
    });
  }
};