wikibase-edit
Version:
Edit Wikibase from NodeJS
143 lines (128 loc) • 6.05 kB
text/typescript
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))
}