@sanity/migrate
Version:
Tooling for running data migrations on Sanity.io projects
197 lines (181 loc) • 6.54 kB
text/typescript
import {type Mutation as RawMutation} from '@sanity/client'
import {SanityEncoder} from '@sanity/mutate'
import {type Path, type SanityDocument} from '@sanity/types'
import arrify from 'arrify'
import {type JsonArray, type JsonObject, type JsonValue} from '../json.js'
import {isMutation, isNodePatch, isOperation, isTransaction} from '../mutations/asserters.js'
import {
at,
type Mutation,
type NodePatch,
type Operation,
patch,
type Transaction,
} from '../mutations/index.js'
import {
type AsyncIterableMigration,
type Migration,
type MigrationContext,
type NodeMigration,
type NodeMigrationReturnValue,
} from '../types.js'
import {flatMapDeep} from './utils/flatMapDeep.js'
import {getValueType} from './utils/getValueType.js'
export function normalizeMigrateDefinition(migration: Migration): AsyncIterableMigration {
if (typeof migration.migrate == 'function') {
// assume AsyncIterableMigration
return normalizeIteratorValues(migration.migrate)
}
return createAsyncIterableMutation(migration.migrate, {
...(migration.filter !== undefined && {filter: migration.filter}),
...(migration.documentTypes !== undefined && {documentTypes: migration.documentTypes}),
})
}
function normalizeIteratorValues(asyncIterable: AsyncIterableMigration): AsyncIterableMigration {
return async function* run(docs, context) {
for await (const documentMutations of asyncIterable(docs, context)) {
yield normalizeMutation(documentMutations)
}
}
}
/**
* Normalize a mutation or a NodePatch to a document mutation
* @param documentId - The document id
* @param change - The Mutation or NodePatch
*/
function normalizeMutation(
change: (Mutation | RawMutation | Transaction)[] | Mutation | RawMutation | Transaction,
): (Mutation | Transaction)[] {
if (Array.isArray(change)) {
return change.flatMap((ch) => normalizeMutation(ch))
}
if (isRawMutation(change)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SanityEncoder.decodeAll requires specific mutation format
return SanityEncoder.decodeAll([change] as any) as Mutation[]
}
return [change]
}
function isRawMutation(
mutation: Mutation | NodePatch | Operation | RawMutation | Transaction,
): mutation is RawMutation {
return (
'createIfNotExists' in mutation ||
'createOrReplace' in mutation ||
'create' in mutation ||
'patch' in mutation ||
'delete' in mutation
)
}
export function createAsyncIterableMutation(
migration: NodeMigration,
opts: {documentTypes?: string[]; filter?: string},
): AsyncIterableMigration {
const documentTypesSet = new Set(opts.documentTypes)
return async function* run(docs, context) {
for await (const doc of docs()) {
if (opts.documentTypes && !documentTypesSet.has(doc._type)) continue
const documentMutations = await collectDocumentMutations(migration, doc, context)
if (documentMutations.length > 0) {
yield documentMutations
}
}
}
}
async function collectDocumentMutations(
migration: NodeMigration,
doc: SanityDocument,
context: MigrationContext,
): Promise<(Mutation | Transaction)[]> {
const documentMutations = Promise.resolve(migration.document?.(doc, context))
const nodeMigrations = flatMapDeep(doc as JsonValue, async (value, path) => {
const [nodeReturnValues, nodeTypeReturnValues] = await Promise.all([
Promise.resolve(migration.node?.(value, path, context)),
Promise.resolve(migrateNodeType(migration, value, path, context)),
])
return [...arrify(nodeReturnValues), ...arrify(nodeTypeReturnValues)].map(
(change) => change && normalizeNodeMutation(path, change),
)
})
const resolvedDocumentMutations = arrify(await documentMutations)
const resolvedNodeMigrations = await Promise.all(nodeMigrations)
return [...resolvedDocumentMutations, ...resolvedNodeMigrations]
.flat()
.flatMap((change) => (change ? normalizeDocumentMutation(doc._id, change) : []))
}
/**
* Normalize a mutation or a NodePatch to a document mutation
* @param documentId - The document id
* @param change - The Mutation or NodePatch
*/
function normalizeDocumentMutation(
documentId: string,
change:
| (Mutation | NodePatch | RawMutation | Transaction)[]
| Mutation
| NodePatch
| RawMutation
| Transaction,
): (Mutation | Transaction)[] | Mutation | Transaction {
if (Array.isArray(change)) {
return change.flatMap((ch) => normalizeDocumentMutation(documentId, ch))
}
if (isRawMutation(change)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SanityEncoder.decodeAll requires specific mutation format
return SanityEncoder.decodeAll([change] as any)[0] as Mutation
}
if (isTransaction(change)) {
return change
}
return isMutation(change) ? change : patch(documentId, change)
}
/**
* Normalize a mutation or a NodePatch to a document mutation
* @param path - The path the operation should be applied at
* @param change - The Mutation or NodePatch
*/
function normalizeNodeMutation(
path: Path,
change: Mutation | NodePatch | Operation | RawMutation | RawMutation[],
): (Mutation | NodePatch)[] | Mutation | NodePatch {
if (Array.isArray(change)) {
return change.flatMap((ch) => normalizeNodeMutation(path, ch))
}
if (isRawMutation(change)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SanityEncoder.decodeAll requires specific mutation format
return SanityEncoder.decodeAll([change] as any)[0] as Mutation
}
if (isNodePatch(change)) {
return at([...path, ...change.path], change.op)
}
return isOperation(change) ? at(path, change) : change
}
function migrateNodeType(
migration: NodeMigration,
value: JsonValue,
path: Path,
context: MigrationContext,
): NodeMigrationReturnValue | Promise<NodeMigrationReturnValue | void> | void {
switch (getValueType(value)) {
case 'array': {
return migration.array?.(value as JsonArray, path, context)
}
case 'boolean': {
return migration.boolean?.(value as boolean, path, context)
}
case 'null': {
return migration.null?.(value as null, path, context)
}
case 'number': {
return migration.number?.(value as number, path, context)
}
case 'object': {
return migration.object?.(value as JsonObject, path, context)
}
case 'string': {
return migration.string?.(value as string, path, context)
}
default: {
throw new Error('Unknown value type')
}
}
}