UNPKG

@sanity/mutate

Version:

Experimental toolkit for working with Sanity mutations in JavaScript & TypeScript

317 lines (316 loc) 12.3 kB
import groupBy from "lodash/groupBy.js"; import { assignId, hasId, applyPatchMutation, applyNodePatch, applyPatches } from "./utils.js"; import { getAtPath } from "./getAtPath.js"; import { nanoid } from "nanoid"; import { stringifyPatches, makePatches } from "@sanity/diff-match-patch"; import { stringify, startsWith } from "./stringify.js"; import { uuid } from "@sanity/uuid"; function getMutationDocumentId(mutation) { if (mutation.type === "patch") return mutation.id; if (mutation.type === "create") return mutation.document._id; if (mutation.type === "delete") return mutation.id; if (mutation.type === "createIfNotExists" || mutation.type === "createOrReplace") return mutation.document._id; throw new Error("Invalid mutation type"); } function applyAll(current, mutation) { return mutation.reduce((doc, m) => { const res = applyDocumentMutation(doc, m); if (res.status === "error") throw new Error(res.message); return res.status === "noop" ? doc : res.after; }, current); } function applyDocumentMutation(document, mutation) { if (mutation.type === "create") return create(document, mutation); if (mutation.type === "createIfNotExists") return createIfNotExists(document, mutation); if (mutation.type === "delete") return del(document, mutation); if (mutation.type === "createOrReplace") return createOrReplace(document, mutation); if (mutation.type === "patch") return patch(document, mutation); throw new Error(`Invalid mutation type: ${mutation.type}`); } function create(document, mutation) { if (document) return { status: "error", message: "Document already exist" }; const result = assignId(mutation.document, nanoid); return { status: "created", id: result._id, after: result }; } function createIfNotExists(document, mutation) { return hasId(mutation.document) ? document ? { status: "noop" } : { status: "created", id: mutation.document._id, after: mutation.document } : { status: "error", message: "Cannot createIfNotExists on document without _id" }; } function createOrReplace(document, mutation) { return hasId(mutation.document) ? document ? { status: "updated", id: mutation.document._id, before: document, after: mutation.document } : { status: "created", id: mutation.document._id, after: mutation.document } : { status: "error", message: "Cannot createIfNotExists on document without _id" }; } function del(document, mutation) { return document ? mutation.id !== document._id ? { status: "error", message: "Delete mutation targeted wrong document" } : { status: "deleted", id: mutation.id, before: document, after: void 0 } : { status: "noop" }; } function patch(document, mutation) { if (!document) return { status: "error", message: "Cannot apply patch on nonexistent document" }; const next = applyPatchMutation(mutation, document); return document === next ? { status: "noop" } : { status: "updated", id: mutation.id, before: document, after: next }; } function applyMutations(mutations, documentMap, transactionId) { const updatedDocs = /* @__PURE__ */ Object.create(null); for (const mutation of mutations) { const documentId = getMutationDocumentId(mutation); if (!documentId) throw new Error("Unable to get document id from mutation"); const before = updatedDocs[documentId]?.after || documentMap.get(documentId), res = applyDocumentMutation(before, mutation); if (res.status === "error") throw new Error(res.message); res.status !== "noop" && (res.status === "updated" || res.status === "created" || res.status === "deleted") && (documentId in updatedDocs || (updatedDocs[documentId] = { before, after: void 0, muts: [] }), transactionId && (res.after._rev = transactionId), documentMap.set(documentId, res.after), updatedDocs[documentId].after = res.after, updatedDocs[documentId].muts.push(mutation)); } return Object.entries(updatedDocs).map(([id, { before, after, muts }]) => ({ id, status: after ? before ? "updated" : "created" : "deleted", mutations: muts, before, after })); } function takeUntilRight(arr, predicate, opts) { const result = []; for (const item of arr.slice().reverse()) { if (predicate(item)) return result; result.push(item); } return result.reverse(); } function isEqualPath(p1, p2) { return stringify(p1) === stringify(p2); } function supersedes(later, earlier) { return (earlier.type === "set" || earlier.type === "unset") && (later.type === "set" || later.type === "unset"); } function squashNodePatches(patches) { return compactSetIfMissingPatches( compactSetPatches(compactUnsetPatches(patches)) ); } function compactUnsetPatches(patches) { return patches.reduce( (earlierPatches, laterPatch) => { if (laterPatch.op.type !== "unset") return earlierPatches.push(laterPatch), earlierPatches; const unaffected = earlierPatches.filter( (earlierPatch) => !startsWith(laterPatch.path, earlierPatch.path) ); return unaffected.push(laterPatch), unaffected; }, [] ); } function compactSetPatches(patches) { return patches.reduceRight( (laterPatches, earlierPatch) => (laterPatches.find( (later) => supersedes(later.op, earlierPatch.op) && isEqualPath(later.path, earlierPatch.path) ) || laterPatches.unshift(earlierPatch), laterPatches), [] ); } function compactSetIfMissingPatches(patches) { return patches.reduce( (previousPatches, laterPatch) => laterPatch.op.type !== "setIfMissing" ? (previousPatches.push(laterPatch), previousPatches) : (takeUntilRight( previousPatches, (patch2) => patch2.op.type === "unset" ).find( (precedingPatch) => precedingPatch.op.type === "setIfMissing" && isEqualPath(precedingPatch.path, laterPatch.path) ) || previousPatches.push(laterPatch), previousPatches), [] ); } function compactDMPSetPatches(base, patches) { let edge = base; return patches.reduce((previousPatches, patch2) => { const before = edge; if (edge = applyNodePatch(patch2, edge), patch2.op.type === "set" && typeof patch2.op.value == "string") { const current = getAtPath(patch2.path, before); if (typeof current == "string") { const replaced = { ...patch2, op: { type: "diffMatchPatch", value: stringifyPatches(makePatches(current, patch2.op.value)) } }; return previousPatches.flatMap((ep) => isEqualPath(ep.path, patch2.path) && ep.op.type === "diffMatchPatch" ? [] : ep).concat(replaced); } } return previousPatches.push(patch2), previousPatches; }, []); } function squashDMPStrings(base, mutationGroups) { return mutationGroups.map((mutationGroup) => ({ ...mutationGroup, mutations: dmpIfyMutations(base, mutationGroup.mutations) })); } function dmpIfyMutations(store, mutations) { return mutations.map((mutation, i) => { if (mutation.type !== "patch") return mutation; const base = store.get(mutation.id); return base ? dmpifyPatchMutation(base, mutation) : mutation; }); } function dmpifyPatchMutation(base, mutation) { return { ...mutation, patches: compactDMPSetPatches(base, mutation.patches) }; } function mergeMutationGroups(mutationGroups) { return chunkWhile(mutationGroups, (group) => !group.transaction).flatMap( (chunk) => ({ ...chunk[0], mutations: chunk.flatMap((c) => c.mutations) }) ); } function chunkWhile(arr, predicate) { const res = []; let currentChunk = []; return arr.forEach((item) => { predicate(item) ? currentChunk.push(item) : (currentChunk.length > 0 && res.push(currentChunk), currentChunk = [], res.push([item])); }), currentChunk.length > 0 && res.push(currentChunk), res; } function squashMutationGroups(staged) { return mergeMutationGroups(staged).map((transaction) => ({ ...transaction, mutations: squashMutations(transaction.mutations) })).map((transaction) => ({ ...transaction, mutations: transaction.mutations.map((mutation) => mutation.type !== "patch" ? mutation : { ...mutation, patches: squashNodePatches(mutation.patches) }) })); } function squashMutations(mutations) { const byDocument = groupBy(mutations, getMutationDocumentId); return Object.values(byDocument).flatMap((documentMutations) => squashCreateIfNotExists(squashDelete(documentMutations)).flat().reduce((acc, docMutation) => { const prev = acc[acc.length - 1]; return (!prev || prev.type === "patch") && docMutation.type === "patch" ? acc.slice(0, -1).concat({ ...docMutation, patches: (prev?.patches || []).concat(docMutation.patches) }) : acc.concat(docMutation); }, [])); } function squashCreateIfNotExists(mutations) { return mutations.length === 0 ? mutations : mutations.reduce((previousMuts, laterMut) => laterMut.type !== "createIfNotExists" ? (previousMuts.push(laterMut), previousMuts) : (takeUntilRight(previousMuts, (m) => m.type === "delete").find( (precedingPatch) => precedingPatch.type === "createIfNotExists" ) || previousMuts.push(laterMut), previousMuts), []); } function squashDelete(mutations) { return mutations.length === 0 ? mutations : mutations.reduce((previousMuts, laterMut) => laterMut.type === "delete" ? [laterMut] : (previousMuts.push(laterMut), previousMuts), []); } function rebase(documentId, oldBase, newBase, localMutations) { let edge = oldBase; const dmpified = localMutations.map((transaction) => { const mutations = transaction.mutations.flatMap((mut) => { if (getMutationDocumentId(mut) !== documentId) return []; const before = edge; return edge = applyAll(edge, [mut]), !before || mut.type !== "patch" ? mut : { type: "dmpified", mutation: { ...mut, // Todo: make compactDMPSetPatches return pairs of patches that was dmpified with their // original as dmpPatches and original is not 1:1 (e..g some of the original may not be dmpified) dmpPatches: compactDMPSetPatches(before, mut.patches), original: mut.patches } }; }); return { ...transaction, mutations }; }); let newBaseWithDMPForOldBaseApplied = newBase; return dmpified.map((transaction) => { const applied = []; return transaction.mutations.forEach((mut) => { if (mut.type === "dmpified") try { newBaseWithDMPForOldBaseApplied = applyPatches( mut.mutation.dmpPatches, newBaseWithDMPForOldBaseApplied ), applied.push(mut); } catch { console.warn("Failed to apply dmp patch, falling back to original"); try { newBaseWithDMPForOldBaseApplied = applyPatches( mut.mutation.original, newBaseWithDMPForOldBaseApplied ), applied.push(mut); } catch (second) { throw new Error( `Failed to apply patch for document "${documentId}": ${second.message}`, { cause: second } ); } } else newBaseWithDMPForOldBaseApplied = applyAll( newBaseWithDMPForOldBaseApplied, [mut] ); }); }), [localMutations.map((transaction) => ({ ...transaction, mutations: transaction.mutations.map((mut) => mut.type !== "patch" || getMutationDocumentId(mut) !== documentId ? mut : { ...mut, patches: mut.patches.map((patch2) => patch2.op.type !== "set" ? patch2 : { ...patch2, op: { ...patch2.op, value: getAtPath(patch2.path, newBaseWithDMPForOldBaseApplied) } }) }) })), newBaseWithDMPForOldBaseApplied]; } function createTransactionId() { return uuid(); } function toTransactions(groups) { return groups.map((group) => group.transaction && group.id !== void 0 ? { id: group.id, mutations: group.mutations } : { id: createTransactionId(), mutations: group.mutations }); } export { applyAll, applyMutations, createTransactionId, getMutationDocumentId, rebase, squashDMPStrings, squashMutationGroups, toTransactions }; //# sourceMappingURL=toTransactions.js.map