UNPKG

@sanity/transaction-collator

Version:

Collate events from a series of transactions

244 lines (218 loc) 8.29 kB
import {isEqual, uniq} from 'lodash' import {EventType, HistoryEvent, Transaction, Mutation} from './types' import {ndjsonToArray} from './utils/ndjsonToArray' const EDIT_EVENT_TIME_TRESHHOLD_MS = 1000 * 60 * 5 // 5 minutes export function transactionsToEvents( documentIds: string[], transactions: string | Buffer | Transaction[] ): HistoryEvent[] { const rawItems = Array.isArray(transactions) ? transactions : ndjsonToArray(transactions) return ( rawItems // Make sure we only deal with transactions that are relevant for our documents .filter( (transaction: Transaction) => transaction.documentIDs && transaction.documentIDs.some((id) => documentIds.includes(id)) ) // ensure transactions are sorted by time .sort(compareTimestamp) // Turn a transaction into a classified HistoryEvent .map((transaction, index) => mapToEvents(transaction, documentIds, index)) // Chunk and group edit events .reduce(reduceEdits, []) // Manipulate truncation events to be able to restore to published version .reduce(createReduceTruncatedFn(), []) ) } function mapToEvents(transaction: Transaction, documentIds: string[], index = 0): HistoryEvent { const {type, documentId} = mutationsToEventTypeAndDocumentId( filterRelevantMutations(transaction.mutations, documentIds), index ) const timestamp = transaction.timestamp const userIds = findUserIds(transaction, type) return { type, documentIDs: transaction.documentIDs, displayDocumentId: documentId, rev: transaction.id, userIds, transactionIds: [transaction.id], startTime: timestamp, endTime: timestamp, } } function reduceEdits( acc: HistoryEvent[], current: HistoryEvent, index: number, arr: HistoryEvent[] ) { const nextEvent = arr[index + 1] const skipEvent = current.type === 'edited' && nextEvent && nextEvent.type === 'edited' && new Date(nextEvent.endTime).getTime() - new Date(current.endTime).getTime() < EDIT_EVENT_TIME_TRESHHOLD_MS && isEqual(current.documentIDs, nextEvent.documentIDs) if (skipEvent) { // Lift authors over to next event nextEvent.userIds = uniq(nextEvent.userIds.concat(current.userIds)) // Lift list of transactions over to next event nextEvent.transactionIds = uniq(current.transactionIds.concat(nextEvent.transactionIds)) // Set startTime on next event to be this one if not done already // (then startTime and endTime would be different) if (current.startTime === current.endTime) { nextEvent.startTime = current.startTime } } else { acc.push(current) } return acc } function createReduceTruncatedFn() { let truncated: HistoryEvent[] | undefined return (acc: HistoryEvent[], current: HistoryEvent, index: number, arr: HistoryEvent[]) => { truncated = truncated || arr.filter((event) => event.type === 'truncated') if (!truncated.includes(current)) { acc.push(current) } if (index === arr.length - 1) { const draftTruncationEvent = truncated.find( (evt) => !!evt.displayDocumentId && evt.displayDocumentId.startsWith('drafts.') ) const publishedTruncationEvent = truncated.find( (evt) => !!evt.displayDocumentId && !evt.displayDocumentId.startsWith('drafts.') ) if (draftTruncationEvent && publishedTruncationEvent) { acc.unshift({...draftTruncationEvent, type: 'edited'}) acc.unshift(publishedTruncationEvent) } else if (publishedTruncationEvent) { acc.unshift(publishedTruncationEvent) } else if (draftTruncationEvent) { acc.unshift(draftTruncationEvent) } } return acc } } export function mutationsToEventTypeAndDocumentId( mutations: Mutation[], transactionIndex: number ): {type: EventType; documentId: string | null} { const withoutPatches = mutations.filter((mut) => mut.patch === undefined) const createOrReplaceMutation = withoutPatches.find((mut) => mut.createOrReplace !== undefined) const createOrReplacePatch = createOrReplaceMutation && createOrReplaceMutation.createOrReplace const createMutation = withoutPatches.find((mut) => mut.create !== undefined) const createPatch = createMutation && createMutation.create const createIfNotExistsMutation = withoutPatches.find( (mut) => mut.createIfNotExists !== undefined ) const createIfNotExistsPatch = createIfNotExistsMutation && createIfNotExistsMutation.createIfNotExists const deleteMutation = withoutPatches.find((mut) => mut.delete !== undefined) const deletePatch = deleteMutation && deleteMutation.delete const squashedMutation = withoutPatches.find((mut) => mut.createSquashed !== undefined) const squashedPatch = squashedMutation && squashedMutation.createSquashed const createValue = createOrReplacePatch || createPatch || createIfNotExistsPatch // Created if (transactionIndex === 0) { const type = 'created' if (createOrReplacePatch) { return {type, documentId: createOrReplacePatch._id} } if (createIfNotExistsPatch) { return {type, documentId: createIfNotExistsPatch._id} } if (createPatch) { return {type, documentId: createPatch._id} } } // (re) created if (transactionIndex > 0 && mutations.length === 1 && createIfNotExistsPatch) { const type = createIfNotExistsPatch._id.startsWith('.draft') ? 'edited' : 'published' return {type, documentId: createIfNotExistsPatch._id} } // Published if ( (createOrReplacePatch || createPatch || createIfNotExistsPatch) && deletePatch && deletePatch.id.startsWith('drafts.') ) { return { type: 'published', documentId: (createValue && createValue._id) || null, } } // Unpublished if ( withoutPatches.length === 2 && (createIfNotExistsPatch || createPatch) && deletePatch && !deletePatch.id.startsWith('drafts.') ) { return { type: 'unpublished', documentId: (createValue && createValue._id) || null, } } // Restored to previous version if ( (createOrReplacePatch && createOrReplacePatch._id.startsWith('drafts.')) || (createPatch && createPatch._id.startsWith('drafts.')) || (createIfNotExistsPatch && createIfNotExistsPatch._id.startsWith('drafts.')) ) { return { type: 'edited', documentId: (createValue && createValue._id) || null, } } // Discard drafted changes if (mutations.length === 1 && deletePatch && deletePatch.id.startsWith('drafts.')) { return {type: 'discardDraft', documentId: deletePatch.id.replace('drafts.', '')} } // Truncated history if (mutations.length === 1 && squashedPatch) { return {type: 'truncated', documentId: squashedPatch.document._id} } // Deleted if (mutations.every((mut) => mut.delete !== undefined)) { return {type: 'deleted', documentId: null} } // Edited const patchedMutation = mutations.find((mut) => mut.patch !== undefined) if (patchedMutation && patchedMutation.patch) { return {type: 'edited', documentId: patchedMutation.patch.id} } // Edited (createOrReplace) if (createOrReplacePatch) { return {type: 'edited', documentId: createOrReplacePatch._id} } return {type: 'unknown', documentId: null} } function findUserIds(transaction: Transaction, type: EventType): string[] { // The truncated event is kind of special if (type === 'truncated') { const createSquasedMut = transaction.mutations.find((mut) => mut.createSquashed !== undefined) const createSquasedPatch = createSquasedMut && createSquasedMut.createSquashed if (createSquasedPatch) { return createSquasedPatch.authors } } // Default is to return the transaction author return [transaction.author] } function compareTimestamp(a: Transaction, b: Transaction) { return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() } function filterRelevantMutations(mutations: Mutation[], documentIds: string[]) { return mutations.filter((mut) => { return Object.keys(mut) .map((key) => { const val = (mut as any)[key] return val.id || val._id || (val.document && val.document._id) || false }) .some((id) => id && documentIds.includes(id)) }) }