Skip to content

Instantly share code, notes, and snippets.

@timofei-iatsenko
Created October 13, 2023 08:43
Show Gist options
  • Save timofei-iatsenko/eff1611f7957360be400c9ad23a83330 to your computer and use it in GitHub Desktop.
Save timofei-iatsenko/eff1611f7957360be400c9ad23a83330 to your computer and use it in GitHub Desktop.
Migration script from payload-lexical-plugin to Payload 2.0
import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-mongodb';
import { Field, RichTextField, BlockField } from 'payload/dist/fields/config/types';
import { CollectionModel } from '@payloadcms/db-mongodb/dist/types';
/**
* Migration steps:
* 1. Go and change all your fields to `richText` instead of plugin's wrapper
* 2. Run a migration
*
* This migration supports:
* - Localized and not localized fields
* - Root fields in collection & globals
* - Recursively searhing for fields in blocks if any defined
* - Versions
*/
export class RethrownError extends Error {
public message: string;
constructor(message: string, originalError: Error) {
super();
this.message = message + ' ' + originalError.message;
this.stack = `Error: ${message} \nOriginal: ` + originalError.stack;
}
}
function migrateLocalizedField(fieldValue: any) {
return Object.keys(fieldValue).reduce((acc, lang) => {
acc[lang] = fieldValue[lang].jsonContent || fieldValue[lang];
return acc;
}, {});
}
function migrateField(fieldValue: any) {
return fieldValue?.jsonContent || fieldValue;
}
function appendPath(prefix: string, ...value: string[]) {
return [...prefix.split('.'), ...value].filter(Boolean).join('.');
}
function updateDoc(
doc: Record<string, any>,
fields: Field[],
path: string = ''
): Record<string, any> {
// update root fields
const richTextFields = fields.filter((field) => field.type === 'richText') as RichTextField[];
let update: Record<string, any> = {};
richTextFields.forEach((field) => {
try {
const backupFieldName = '__old__' + field.name;
// if already processed (has a backup) skip or if empty
if (doc[backupFieldName] || !doc[field.name]) {
return;
}
update[appendPath(path, field.name)] = field.localized
? migrateLocalizedField(doc[field.name])
: migrateField(doc[field.name]);
// store a backup
update[appendPath(path, backupFieldName)] = doc[field.name];
} catch (e) {
throw new RethrownError(`Error while processing ${appendPath(path, field.name)}`, e);
}
});
// update blocks if any
const blockFields = fields.filter((field) => field.type === 'blocks') as BlockField[];
blockFields.forEach((field) => {
if (!doc[field.name]) {
return;
}
function updateBlock(blockDoc: any, blockPath: string) {
const blockDef = field.blocks.find((block) => block.slug === blockDoc.blockType);
if (!blockDef) {
throw new Error(
`found unsupported block of type ${blockDoc.blockType} in path: ${blockPath}, id: ${doc._id}`
);
}
update = {
...update,
...updateDoc(blockDoc, blockDef.fields, blockPath),
};
}
if (Array.isArray(doc[field.name])) {
doc[field.name].forEach((blockDoc, index) => {
updateBlock(blockDoc, appendPath(path, field.name, index.toString()));
});
} else {
updateBlock(doc[field.name], appendPath(path, field.name));
}
});
return update;
}
async function writeDoc(_id: any, collection: CollectionModel['collection'], update: any) {
if (!Object.keys(update).length) {
return;
}
await collection.findOneAndUpdate(
{
_id,
},
{
$set: update,
}
);
}
async function updateAndWriteVersionDoc(
doc: Record<string, any>,
fields: Field[],
collection: CollectionModel['collection']
) {
try {
const update = updateDoc(doc.version, fields, 'version');
await writeDoc(doc._id, collection, update);
} catch (e) {
throw new RethrownError(
`Error processing doc id: ${doc._id}, collection: ${collection.name}`,
e
);
}
}
async function updateAndWriteDoc(
doc: Record<string, any>,
fields: Field[],
collection: CollectionModel['collection']
) {
try {
const update = updateDoc(doc, fields);
await writeDoc(doc._id, collection, update);
} catch (e) {
throw new RethrownError(
`Error processing doc id: ${doc._id}, collection: ${collection.name}`,
e
);
}
}
export async function up({ payload }: MigrateUpArgs): Promise<void> {
// For each collection
await Promise.all(
payload.config.collections.map(async ({ slug, fields, versions }) => {
const Collection = payload.db.collections[slug].collection;
const docs = await Collection.find().toArray();
await Promise.all(docs.map((doc) => updateAndWriteDoc(doc, fields, Collection)));
// for each version of collection
if (versions) {
const Collection = payload.db.versions[slug].collection;
const docs = await Collection.find().toArray();
return Promise.all(docs.map((doc) => updateAndWriteVersionDoc(doc, fields, Collection)));
}
})
);
// For each global
const Globals = payload.db.globals.collection;
const docs = await Globals.find().toArray();
await Promise.all(
docs.map((doc) => {
const globalDef = payload.config.globals.find((item) => item.slug === doc.globalType);
return updateAndWriteDoc(doc, globalDef.fields, Globals);
})
);
// for each global versions
await Promise.all(
payload.config.globals.map(async ({ slug, fields, versions }) => {
if (versions) {
const VersionCollection = payload.db.versions[slug].collection;
const docs = await VersionCollection.find().toArray();
return Promise.all(
docs.map((doc) => updateAndWriteVersionDoc(doc, fields, VersionCollection))
);
}
})
);
}
export async function down({ payload }: MigrateDownArgs): Promise<void> {
// Migration code
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment