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;
});
}
};Related Pages
Model & Schema
Overview of model and schema concepts
Collaborative Editing
Operational Transformation and CRDTs
Indexing Strategies
Document, position, and content indexing
Advanced Validation
Recursive validation and performance optimization
Transaction System
Atomic operations and rollback mechanisms
Node Types
Comprehensive node type examples