UNPKG

wikibase-edit

Version:

Edit Wikibase from NodeJS

143 lines (128 loc) 6.05 kB
import { isEqual } from 'lodash-es' import { simplifyReferences, simplifySnak, type PropertyClaims, type PropertyStatements } from 'wikibase-sdk' import { isMatchingClaimFactory } from '../claim/is_matching_claim.js' import { isMatchingSnak } from '../claim/is_matching_snak.js' import { newError } from '../error.js' import { validateReconciliationObject, type Reconciliation } from './validate_reconciliation_object.js' import type { CustomSimplifiedEditableClaim } from '../types/edit_entity.js' // Ignoring MediaInfo statements weirdness here, as it doesn't rely on snaks datatypes export function reconcileClaimFactory (reconciliation: Reconciliation, existingPropertyClaims: PropertyClaims | PropertyStatements) { return function reconcileClaim (claim: CustomSimplifiedEditableClaim): CustomSimplifiedEditableClaim | CustomSimplifiedEditableClaim[] | void { reconciliation = 'reconciliation' in claim ? claim.reconciliation : reconciliation if (!reconciliation) return claim validateReconciliationObject(reconciliation, claim) const { mode, matchingQualifiers, matchingReferences } = reconciliation if (mode === 'skip-on-any-value') { if (existingPropertyClaims.length > 0) { console.warn(`[wikibase-edit] skipping claim: a claim already exists for that property\n${JSON.stringify({ claim, existingPropertyClaims })}`) return } } const existingClaims = existingPropertyClaims.filter(isMatchingClaimFactory(claim, matchingQualifiers)) if (claim.remove) { if (existingClaims.length > 0) { return existingClaims.map(({ id }) => ({ id, remove: true })) } else { throw newError("can't remove claim: claim not found", claim) } } if (existingClaims.length === 0) return claim if (mode === 'skip-on-value-match') { console.warn(`[wikibase-edit] skipping claim: a similar claim already exists\n${JSON.stringify({ claim, existingClaims })}`) } else if (mode === 'merge') { if (existingClaims.length > 1) { throw newError('too many matching claims found', { claim, existingClaims }) } const existingClaim = existingClaims[0] if (claim.qualifiers != null) { existingClaim.qualifiers = existingClaim.qualifiers || {} addMissingQualifiers(existingClaim.qualifiers, claim.qualifiers) } if (claim.references != null) { existingClaim.references = existingClaim.references || [] if (matchingReferences) { mergeReferences(existingClaim.references, claim.references, matchingReferences) } else { const currentReference = simplifyReferences(existingClaim.references) // @ts-expect-error const newReferenceReference = claim.references.filter(isNewReference(currentReference)) existingClaim.references.push(...newReferenceReference) } } // @ts-expect-error return existingClaim } else { throw newError('unexpected reconciliation mode', 500, { reconciliation }) } } } function addMissingQualifiers (existingQualifiers, newQualifiers) { for (const property in newQualifiers) { existingQualifiers[property] = existingQualifiers[property] || [] existingQualifiers[property].push(...newQualifiers[property]) } } const isNewReference = currentReference => reference => { const simplifiedReference = aggregateReferenceSnaks(reference.snaks, true) return !currentReference.some(currentReference => { return isEqual(currentReference, simplifiedReference) }) } function mergeReferences (existingReferences, newReferences, matchingReferences) { const addedReferences = [] for (const newReference of newReferences) { const newReferenceSnaks = aggregateReferenceSnaks(newReference.snaks, false) const matchingReference = existingReferences.find(isMatchingReference(newReferenceSnaks, matchingReferences)) if (matchingReference) { mergeMatchingReference(matchingReference, newReferenceSnaks) } else { addedReferences.push(newReference) } } existingReferences.push(...addedReferences) } function mergeMatchingReference (matchingReference, newReferenceSnaks) { for (const property in newReferenceSnaks) { if (property in matchingReference.snaks) { const existingPropertySnaks = matchingReference.snaks[property] const newSnaks = newReferenceSnaks[property] .filter(snak => !hasSomeMatch(existingPropertySnaks, snak)) matchingReference.snaks[property].push(...newSnaks) } else { matchingReference.snaks[property] = newReferenceSnaks[property] } } } const isMatchingReference = (newReferenceSnaks, matchingReferences) => existingReference => { const existingReferenceSnaks = existingReference.snaks for (const property of matchingReferences) { const [ pid, option = 'all' ] = property.split(':') const newPropertySnaks = newReferenceSnaks[pid] const existingPropertySnaks = existingReferenceSnaks[pid] if (newPropertySnaks == null && existingPropertySnaks == null) return false if (newPropertySnaks != null && existingPropertySnaks == null) return false if (newPropertySnaks == null && existingPropertySnaks != null) return false const methodName = methodNameByOption[option] const everyNewSnakHasAnExistingMatch = newPropertySnaks[methodName](snak => { return hasSomeMatch(existingPropertySnaks, snak) }) if (!everyNewSnakHasAnExistingMatch) return false } return true } const methodNameByOption = { any: 'some', all: 'every', } function aggregateReferenceSnaks (snaksArray, simplify) { return snaksArray.reduce((aggregatedSnaks, snak) => { const { property } = snak aggregatedSnaks[property] = aggregatedSnaks[property] || [] if (simplify) snak = simplifySnak(snak, {}) aggregatedSnaks[property].push(snak) return aggregatedSnaks }, {}) } function hasSomeMatch (existingPropertySnaks, snak) { return existingPropertySnaks.some(existingSnak => isMatchingSnak(existingSnak, snak)) }