wikibase-edit
Version:
Edit Wikibase from NodeJS
184 lines (160 loc) • 7.05 kB
text/typescript
import { omit } from 'lodash-es'
import { isEntityId, type EntityType, type Item, type Lexeme, type MediaInfo, type Property, datatypes, type Claims, type Statements } from 'wikibase-sdk'
import { newError } from '../error.js'
import { getEntityClaims } from '../get_entity.js'
import { arrayIncludes, forceArray, objectEntries } from '../utils.js'
import { formatClaims, formatSitelinks, formatTermsObject } from './format.js'
import { isIdAliasPattern, resolveIdAlias } from './id_alias.js'
import type { CreateEntityResponse } from './create.js'
import type { Reconciliation } from './validate_reconciliation_object.js'
import type { PropertiesDatatypes } from '../properties/fetch_properties_datatypes.js'
import type { AbsoluteUrl, BaseRevId } from '../types/common.js'
import type { SerializedConfig } from '../types/config.js'
import type { RawEditableEntity, RawEditableItem, RawEditableLexeme, RawEditableMediaInfo, RawEditableProperty, SimplifiedEditableClaims, SimplifiedEditableEntity } from '../types/edit_entity.js'
interface EditEntityParamsBase {
clear?: boolean
create?: boolean
reconciliation?: Reconciliation
summary?: string
baserevid?: BaseRevId
}
export type EditEntityRawModeParams = EditEntityParamsBase & Partial<RawEditableEntity> & { rawMode: true }
export type EditEntitySimplifiedModeParams = EditEntityParamsBase & Partial<SimplifiedEditableEntity>
export type EditEntityParams = EditEntityRawModeParams | EditEntitySimplifiedModeParams
const editableTypes = [
// 'form',
'item',
'lexeme',
'mediainfo',
'property',
// 'sense',
] as const
interface WbeditentityDataBase <T extends RawEditableEntity> {
type: T['type']
new?: T['type']
clear?: boolean
id?: T['id']
data: Partial<T>
}
type WbeditentityItemData = WbeditentityDataBase<RawEditableItem>
type WbeditentityPropertyData = WbeditentityDataBase<RawEditableProperty>
type WbeditentityLexemeData = WbeditentityDataBase<RawEditableLexeme>
type WbeditentityMediaInfoData = WbeditentityDataBase<RawEditableMediaInfo>
type WbeditentityData = WbeditentityItemData | WbeditentityPropertyData | WbeditentityLexemeData | WbeditentityMediaInfoData
export async function editEntity (inputParams: EditEntitySimplifiedModeParams, properties: PropertiesDatatypes, instance: AbsoluteUrl, config: SerializedConfig) {
return _editEntity(inputParams, properties, instance, config)
}
export async function _rawEditEntity (inputParams: EditEntityRawModeParams, properties: PropertiesDatatypes, instance: AbsoluteUrl, config: SerializedConfig) {
return _editEntity(inputParams, properties, instance, config)
}
async function _editEntity (inputParams: EditEntitySimplifiedModeParams | EditEntityRawModeParams, properties: PropertiesDatatypes, instance: AbsoluteUrl, config: SerializedConfig) {
validateParameters(inputParams)
let { id } = inputParams
const { create, type = 'item', clear, reconciliation } = inputParams
const datatype = 'datatype' in inputParams ? inputParams.datatype : undefined
const rawMode = 'rawMode' in inputParams ? inputParams.rawMode : false
if (!arrayIncludes(editableTypes, type)) {
throw newError('invalid entity type', { type })
}
let params: Partial<WbeditentityData> = { type, data: {} }
let existingClaims: Claims | Statements
if (create) {
if (type === 'property') {
if (!datatype) throw newError('missing property datatype', { datatype })
if (!datatypesSet.has(datatype)) {
throw newError('invalid property datatype', { datatype, knownDatatypes: datatypes })
}
params = {
...params,
new: 'property',
data: {
datatype,
},
} as WbeditentityPropertyData
} else {
if (datatype) {
throw newError("an item can't have a datatype", { datatype })
}
params.new = 'item'
}
} else if (isEntityId(id) || isIdAliasPattern(id)) {
if (isIdAliasPattern(id)) {
id = await resolveIdAlias(id, instance)
}
params.id = id
// @ts-expect-error
if (hasReconciliationSettings(reconciliation, inputParams.claims || inputParams.statements)) {
existingClaims = await getEntityClaims(id, config)
}
} else {
throw newError('invalid entity id', { id })
}
for (const [ attribute, types ] of objectEntries(attributesPerEntityType)) {
if (arrayIncludes(types, params.type)) {
if (attribute in inputParams && inputParams[attribute] != null) {
const inputParam = inputParams[attribute]
if (rawMode) {
params.data[attribute] = inputParam
} else {
if (attribute === 'claims' || attribute === 'statements') {
params.data[attribute] = formatClaims(inputParam, properties, instance, reconciliation, existingClaims)
} else {
params.data[attribute] = simplifiedInputFormatters[attribute](inputParam)
}
}
}
}
}
if (clear === true) params.clear = true
if (!clear && Object.keys(params.data).length === 0) {
throw newError('no data was passed', { id })
}
return {
action: 'wbeditentity',
data: {
...omit(params, 'type', 'data'),
// Stringify as it will be passed as form data
data: JSON.stringify(params.data),
},
}
}
// TODO: add support for lemmas, forms and senses
const attributesPerEntityType = {
aliases: [ 'item', 'property' ],
claims: [ 'item', 'property', 'lexeme' ],
descriptions: [ 'item', 'property', 'mediainfo' ],
labels: [ 'item', 'property', 'mediainfo' ],
statements: [ 'mediainfo' ],
sitelinks: [ 'item' ],
} satisfies Partial<Record<keyof Item | keyof Property | keyof Lexeme | keyof MediaInfo, EntityType[]>>
type HasSimplifiedInputFormatters = Exclude<keyof typeof attributesPerEntityType, 'claims' | 'statements'>
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const simplifiedInputFormatters: Record<HasSimplifiedInputFormatters, Function> = {
aliases: formatTermsObject.bind(null, 'alias'),
descriptions: formatTermsObject.bind(null, 'description'),
labels: formatTermsObject.bind(null, 'label'),
sitelinks: formatSitelinks,
}
const datatypesSet = new Set(datatypes)
const allowedParameters = new Set([
'id', 'create', 'type', 'datatype', 'clear', 'rawMode', 'summary', 'baserevid',
'labels', 'aliases', 'descriptions', 'claims', 'statements', 'sitelinks', 'reconciliation',
'origin',
])
function validateParameters (params: EditEntityParams) {
for (const parameter in params) {
if (!allowedParameters.has(parameter)) {
throw newError(`invalid parameter: ${parameter}`, 400, { parameter, allowedParameters, params })
}
}
}
function hasReconciliationSettings (reconciliation: Reconciliation, claims: SimplifiedEditableClaims) {
if (reconciliation != null) return true
for (const property in claims) {
for (const claim of forceArray(claims[property])) {
if (claim.reconciliation != null) return true
}
}
return false
}
export type EditEntityResponse = CreateEntityResponse