149 lines
4.4 KiB
TypeScript
149 lines
4.4 KiB
TypeScript
import wikibaseEdit from "wikibase-edit"
|
|
import wikibaseSDK, { SparqlResults } from "wikibase-sdk"
|
|
import fetch from "node-fetch"
|
|
import { config as loadDotEnv } from "dotenv"
|
|
import { bool, cleanEnv, num, str, url } from "envalid"
|
|
|
|
/** == TYPES == */
|
|
|
|
type ID = string
|
|
type GUID = `${ID}$${string}`
|
|
|
|
// The fields here will depend on the SELECT statement in incorrectStatementsQuery
|
|
interface QueryResultItem {
|
|
item: ID
|
|
statement: GUID
|
|
}
|
|
|
|
/** == HELPER FUNCTIONS == */
|
|
|
|
/** Generates a wikidata.org URL that points to the provided item, property, or statement */
|
|
function wikidataURL(item: ID, property?: ID | null, statement?: GUID) {
|
|
const url = new URL("https://www.wikidata.org")
|
|
const path = ["wiki", item]
|
|
url.pathname = path.join("/")
|
|
|
|
if (property) url.hash = property
|
|
if (statement) url.hash = statement
|
|
|
|
return url
|
|
}
|
|
|
|
async function fetchSparqlResults<TResult>(query: string) {
|
|
const url = wbk.sparqlQuery(query)
|
|
const results = (await fetch(url).then((res) => res.json())) as SparqlResults
|
|
const simplifiedResults = wbk.simplify.sparqlResults(results)
|
|
|
|
return simplifiedResults as TResult[]
|
|
}
|
|
|
|
/** Prints a summary of the completed operations. Called at the end of the script. */
|
|
function printStatistics(status: string) {
|
|
const items = editedItems.size
|
|
console.log(`${status}: Made ${edits} edits to ${items} items.`)
|
|
}
|
|
|
|
/** == LOAD AND VALIDATE ENVIRONMENT VARIABLES == */
|
|
|
|
loadDotEnv()
|
|
|
|
const env = cleanEnv(process.env, {
|
|
WIKIDATA_USERNAME: str(),
|
|
WIKIDATA_PASSWORD: str(),
|
|
WIKIDATA_INSTANCE: url(),
|
|
SPARQL_ENDPOINT: url(),
|
|
WIKIDATA_BOT: bool({ default: false }),
|
|
WIKIDATA_EDIT_SUMMARY: str(),
|
|
WIKIDATA_MAX_EDITS: num(),
|
|
DRY_RUN: bool({ default: false }),
|
|
})
|
|
|
|
/** == THE IMPORTANT CONFIGURATION == */
|
|
|
|
// The property that we are working with
|
|
const targetProperty = "P1641" // P1641 "port"
|
|
|
|
// The qualifier properties that we are working with
|
|
const oldQualifier = "P642" // P642 "of"
|
|
const newQualifier = "P2700" // P2700 "protocol"
|
|
|
|
// The SPARQL query for the statements that we need to modify
|
|
const incorrectStatementsQuery = `
|
|
SELECT ?item ?statement WHERE {
|
|
?item p:${targetProperty} ?statement.
|
|
?statement pq:${oldQualifier} ?protocol.
|
|
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
|
|
}
|
|
`
|
|
|
|
/** == WIKIDATA API INTERACTION == */
|
|
|
|
// Configure the wikibase-sdk API client
|
|
const wbk = wikibaseSDK({
|
|
instance: env.WIKIDATA_INSTANCE,
|
|
sparqlEndpoint: env.SPARQL_ENDPOINT,
|
|
})
|
|
|
|
// Configure the wikibase-edit API client
|
|
const wbEdit = wikibaseEdit({
|
|
instance: env.WIKIDATA_INSTANCE,
|
|
credentials: {
|
|
username: env.WIKIDATA_USERNAME,
|
|
password: env.WIKIDATA_PASSWORD,
|
|
},
|
|
bot: !!env.WIKIDATA_BOT,
|
|
summary: env.WIKIDATA_EDIT_SUMMARY,
|
|
})
|
|
|
|
// Get the list of statements that we'll edit
|
|
const statements = await fetchSparqlResults<QueryResultItem>(
|
|
incorrectStatementsQuery
|
|
)
|
|
const targetStatements = statements.slice(0, env.WIKIDATA_MAX_EDITS)
|
|
|
|
// Track some statistics to be printed at the end
|
|
const editedItems = new Set<ID>()
|
|
let edits = 0
|
|
|
|
for (const { item, statement } of targetStatements) {
|
|
try {
|
|
if (!env.DRY_RUN) {
|
|
// Swap the old qualifier for the new qualifier
|
|
await wbEdit.qualifier.move({
|
|
guid: statement,
|
|
oldProperty: oldQualifier,
|
|
newProperty: newQualifier,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
if (
|
|
error instanceof Error &&
|
|
error.message === "no qualifiers found for this property"
|
|
) {
|
|
// This error occurs when there's an statement in targetStatements that doesn't have the old qualifier.
|
|
// In other words, the property has qualifier already been updated since the query was run.
|
|
// This will happen if there are multiple qualifier triples that use the same propriety,
|
|
// since they both get updated at once when the script finds the first qualifier, leaving it with
|
|
// nothing to change when it tries to update the second qualifier.
|
|
// Could theoretically also be caused by someone removing the old qualifier during the script's runtime.
|
|
|
|
continue
|
|
}
|
|
|
|
printStatistics("Exited due to an error")
|
|
console.error(`🔥 Failed to edit ${item}. See below for error.`)
|
|
debugger
|
|
// Stop executing the script if we encounter an error
|
|
throw error
|
|
}
|
|
|
|
editedItems.add(item)
|
|
edits++
|
|
|
|
const url = wikidataURL(item, null, statement)
|
|
console.log(`✅ Updated ${item} (${url})`)
|
|
// debugger
|
|
}
|
|
|
|
printStatistics("🎉 Finished successfully")
|