diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1f291800..a95b0048 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -11,16 +11,18 @@ module.exports = { plugins: [ '@typescript-eslint', ], + parserOptions: { + project: './tsconfig.json' + }, extends: [ // See https://github.com/standard/eslint-config-standard/blob/master/eslintrc.json - 'standard', + 'standard-with-typescript', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', ], rules: { 'array-bracket-spacing': [ 'error', 'always' ], 'arrow-parens': [ 'error', 'as-needed' ], - 'comma-dangle': 'off', '@typescript-eslint/comma-dangle': [ 'error', 'always-multiline' ], eqeqeq: [ 'error', 'smart' ], 'implicit-arrow-linebreak': [ 'error', 'beside' ], @@ -36,28 +38,26 @@ module.exports = { alphabetize: { order: 'asc' }, }, ], - 'indent': 'off', '@typescript-eslint/indent': [ 'error', 2, { MemberExpression: 'off' } ], 'no-var': [ 'error' ], - // Primarily to avoid false positive with interfaces declarations - // See https://github.com/typescript-eslint/typescript-eslint/issues/1262 - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': 'off', 'nonblock-statement-body-position': [ 'error', 'beside' ], - 'object-curly-spacing': 'off', - '@typescript-eslint/object-curly-spacing': [ 'error', 'always' ], 'object-shorthand': [ 'error', 'properties' ], 'prefer-arrow-callback': [ 'error' ], - 'prefer-const': [ 'error' ], 'prefer-rest-params': 'off', + + // TODO remove (= enable these lints and fix the issues) '@typescript-eslint/ban-ts-comment': [ 'error', { 'ts-expect-error': false, // TODO: "allow-with-description", 'ts-nocheck': false, } ], - '@typescript-eslint/consistent-type-imports': [ 'error', { prefer: 'type-imports' } ], + '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'off', - semi: 'off', - '@typescript-eslint/semi': [ 'error', 'never' ], + '@typescript-eslint/restrict-plus-operands': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + + // Enable when strict or strictNullChecks is enabled in tsconfig + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', }, globals: { // Mocha globals diff --git a/package-lock.json b/package-lock.json index 41a18c09..f22f3ca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@typescript-eslint/parser": "^5.49.0", "@vercel/git-hooks": "^1.0.0", "eslint": "^8.32.0", - "eslint-config-standard": "^17.0.0", + "eslint-config-standard-with-typescript": "^34.0.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-n": "^15.6.1", "eslint-plugin-promise": "^6.1.1", @@ -1189,6 +1189,24 @@ "eslint-plugin-promise": "^6.0.0" } }, + "node_modules/eslint-config-standard-with-typescript": { + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-34.0.0.tgz", + "integrity": "sha512-zhCsI4/A0rJ1ma8sf3RLXYc0gc7yPmdTWRVXMh9dtqeUx3yBQyALH0wosHhk1uQ9QyItynLdNOtcHKNw8G7lQw==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "typescript": "*" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", @@ -4750,6 +4768,16 @@ "dev": true, "requires": {} }, + "eslint-config-standard-with-typescript": { + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-34.0.0.tgz", + "integrity": "sha512-zhCsI4/A0rJ1ma8sf3RLXYc0gc7yPmdTWRVXMh9dtqeUx3yBQyALH0wosHhk1uQ9QyItynLdNOtcHKNw8G7lQw==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + } + }, "eslint-import-resolver-node": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", diff --git a/package.json b/package.json index b7ba87db..b5ff0d96 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@typescript-eslint/parser": "^5.49.0", "@vercel/git-hooks": "^1.0.0", "eslint": "^8.32.0", - "eslint-config-standard": "^17.0.0", + "eslint-config-standard-with-typescript": "^34.0.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-n": "^15.6.1", "eslint-plugin-promise": "^6.1.1", diff --git a/scripts/compare_datatypes.ts b/scripts/compare_datatypes.ts index 3f6927a5..0980f63b 100755 --- a/scripts/compare_datatypes.ts +++ b/scripts/compare_datatypes.ts @@ -1,11 +1,10 @@ #!/usr/bin/env ts-node import { kebabCase } from 'lodash-es' import { red, green } from 'tiny-chalk' -import { parsers } from '../src/helpers/parse_claim.js' +import { DataTypes } from '../src/types/claim.js' +import { isOfType } from '../src/utils/utils.js' import { readJsonFile } from '../tests/lib/utils.js' -const supportedTypes = Object.keys(parsers) - const allDatatypes = readJsonFile('/tmp/all_wikidata_datatypes.json') as string[] allDatatypes .map(typeUri => { @@ -15,7 +14,7 @@ allDatatypes return kebabCase(typeName) }) .forEach(type => { - if (supportedTypes.includes(type)) { + if (isOfType(DataTypes, type)) { console.log(green('ok'), type) } else { console.error(red('unsupported type'), type) diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index f1d04530..870e714e 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -1,4 +1,3 @@ -import { wikibaseTimeToDateObject as toDateObject } from './wikibase_time_to_date_object.js' import type { EntityId, EntityPageTitle, @@ -23,7 +22,7 @@ function isIdBuilder (regex: { readonly source: string, readon } export const isNumericId = isIdBuilder(/^[1-9][0-9]*$/) -export const isEntityId = isIdBuilder(/^((Q|P|L|M)[1-9][0-9]*|L[1-9][0-9]*-(F|S)[1-9][0-9]*)$/) +export const isEntityId = isIdBuilder(/^((Q|P|L|M|E)[1-9][0-9]*|L[1-9][0-9]*-(F|S)[1-9][0-9]*)$/) export const isEntitySchemaId = isIdBuilder(/^E[1-9][0-9]*$/) export const isItemId = isIdBuilder(/^Q[1-9][0-9]*$/) export const isPropertyId = isIdBuilder(/^P[1-9][0-9]*$/) @@ -31,10 +30,10 @@ export const isLexemeId = isIdBuilder(/^L[1-9][0-9]*$/) export const isFormId = isIdBuilder(/^L[1-9][0-9]*-F[1-9][0-9]*$/) export const isSenseId = isIdBuilder(/^L[1-9][0-9]*-S[1-9][0-9]*$/) export const isMediaInfoId = isIdBuilder(/^M[1-9][0-9]*$/) -export const isGuid = isIdBuilder(/^((Q|P|L|M)[1-9][0-9]*|L[1-9][0-9]*-(F|S)[1-9][0-9]*)\$[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) +export const isGuid = isIdBuilder(/^((Q|P|L|M|E)[1-9][0-9]*|L[1-9][0-9]*-(F|S)[1-9][0-9]*)\$[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) export const isHash = isIdBuilder(/^[0-9a-f]{40}$/) export const isRevisionId = isIdBuilder(/^\d+$/) -export const isNonNestedEntityId = isIdBuilder(/^(Q|P|L|M)[1-9][0-9]*$/) +export const isNonNestedEntityId = isIdBuilder(/^(Q|P|L|M|E)[1-9][0-9]*$/) export function isPropertyClaimsId (id: string): id is PropertyClaimsId { if (typeof id !== 'string') return false @@ -62,80 +61,9 @@ export function isEntityPageTitle (title: string): title is EntityPageTitle { export function getNumericId (id: string): NumericId { if (!isNonNestedEntityId(id)) throw new Error(`invalid entity id: ${id}`) - return id.replace(/^(Q|P|L|M)/, '') as NumericId + return id.replace(/^(Q|P|L|M|E)/, '') as NumericId } -export interface WikibaseTimeObject { - time: string - precision: number -} - -export type TimeInputValue = string | WikibaseTimeObject - -type TimeFunction = (wikibaseTime: TimeInputValue) => T - -// Try to parse the date or return the input -function bestEffort (fn: TimeFunction) { - return (value: TimeInputValue) => { - try { - return fn(value) - } catch { - value = typeof value === 'string' ? value : value.time - - const sign = value[0] - let [ yearMonthDay, withinDay ] = value.slice(1).split('T') - if (!sign || !yearMonthDay || !withinDay) { - throw new Error('TimeInput is invalid: ' + JSON.stringify(value)) - } - - yearMonthDay = yearMonthDay.replace(/-00/g, '-01') - - return `${sign}${yearMonthDay}T${withinDay}` - } - } -} - -const toEpochTime = (wikibaseTime: TimeInputValue) => toDateObject(wikibaseTime).getTime() -const toISOString = (wikibaseTime: TimeInputValue) => toDateObject(wikibaseTime).toISOString() - -// A date format that knows just three precisions: -// 'yyyy', 'yyyy-mm', and 'yyyy-mm-dd' (including negative and non-4 digit years) -// Should be able to handle the old and the new Wikidata time: -// - in the old one, units below the precision where set to 00 -// - in the new one, those months and days are set to 01 in those cases, -// so when we can access the full claim object, we check the precision -// to recover the old format -const toSimpleDay = (wikibaseTime: TimeInputValue): string => { - // Also accept claim datavalue.value objects, and actually prefer those, - // as we can check the precision - if (typeof wikibaseTime === 'object') { - const { time, precision } = wikibaseTime - // Year precision - if (precision === 9) wikibaseTime = time.replace('-01-01T', '-00-00T') - // Month precision - else if (precision === 10) wikibaseTime = time.replace('-01T', '-00T') - else wikibaseTime = time - } - - return wikibaseTime.split('T')[0] - // Remove positive years sign - .replace(/^\+/, '') - // Remove years padding zeros - .replace(/^(-?)0+/, '$1') - // Remove days if not included in the Wikidata date precision - .replace(/-00$/, '') - // Remove months if not included in the Wikidata date precision - .replace(/-00$/, '') -} - -export const wikibaseTimeToEpochTime = bestEffort(toEpochTime) - -export const wikibaseTimeToISOString = bestEffort(toISOString) - -export const wikibaseTimeToSimpleDay = bestEffort(toSimpleDay) - -export const wikibaseTimeToDateObject = toDateObject - export function getImageUrl (filename: string, width?: number): Url { let url = `https://commons.wikimedia.org/wiki/Special:FilePath/${filename}` if (typeof width === 'number') url += `?width=${width}` diff --git a/src/helpers/parse_claim.ts b/src/helpers/parse_claim.ts index a20afd01..ee845cd5 100644 --- a/src/helpers/parse_claim.ts +++ b/src/helpers/parse_claim.ts @@ -1,13 +1,18 @@ -import { wikibaseTimeToISOString, wikibaseTimeToEpochTime, wikibaseTimeToSimpleDay } from './helpers.js' -import type { TimeInputValue } from './helpers.js' +import { convertTime, type TimeConverter, type TimeConverterFn } from './wikibase_time.js' +import type { DataType } from '../types/claim.js' +import type { SnakEntityValue, SnakGlobeCoordinateValue, SnakMonolingualTextValue, SnakQuantityValue, SnakStringValue, SnakTimeValue, SnakValue } from '../types/snakvalue.js' -const simple = datavalue => datavalue.value +const simple = (datavalue: { readonly value: T }): T => datavalue.value -const monolingualtext = (datavalue, options) => { +const monolingualtext = (datavalue: SnakMonolingualTextValue, options: { readonly keepRichValues?: boolean } = {}) => { return options.keepRichValues ? datavalue.value : datavalue.value.text } -const entity = (datavalue, options) => prefixedId(datavalue, options.entityPrefix) +interface SimplifyEntitySnakOptions { + readonly entityPrefix?: string +} + +const entity = (datavalue: SnakEntityValue, options: SimplifyEntitySnakOptions = {}) => prefixedId(datavalue, options.entityPrefix) const entityLetter = { item: 'Q', @@ -15,17 +20,24 @@ const entityLetter = { property: 'P', } as const -const prefixedId = (datavalue, prefix) => { +const prefixedId = (datavalue: SnakEntityValue, prefix: string | undefined) => { const { value } = datavalue - const id = value.id || entityLetter[value['entity-type']] + value['numeric-id'] + const id = 'id' in value ? value.id : (entityLetter[value['entity-type']] + value['numeric-id']) return typeof prefix === 'string' ? `${prefix}:${id}` : id } -const quantity = (datavalue, options) => { +interface SimplifiedQuantity { + amount: number + unit: string + upperBound?: number + lowerBound?: number +} + +const quantity = (datavalue: SnakQuantityValue, options: { readonly keepRichValues?: boolean } = {}) => { const { value } = datavalue const amount = parseFloat(value.amount) if (options.keepRichValues) { - const richValue: any = { + const richValue: SimplifiedQuantity = { amount: parseFloat(value.amount), // ex: http://www.wikidata.org/entity/ unit: value.unit.replace(/^https?:\/\/.*\/entity\//, ''), @@ -38,7 +50,7 @@ const quantity = (datavalue, options) => { } } -const coordinate = (datavalue, options) => { +const coordinate = (datavalue: SnakGlobeCoordinateValue, options: { readonly keepRichValues?: boolean } = {}) => { if (options.keepRichValues) { return datavalue.value } else { @@ -46,72 +58,85 @@ const coordinate = (datavalue, options) => { } } -const time = (datavalue, options) => { - let timeValue - if (typeof options.timeConverter === 'function') { - timeValue = options.timeConverter(datavalue.value) - } else { - timeValue = getTimeConverter(options.timeConverter)(datavalue.value) - } +interface SimplifyTimeSnakOptions { + readonly keepRichValues?: boolean + readonly timeConverter?: TimeConverterFn | TimeConverter +} +const time = (datavalue: SnakTimeValue, options: SimplifyTimeSnakOptions = {}) => { + const timeValue = convertTime(options.timeConverter, datavalue.value) if (options.keepRichValues) { - const { timezone, before, after, precision, calendarmodel } = datavalue.value - return { time: timeValue, timezone, before, after, precision, calendarmodel } + return { ...datavalue.value, time: timeValue } } else { return timeValue } } -const getTimeConverter = (key = 'iso') => { - const converter = timeConverters[key] - if (!converter) throw new Error(`invalid converter key: ${JSON.stringify(key).substring(0, 100)}`) - return converter -} +type SimplifySnakOptions = SimplifyTimeSnakOptions & SimplifyEntitySnakOptions -// Each time converter should be able to accept 2 keys of arguments: -// - either datavalue.value objects (prefered as it gives access to the precision) -// - or the time string (datavalue.value.time) -export const timeConverters = { - iso: wikibaseTimeToISOString, - epoch: wikibaseTimeToEpochTime, - 'simple-day': wikibaseTimeToSimpleDay, - none: (wikibaseTime: TimeInputValue) => typeof wikibaseTime === 'string' ? wikibaseTime : wikibaseTime.time, -} as const +export function parseClaim ( + datatype: DataType | undefined, + datavalue: SnakValue, + options: SimplifySnakOptions, + claimId: string, +) { + // Known case of missing datatype: form.claims, sense.claims + // datavalue.type is used then -export const parsers = { - commonsMedia: simple, - 'external-id': simple, - 'geo-shape': simple, - 'globe-coordinate': coordinate, - math: simple, - monolingualtext, - 'musical-notation': simple, - quantity, - string: simple, - 'tabular-data': simple, - time, - url: simple, - 'wikibase-entityid': entity, - 'wikibase-form': entity, - 'wikibase-item': entity, - 'wikibase-lexeme': entity, - 'wikibase-property': entity, - 'wikibase-sense': entity, -} as const + // @ts-expect-error known case requiring this: legacy "musical notation" datatype + datatype = datatype?.replace(' ', '-') -export function parseClaim (datatype, datavalue, options, claimId) { - // Known case of missing datatype: form.claims, sense.claims - datatype = datatype || datavalue.type - // Known case requiring this: legacy "muscial notation" datatype - datatype = datatype.replace(' ', '-') - - try { - return parsers[datatype](datavalue, options) - } catch (err) { - if (err.message === 'parsers[datatype] is not a function') { - err.message = `${datatype} claim parser isn't implemented - Claim id: ${claimId} - Please report to https://github.com/maxlath/wikibase-sdk/issues` - } - throw err + if ( + datatype === 'wikibase-form' || + datatype === 'wikibase-item' || + datatype === 'wikibase-lexeme' || + datatype === 'wikibase-property' || + datatype === 'wikibase-sense' || + datavalue.type === 'wikibase-entityid' + ) { + return entity(datavalue as SnakEntityValue, options) } + + if (datatype === 'globe-coordinate' || datavalue.type === 'globecoordinate') { + return coordinate(datavalue as SnakGlobeCoordinateValue, options) + } + + if (datatype === 'monolingualtext' || datavalue.type === 'monolingualtext') { + return monolingualtext(datavalue as SnakMonolingualTextValue, options) + } + + if (datatype === 'quantity' || datavalue.type === 'quantity') { + return quantity(datavalue as SnakQuantityValue, options) + } + + if (datatype === 'time' || datavalue.type === 'time') { + return time(datavalue as SnakTimeValue, options) + } + + if ( + datatype === 'commonsMedia' || + datatype === 'external-id' || + datatype === 'geo-shape' || + datatype === 'math' || + datatype === 'musical-notation' || + datatype === 'string' || + datatype === 'tabular-data' || + datatype === 'url' || + datavalue.type === 'string' + ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + return simple(datavalue as SnakStringValue) + } + + unknownClaimType(datatype, datavalue, claimId) +} + +// TypeScript notices when the argument isnt `never` and does not compile in that case -> some case is not implemented +function unknownClaimType ( + datatype: never, + datavalue: { readonly type: never }, + claimId: string, +): never { + const minimal = String(datatype) || String(datavalue) + const full = JSON.stringify({ datatype, datavalue }) + throw new Error(`${minimal} claim parser isn't implemented\nPlease report to https://github.com/maxlath/wikibase-sdk/issues\n\nClaim id: ${claimId}\n${full}`) } diff --git a/src/helpers/rank.ts b/src/helpers/rank.ts index 0e1e6e3c..91799352 100644 --- a/src/helpers/rank.ts +++ b/src/helpers/rank.ts @@ -1,7 +1,14 @@ -import type { Claims, PropertyClaims } from '../types/claim.js' +import { typedEntries } from '../utils/utils.js' +import type { Claim, Claims, PropertyClaims, Rank } from '../types/claim.js' export function truthyPropertyClaims (propertyClaims: PropertyClaims): PropertyClaims { - const aggregate = propertyClaims.reduce(aggregatePerRank, {}) + const aggregate: Partial> = {} + for (const claim of propertyClaims) { + const { rank } = claim + aggregate[rank] ??= [] + aggregate[rank].push(claim) + } + // on truthyness: https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Truthy_statements return aggregate.preferred || aggregate.normal || [] } @@ -10,17 +17,10 @@ export function nonDeprecatedPropertyClaims (propertyClaims: PropertyClaims): Pr return propertyClaims.filter(claim => claim.rank !== 'deprecated') } -const aggregatePerRank = (aggregate, claim) => { - const { rank } = claim - aggregate[rank] || (aggregate[rank] = []) - aggregate[rank].push(claim) - return aggregate -} - export function truthyClaims (claims: Claims): Claims { - const truthClaimsOnly = {} - Object.keys(claims).forEach(property => { - truthClaimsOnly[property] = truthyPropertyClaims(claims[property]) - }) + const truthClaimsOnly: Claims = {} + for (const [ property, value ] of typedEntries(claims)) { + truthClaimsOnly[property] = truthyPropertyClaims(value) + } return truthClaimsOnly } diff --git a/src/helpers/simplify_claims.ts b/src/helpers/simplify_claims.ts index 6b3e25a7..46c3abcd 100644 --- a/src/helpers/simplify_claims.ts +++ b/src/helpers/simplify_claims.ts @@ -1,14 +1,15 @@ import { uniq } from '../utils/utils.js' import { parseClaim } from './parse_claim.js' import { truthyPropertyClaims, nonDeprecatedPropertyClaims } from './rank.js' -import type { Claim, Claims, PropertyClaims, PropertyQualifiers, Qualifier, Qualifiers } from '../types/claim.js' +import type { Claim, Claims, DataType, PropertyClaims, PropertyQualifiers, QualifierSnak, Qualifiers, Reference, ReferenceSnak, SnakType } from '../types/claim.js' +import type { PropertyId } from '../types/entity.js' import type { SimplifiedClaim, SimplifiedClaims, SimplifiedPropertyClaims, SimplifySnakOptions, SimplifySnaksOptions } from '../types/simplify_claims.js' +import type { SnakValue } from '../types/snakvalue.js' -function simplifySnaks (snaks, options) { +function simplifySnaks (snaks: Record, options: SimplifySnaksOptions) { const { propertyPrefix } = options const simplifiedSnaks: any = {} - for (let id in snaks) { - const propertySnaks = snaks[id] + for (let [ id, propertySnaks ] of Object.entries(snaks)) { if (propertyPrefix) { id = propertyPrefix + ':' + id } @@ -17,7 +18,7 @@ function simplifySnaks (snaks, options) { return simplifiedSnaks } -function simplifyPropertySnaks (propertySnaks, options) { +function simplifyPropertySnaks (propertySnaks: any[], options: SimplifySnaksOptions) { // Avoid to throw on empty inputs to allow to simplify claims array // without having to know if the entity as claims for this property // Ex: simplifyPropertyClaims(entity.claims.P124211616) @@ -31,7 +32,7 @@ function simplifyPropertySnaks (propertySnaks, options) { propertySnaks = truthyPropertyClaims(propertySnaks) } - propertySnaks = propertySnaks + const simplified = propertySnaks .map(claim => simplifyClaim(claim, options)) // Filter-out novalue and somevalue claims, // unless a novalueValue or a somevalueValue is passed in options @@ -39,27 +40,32 @@ function simplifyPropertySnaks (propertySnaks, options) { .filter(obj => obj !== undefined) // Deduplicate values unless we return a rich value object - if (propertySnaks[0] && typeof propertySnaks[0] !== 'object') { - return uniq(propertySnaks) + if (simplified[0] && typeof simplified[0] !== 'object') { + return uniq(simplified) } else { - return propertySnaks + return simplified } } -// Expects a single snak object -// Ex: entity.claims.P369[0] -function simplifySnak (claim, options) { +/** + * tries to replace wikidata deep claim object by a simple value + * e.g. a string, an entity Qid or an epoch time number + * + * Expects a single snak object + * Ex: entity.claims.P369[0] + */ +function simplifySnak (claim: Claim | QualifierSnak | ReferenceSnak, options: SimplifySnakOptions) { const { keepQualifiers, keepReferences, keepIds, keepHashes, keepTypes, keepSnaktypes, keepRanks } = parseKeepOptions(options) - // tries to replace wikidata deep claim object by a simple value - // e.g. a string, an entity Qid or an epoch time number - const { mainsnak, rank } = claim - - let value, datatype, datavalue, snaktype, isQualifierSnak, isReferenceSnak - if (mainsnak) { - datatype = mainsnak.datatype - datavalue = mainsnak.datavalue - snaktype = mainsnak.snaktype + let datatype: DataType | undefined + let datavalue: SnakValue + let snaktype: SnakType + let isQualifierSnak: boolean + let isReferenceSnak: boolean + if ('mainsnak' in claim) { + datatype = claim.mainsnak.datatype + datavalue = claim.mainsnak.datavalue + snaktype = claim.mainsnak.snaktype } else { // Qualifiers have no mainsnak, and define datatype, datavalue on claim datavalue = claim.datavalue @@ -70,6 +76,7 @@ function simplifySnak (claim, options) { else isReferenceSnak = true } + let value: any if (datavalue) { value = parseClaim(datatype, datavalue, options, claim.id) } else { @@ -84,7 +91,7 @@ function simplifySnak (claim, options) { const valueObj: any = { value } - if (keepHashes) valueObj.hash = claim.hash + if (keepHashes && 'hash' in claim) valueObj.hash = claim.hash if (keepTypes) valueObj.type = datatype if (keepSnaktypes) valueObj.snaktype = snaktype @@ -109,18 +116,16 @@ function simplifySnak (claim, options) { if (keepSnaktypes) valueObj.snaktype = snaktype - if (keepRanks) valueObj.rank = rank + if (keepRanks && 'rank' in claim) valueObj.rank = claim.rank - const subSnaksOptions = getSubSnakOptions(options) - subSnaksOptions.keepHashes = keepHashes + const subSnaksOptions = { ...options, areSubSnaks: true } if (keepQualifiers) { - valueObj.qualifiers = simplifyQualifiers(claim.qualifiers, subSnaksOptions) + valueObj.qualifiers = 'qualifiers' in claim ? simplifyQualifiers(claim.qualifiers, subSnaksOptions) : {} } if (keepReferences) { - claim.references = claim.references || [] - valueObj.references = simplifyReferences(claim.references, subSnaksOptions) + valueObj.references = 'references' in claim ? simplifyReferences(claim.references, subSnaksOptions) : [] } if (keepIds) valueObj.id = claim.id @@ -139,38 +144,42 @@ export function simplifyClaim (claim: Claim, options: SimplifySnakOptions = {}): } export function simplifyQualifiers (qualifiers: Qualifiers, options: SimplifySnaksOptions = {}) { - return simplifySnaks(qualifiers, getSubSnakOptions(options)) + return simplifySnaks(qualifiers, { ...options, areSubSnaks: true }) } export function simplifyPropertyQualifiers (propertyQualifiers: PropertyQualifiers, options: SimplifySnaksOptions = {}) { - return simplifyPropertySnaks(propertyQualifiers, getSubSnakOptions(options)) + return simplifyPropertySnaks(propertyQualifiers, { ...options, areSubSnaks: true }) } -export function simplifyQualifier (qualifier: Qualifier, options: SimplifySnakOptions = {}) { +export function simplifyQualifier (qualifier: QualifierSnak, options: SimplifySnakOptions = {}) { return simplifySnak(qualifier, options) } -export function simplifyReferences (references, options) { +export function simplifyReferences (references: Reference[], options: SimplifySnaksOptions) { return references.map(refRecord => simplifyReferenceRecord(refRecord, options)) } -export function simplifyReferenceRecord (refRecord, options) { - const subSnaksOptions = getSubSnakOptions(options) +export function simplifyReferenceRecord (refRecord: Reference, options: SimplifySnaksOptions) { + const subSnaksOptions = { ...options, areSubSnaks: true } const snaks = simplifySnaks(refRecord.snaks, subSnaksOptions) if (subSnaksOptions.keepHashes) return { snaks, hash: refRecord.hash } else return snaks } -const getSubSnakOptions = (options: any = {}) => { - if (options.areSubSnaks) return options - // Using a new object so that the original options object isn't modified - else return Object.assign({}, options, { areSubSnaks: true }) -} - -const keepOptions = [ 'keepQualifiers', 'keepReferences', 'keepIds', 'keepHashes', 'keepTypes', 'keepSnaktypes', 'keepRanks', 'keepRichValues' ] - -const parseKeepOptions = options => { +const keepOptions = [ + 'keepHashes', + 'keepIds', + 'keepQualifiers', + 'keepRanks', + 'keepReferences', + 'keepRichValues', + 'keepSnaktypes', + 'keepTypes', +] as const +type KeepOption = typeof keepOptions[number] + +const parseKeepOptions = (options: SimplifySnakOptions): Record => { if (options.keepAll) { keepOptions.forEach(optionName => { - if (options[optionName] == null) options[optionName] = true + options[optionName] = options[optionName] ?? true }) } - return options + return options as Record } diff --git a/src/helpers/simplify_entity.ts b/src/helpers/simplify_entity.ts index 91912dae..67ded80d 100644 --- a/src/helpers/simplify_entity.ts +++ b/src/helpers/simplify_entity.ts @@ -1,43 +1,40 @@ import * as simplify from './simplify.js' -import type { Entities, Entity, SimplifiedEntity } from '../types/entity.js' +import type { Entities, Entity, SimplifiedEntity, SimplifiedItem, SimplifiedProperty, SimplifiedLexeme } from '../types/entity.js' import type { SimplifyEntityOptions } from '../types/options.js' export const simplifyEntity = (entity: Entity, options: SimplifyEntityOptions = {}): SimplifiedEntity => { - const { type } = entity - const simplified: any = { - id: entity.id, - type, - modified: entity.modified, - } - + const { id, modified, type } = entity if (type === 'item') { - simplifyIfDefined(entity, simplified, 'labels') - simplifyIfDefined(entity, simplified, 'descriptions') - simplifyIfDefined(entity, simplified, 'aliases') - simplifyIfDefined(entity, simplified, 'claims', options) - simplifyIfDefined(entity, simplified, 'sitelinks', options) + const simplified: SimplifiedItem = { id, type, modified } + + if (entity.labels != null) simplified.labels = simplify.labels(entity.labels) + if (entity.descriptions != null) simplified.descriptions = simplify.descriptions(entity.descriptions) + if (entity.aliases != null) simplified.aliases = simplify.aliases(entity.aliases) + if (entity.claims != null) simplified.claims = simplify.claims(entity.claims, options) + if (entity.sitelinks != null) simplified.sitelinks = simplify.sitelinks(entity.sitelinks, options) + + return simplified } else if (type === 'property') { - simplified.datatype = entity.datatype - simplifyIfDefined(entity, simplified, 'labels') - simplifyIfDefined(entity, simplified, 'descriptions') - simplifyIfDefined(entity, simplified, 'aliases') - simplifyIfDefined(entity, simplified, 'claims', options) + const simplified: SimplifiedProperty = { id, type, modified, datatype: entity.datatype } + + if (entity.labels != null) simplified.labels = simplify.labels(entity.labels) + if (entity.descriptions != null) simplified.descriptions = simplify.descriptions(entity.descriptions) + if (entity.aliases != null) simplified.aliases = simplify.aliases(entity.aliases) + if (entity.claims != null) simplified.claims = simplify.claims(entity.claims, options) + + return simplified } else if (type === 'lexeme') { - simplifyIfDefined(entity, simplified, 'lemmas') - simplified.lexicalCategory = entity.lexicalCategory - simplified.language = entity.language - simplifyIfDefined(entity, simplified, 'claims', options) - simplifyIfDefined(entity, simplified, 'forms', options) - simplifyIfDefined(entity, simplified, 'senses', options) - } + const simplified: SimplifiedLexeme = { id, type, modified, lexicalCategory: entity.lexicalCategory, language: entity.language } - return simplified -} + if (entity.lemmas != null) simplified.lemmas = simplify.lemmas(entity.lemmas) + if (entity.claims != null) simplified.claims = simplify.claims(entity.claims, options) + if (entity.forms != null) simplified.forms = simplify.forms(entity.forms, options) + if (entity.senses != null) simplified.senses = simplify.senses(entity.senses, options) -const simplifyIfDefined = (entity, simplified, attribute, options?) => { - if (entity[attribute] != null) { - simplified[attribute] = simplify[attribute](entity[attribute], options) + return simplified } + + return { id, type, modified } } export const simplifyEntities = (entities: Entities, options: SimplifyEntityOptions = {}) => { diff --git a/src/helpers/simplify_forms.ts b/src/helpers/simplify_forms.ts index 567a4dc3..8048fcc1 100644 --- a/src/helpers/simplify_forms.ts +++ b/src/helpers/simplify_forms.ts @@ -15,4 +15,4 @@ export const simplifyForm = (form: Form, options: SimplifyClaimsOptions = {}): S } } -export const simplifyForms = (forms, options) => forms.map(form => simplifyForm(form, options)) +export const simplifyForms = (forms: readonly Form[], options: SimplifyClaimsOptions = {}) => forms.map(form => simplifyForm(form, options)) diff --git a/src/helpers/simplify_sitelinks.ts b/src/helpers/simplify_sitelinks.ts index e79e57c8..0297f37b 100644 --- a/src/helpers/simplify_sitelinks.ts +++ b/src/helpers/simplify_sitelinks.ts @@ -1,3 +1,4 @@ +import { typedEntries } from '../utils/utils.js' import { getSitelinkUrl } from './sitelinks.js' import type { SimplifySitelinkOptions } from '../types/options.js' import type { SimplifiedSitelinks, Sitelinks } from '../types/sitelinks.js' @@ -5,28 +6,28 @@ import type { SimplifiedSitelinks, Sitelinks } from '../types/sitelinks.js' export function simplifySitelinks (sitelinks: Sitelinks, options: SimplifySitelinkOptions = {}): SimplifiedSitelinks { let { addUrl, keepBadges, keepAll } = options keepBadges = keepBadges || keepAll - return Object.keys(sitelinks).reduce(aggregateValues({ - sitelinks, - addUrl, - keepBadges, - }), {}) -} -const aggregateValues = ({ sitelinks, addUrl, keepBadges }) => (index, key) => { - // Accomodating for wikibase-cli, which might set the sitelink to null - // to signify that a requested sitelink was not found - if (sitelinks[key] == null) { - index[key] = sitelinks[key] - return index - } + const result: SimplifiedSitelinks = {} - const { title, badges } = sitelinks[key] - if (addUrl || keepBadges) { - index[key] = { title } - if (addUrl) index[key].url = getSitelinkUrl({ site: key, title }) - if (keepBadges) index[key].badges = badges - } else { - index[key] = title + for (const [ key, value ] of typedEntries(sitelinks)) { + // Accomodating for wikibase-cli, which might set the sitelink to null + // to signify that a requested sitelink was not found + if (value == null) { + result[key] = null + continue + } + + const { title, badges } = value + if (addUrl || keepBadges) { + result[key] = { title } + // @ts-expect-error TypeScript cant assume which of the two types it is + if (addUrl) result[key].url = getSitelinkUrl({ site: key, title }) + // @ts-expect-error TypeScript cant assume which of the two types it is + if (keepBadges) result[key].badges = badges + } else { + result[key] = title + } } - return index + + return result } diff --git a/src/helpers/simplify_sparql_results.ts b/src/helpers/simplify_sparql_results.ts index b9f65263..35c2dea0 100644 --- a/src/helpers/simplify_sparql_results.ts +++ b/src/helpers/simplify_sparql_results.ts @@ -1,9 +1,12 @@ -import type { SimplifiedSparqlResults, SparqlResults } from '../types/sparql.js' +import type { SimplifiedSparqlResultMinified, SimplifiedSparqlResultRecord, SimplifiedSparqlResults, SparqlResults, SparqlValueObj, SparqlValueRaw, SparqlValueType } from '../types/sparql.js' -export type SimplifySparqlResultsOptions = { +export interface SimplifySparqlResultsOptions { readonly minimize?: boolean } +export function simplifySparqlResults (input: SparqlResults): SimplifiedSparqlResultRecord +export function simplifySparqlResults (input: SparqlResults, options: { readonly minimize: true }): SimplifiedSparqlResultMinified + export function simplifySparqlResults (input: SparqlResults, options: SimplifySparqlResultsOptions = {}): SimplifiedSparqlResults { if (typeof input === 'string') { input = JSON.parse(input) @@ -12,7 +15,7 @@ export function simplifySparqlResults (input: SparqlResults, options: SimplifySp const { vars } = input.head const results = input.results.bindings - if (vars.length === 1 && options.minimize === true) { + if (vars.length === 1 && options.minimize) { const varName = vars[0] return results .map(result => parseValue(result[varName])) @@ -24,13 +27,7 @@ export function simplifySparqlResults (input: SparqlResults, options: SimplifySp return results.map(getSimplifiedResult(richVars, associatedVars, standaloneVars)) } -type ValueObj = { - readonly type: 'uri' | 'bnode' - readonly datatype?: string - readonly value: string -} - -function parseValue (valueObj: ValueObj | undefined): string | number | boolean | null { +function parseValue (valueObj: SparqlValueObj | undefined): string | number | boolean | null { // blank nodes will be filtered-out in order to get things simple if (!valueObj || valueObj.type === 'bnode') return null @@ -72,7 +69,7 @@ function convertStatementUriToGuid (uri: string) { return parts[0] + '$' + parts.slice(1).join('-') } -const identifyVars = vars => { +const identifyVars = (vars: readonly string[]) => { let richVars = vars.filter(varName => vars.some(isAssociatedVar(varName))) richVars = richVars.filter(richVar => { return !richVars.some(otherRichVar => { @@ -87,15 +84,15 @@ const identifyVars = vars => { return { richVars, associatedVars, standaloneVars } } -const isAssociatedVar = varNameA => { +const isAssociatedVar = (varNameA: string) => { const pattern = new RegExp(`^${varNameA}[A-Z]\\w+`) return pattern.test.bind(pattern) } -const getSimplifiedResult = (richVars, associatedVars, standaloneVars) => result => { - const simplifiedResult = {} +const getSimplifiedResult = (richVars: readonly string[], associatedVars: readonly string[], standaloneVars: readonly string[]) => (result: Record) => { + const simplifiedResult: Record = {} for (const varName of richVars) { - const richVarData: any = {} + const richVarData: Record = {} const value = parseValue(result[varName]) if (value != null) richVarData.value = value for (const associatedVarName of associatedVars) { @@ -109,17 +106,13 @@ const getSimplifiedResult = (richVars, associatedVars, standaloneVars) => result return simplifiedResult } -const addAssociatedValue = (result, varName, associatedVarName, richVarData) => { +const addAssociatedValue = (result: Record, varName: string, associatedVarName: string, richVarData: Record) => { // ex: propertyType => Type let shortAssociatedVarName = associatedVarName.split(varName)[1] // ex: Type => type shortAssociatedVarName = shortAssociatedVarName[0].toLowerCase() + shortAssociatedVarName.slice(1) // ex: altLabel => aliases - shortAssociatedVarName = specialNames[shortAssociatedVarName] || shortAssociatedVarName + shortAssociatedVarName = shortAssociatedVarName === 'altLabel' ? 'aliases' : shortAssociatedVarName const associatedVarData = result[associatedVarName] if (associatedVarData != null) richVarData[shortAssociatedVarName] = associatedVarData.value } - -const specialNames = { - altLabel: 'aliases', -} as const diff --git a/src/helpers/simplify_text_attributes.ts b/src/helpers/simplify_text_attributes.ts index 8f208ea3..75b44df1 100644 --- a/src/helpers/simplify_text_attributes.ts +++ b/src/helpers/simplify_text_attributes.ts @@ -1,11 +1,12 @@ +import { typedEntries } from '../utils/utils.js' import type { WmLanguageCode } from '../types/options.js' import type { Aliases, Descriptions, Glosses, Labels, Lemmas, Representations, SimplifiedAliases, SimplifiedDescriptions, SimplifiedGlosses, SimplifiedLabels, SimplifiedLemmas, SimplifiedRepresentations } from '../types/terms.js' -type InValue = { readonly value: T } +interface InValue { readonly value: T } function singleValue (data: Partial>>>) { const simplified: Partial> = {} - for (const [ lang, obj ] of Object.entries(data)) { + for (const [ lang, obj ] of typedEntries(data)) { simplified[lang] = obj != null ? obj.value : null } return simplified @@ -13,7 +14,7 @@ function singleValue (data: Partial (data: Partial>>>>) { const simplified: Partial> = {} - for (const [ lang, obj ] of Object.entries(data)) { + for (const [ lang, obj ] of typedEntries(data)) { simplified[lang] = obj != null ? obj.map(o => o.value) : [] } return simplified diff --git a/src/helpers/sitelinks.ts b/src/helpers/sitelinks.ts index 8d6a9c57..e19218f8 100644 --- a/src/helpers/sitelinks.ts +++ b/src/helpers/sitelinks.ts @@ -1,7 +1,6 @@ -import { fixedEncodeURIComponent, isOfType, rejectObsoleteInterface, replaceSpaceByUnderscores } from '../utils/utils.js' +import { fixedEncodeURIComponent, isOfType, isAKey, rejectObsoleteInterface, replaceSpaceByUnderscores } from '../utils/utils.js' import { languages } from './sitelinks_languages.js' import { specialSites } from './special_sites.js' -import type { EntityId } from '../types/entity.js' import type { Url, WmLanguageCode } from '../types/options.js' import type { Site } from '../types/sitelinks.js' @@ -18,9 +17,14 @@ export function getSitelinkUrl ({ site, title }: GetSitelinkUrlOptions): Url { if (!site) throw new Error('missing a site') if (!title) throw new Error('missing a title') + if (isAKey(siteUrlBuilders, site)) { + return siteUrlBuilders[site](title) + } + const shortSiteKey = site.replace(/wiki$/, '') - const specialUrlBuilder = siteUrlBuilders[shortSiteKey] || siteUrlBuilders[site] - if (specialUrlBuilder) return specialUrlBuilder(title) + if (isAKey(siteUrlBuilders, shortSiteKey)) { + return siteUrlBuilders[shortSiteKey](title) + } const { lang, project } = getSitelinkData(site) title = fixedEncodeURIComponent(replaceSpaceByUnderscores(title)) @@ -34,9 +38,8 @@ const siteUrlBuilders = { mediawiki: (title: string) => `https://www.mediawiki.org/wiki/${title}`, meta: wikimediaSite('meta'), species: wikimediaSite('species'), - wikidata: (entityId: EntityId) => { - const prefix = prefixByEntityLetter[entityId[0]] - let title = prefix ? `${prefix}:${entityId}` : entityId + wikidata: (entityId: string) => { + let title = prefixByEntity(entityId) // Required for forms and senses title = title.replace('-', '#') return `${wikidataBase}${title}` @@ -44,11 +47,12 @@ const siteUrlBuilders = { wikimania: wikimediaSite('wikimania'), } as const -const prefixByEntityLetter = { - E: 'EntitySchema', - L: 'Lexeme', - P: 'Property', -} as const +function prefixByEntity (entityId: string) { + if (entityId.startsWith('E')) return `EntitySchema:${entityId}` + if (entityId.startsWith('L')) return `Lexeme:${entityId}` + if (entityId.startsWith('P')) return `Property:${entityId}` + return entityId +} const sitelinkUrlPattern = /^https?:\/\/([\w-]{2,10})\.(\w+)\.org\/\w+\/(.*)/ @@ -84,9 +88,9 @@ export function getSitelinkData (site: Site | Url): SitelinkData { return { lang, project, key, title, url } } else { const key = site - const specialProjectName = specialSites[key] - if (specialProjectName) { - return { lang: 'en', project: specialProjectName, key } + if (isAKey(specialSites, site)) { + const project = specialSites[site] + return { lang: 'en', project, key } } let [ lang, projectSuffix, rest ] = key.split('wik') @@ -101,10 +105,10 @@ export function getSitelinkData (site: Site | Url): SitelinkData { // Support keys such as be_x_oldwiki, which refers to be-x-old.wikipedia.org lang = lang.replace(/_/g, '-') + if (!isAKey(projectsBySuffix, projectSuffix)) throw new Error(`sitelink project not found: ${site}`) const project = projectsBySuffix[projectSuffix] - if (!project) throw new Error(`sitelink project not found: ${project}`) - // @ts-expect-error + // @ts-expect-error lang has replaced _ with - and is not a perfect WmLanguageCode anymore return { lang, project, key } } } diff --git a/src/helpers/validate.ts b/src/helpers/validate.ts index 50ad95eb..d9b6865f 100644 --- a/src/helpers/validate.ts +++ b/src/helpers/validate.ts @@ -3,7 +3,7 @@ import { isEntityId, isEntityPageTitle, isPropertyId, isRevisionId } from './hel /** Ensure both via TypeScript and at runtime that the input value is of the expected type. Throws error when it is not */ function validate (name: string, testFn: (value: string) => value is T) { return function (value: T): void { - if (!testFn(value)) throw new Error(`invalid ${name}: ${value}`) + if (!testFn(value)) throw new Error(`invalid ${name}: ${String(value)}`) } } diff --git a/src/helpers/wikibase_time.ts b/src/helpers/wikibase_time.ts new file mode 100644 index 00000000..ee927f78 --- /dev/null +++ b/src/helpers/wikibase_time.ts @@ -0,0 +1,132 @@ +import { isAKey } from '../utils/utils.js' + +export function wikibaseTimeToDateObject (wikibaseTime: TimeInputValue): Date { + // Also accept claim datavalue.value objects + if (typeof wikibaseTime === 'object') { + wikibaseTime = wikibaseTime.time + } + + const sign = wikibaseTime[0] + let [ yearMonthDay, withinDay ] = wikibaseTime.slice(1).split('T') + + // Wikidata generates invalid ISO dates to indicate precision + // ex: +1990-00-00T00:00:00Z to indicate 1990 with year precision + yearMonthDay = yearMonthDay.replace(/-00/g, '-01') + const rest = `${yearMonthDay}T${withinDay}` + + return fullDateData(sign, rest) +} + +const fullDateData = (sign: string, rest: string) => { + const year = rest.split('-')[0] + const needsExpandedYear = sign === '-' || year.length > 4 + + return needsExpandedYear ? expandedYearDate(sign, rest, year) : new Date(rest) +} + +const expandedYearDate = (sign: string, rest: string, year: string) => { + let date: string + // Using ISO8601 expanded notation for negative years or positive + // years with more than 4 digits: adding up to 2 leading zeros + // when needed. Can't find the documentation again, but testing + // with `new Date(date)` gives a good clue of the implementation + if (year.length === 4) { + date = `${sign}00${rest}` + } else if (year.length === 5) { + date = `${sign}0${rest}` + } else { + date = sign + rest + } + return new Date(date) +} + +export interface WikibaseTimeObject { + time: string + precision: number +} + +export type TimeInputValue = string | WikibaseTimeObject + +type TimeFunction = (wikibaseTime: TimeInputValue) => T + +// Try to parse the date or return the input +function bestEffort (fn: TimeFunction) { + return (value: TimeInputValue) => { + try { + return fn(value) + } catch { + value = typeof value === 'string' ? value : value.time + + const sign = value[0] + let [ yearMonthDay, withinDay ] = value.slice(1).split('T') + if (!sign || !yearMonthDay || !withinDay) { + throw new Error('TimeInput is invalid: ' + JSON.stringify(value)) + } + + yearMonthDay = yearMonthDay.replace(/-00/g, '-01') + + return `${sign}${yearMonthDay}T${withinDay}` + } + } +} + +const toEpochTime = (wikibaseTime: TimeInputValue) => wikibaseTimeToDateObject(wikibaseTime).getTime() +const toISOString = (wikibaseTime: TimeInputValue) => wikibaseTimeToDateObject(wikibaseTime).toISOString() + +// A date format that knows just three precisions: +// 'yyyy', 'yyyy-mm', and 'yyyy-mm-dd' (including negative and non-4 digit years) +// Should be able to handle the old and the new Wikidata time: +// - in the old one, units below the precision where set to 00 +// - in the new one, those months and days are set to 01 in those cases, +// so when we can access the full claim object, we check the precision +// to recover the old format +const toSimpleDay = (wikibaseTime: TimeInputValue): string => { + // Also accept claim datavalue.value objects, and actually prefer those, + // as we can check the precision + if (typeof wikibaseTime === 'object') { + const { time, precision } = wikibaseTime + // Year precision + if (precision === 9) wikibaseTime = time.replace('-01-01T', '-00-00T') + // Month precision + else if (precision === 10) wikibaseTime = time.replace('-01T', '-00T') + else wikibaseTime = time + } + + return wikibaseTime.split('T')[0] + // Remove positive years sign + .replace(/^\+/, '') + // Remove years padding zeros + .replace(/^(-?)0+/, '$1') + // Remove days if not included in the Wikidata date precision + .replace(/-00$/, '') + // Remove months if not included in the Wikidata date precision + .replace(/-00$/, '') +} + +export const wikibaseTimeToEpochTime = bestEffort(toEpochTime) +export const wikibaseTimeToISOString = bestEffort(toISOString) +export const wikibaseTimeToSimpleDay = bestEffort(toSimpleDay) + +export function convertTime (timeConverter: TimeConverter | TimeFunction = 'iso', wikibaseTime: TimeInputValue) { + if (typeof timeConverter === 'function') { + return timeConverter(wikibaseTime) + } + + if (isAKey(timeConverters, timeConverter)) { + return timeConverters[timeConverter](wikibaseTime) + } + + throw new Error(`invalid time converter key: ${JSON.stringify(timeConverter).substring(0, 100)}`) +} + +// Each time converter should be able to accept 2 keys of arguments: +// - either datavalue.value objects (prefered as it gives access to the precision) +// - or the time string (datavalue.value.time) +const timeConverters = { + iso: wikibaseTimeToISOString, + epoch: wikibaseTimeToEpochTime, + 'simple-day': wikibaseTimeToSimpleDay, + none: (wikibaseTime: TimeInputValue) => typeof wikibaseTime === 'string' ? wikibaseTime : wikibaseTime.time, +} as const +export type TimeConverterFn = TimeFunction +export type TimeConverter = keyof typeof timeConverters diff --git a/src/helpers/wikibase_time_to_date_object.ts b/src/helpers/wikibase_time_to_date_object.ts deleted file mode 100644 index 87ebc66d..00000000 --- a/src/helpers/wikibase_time_to_date_object.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { TimeInputValue } from './helpers.js' - -export function wikibaseTimeToDateObject (wikibaseTime: TimeInputValue): Date { - // Also accept claim datavalue.value objects - if (typeof wikibaseTime === 'object') { - wikibaseTime = wikibaseTime.time - } - - const sign = wikibaseTime[0] - let [ yearMonthDay, withinDay ] = wikibaseTime.slice(1).split('T') - - // Wikidata generates invalid ISO dates to indicate precision - // ex: +1990-00-00T00:00:00Z to indicate 1990 with year precision - yearMonthDay = yearMonthDay.replace(/-00/g, '-01') - const rest = `${yearMonthDay}T${withinDay}` - - return fullDateData(sign, rest) -} - -const fullDateData = (sign: string, rest: string) => { - const year = rest.split('-')[0] - const needsExpandedYear = sign === '-' || year.length > 4 - - return needsExpandedYear ? expandedYearDate(sign, rest, year) : new Date(rest) -} - -const expandedYearDate = (sign: string, rest: string, year: string) => { - let date: string - // Using ISO8601 expanded notation for negative years or positive - // years with more than 4 digits: adding up to 2 leading zeros - // when needed. Can't find the documentation again, but testing - // with `new Date(date)` gives a good clue of the implementation - if (year.length === 4) { - date = `${sign}00${rest}` - } else if (year.length === 5) { - date = `${sign}0${rest}` - } else { - date = sign + rest - } - return new Date(date) -} diff --git a/src/index.ts b/src/index.ts index 765c7b6c..45572c61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,12 +2,10 @@ import { WBK } from './wikibase-sdk.js' export default WBK -export * from './wikibase-sdk.js' -export * from './helpers/helpers.js' -export * from './helpers/rank.js' -export * from './helpers/sitelinks.js' export * as parse from './helpers/parse_responses.js' export * as simplify from './helpers/simplify.js' +export * from './helpers/helpers.js' +export * from './helpers/rank.js' export * from './helpers/simplify_claims.js' export * from './helpers/simplify_entity.js' export * from './helpers/simplify_forms.js' @@ -15,6 +13,8 @@ export * from './helpers/simplify_senses.js' export * from './helpers/simplify_sitelinks.js' export * from './helpers/simplify_sparql_results.js' export * from './helpers/simplify_text_attributes.js' +export * from './helpers/sitelinks.js' +export * from './helpers/wikibase_time.js' export * from './types/claim.js' export * from './types/entity.js' export * from './types/lexeme.js' @@ -24,3 +24,4 @@ export * from './types/simplify_claims.js' export * from './types/sitelinks.js' export * from './types/sparql.js' export * from './types/terms.js' +export * from './wikibase-sdk.js' diff --git a/src/queries/cirrus_search.ts b/src/queries/cirrus_search.ts index b54c5f24..a798a49b 100644 --- a/src/queries/cirrus_search.ts +++ b/src/queries/cirrus_search.ts @@ -1,6 +1,6 @@ // See https://www.wikidata.org/w/api.php?action=help&modules=query%2Bsearch -import { rejectObsoleteInterface } from '../utils/utils.js' +import { isAKey, rejectObsoleteInterface } from '../utils/utils.js' import type { Url, UrlResultFormat } from '../types/options.js' import type { BuildUrlFunction } from '../utils/build_url.js' @@ -23,11 +23,12 @@ export function cirrusSearchPagesFactory (buildUrl: BuildUrlFunction) { rejectObsoleteInterface(arguments) // Accept sr parameters with or without prefix - for (const key in options) { + for (const [ key, value ] of Object.entries(options)) { if (key.startsWith('sr')) { const shortKey = key.replace(/^sr/, '') + if (!isAKey(options, shortKey)) throw new Error(`${key} is not a valid option`) if (options[shortKey] != null) throw new Error(`${shortKey} and ${key} are the same`) - options[shortKey] = options[key] + options[shortKey] = value } } @@ -63,20 +64,20 @@ export function cirrusSearchPagesFactory (buildUrl: BuildUrlFunction) { } if (profile != null && typeof profile !== 'string') { - throw new Error(`invalid profile: ${profile} (${typeof profile}, expected string)`) + throw new Error(`invalid profile: ${String(profile)} (${typeof profile}, expected string)`) } if (sort != null && typeof sort !== 'string') { - throw new Error(`invalid sort: ${sort} (${typeof sort}, expected string)`) + throw new Error(`invalid sort: ${String(sort)} (${typeof sort}, expected string)`) } - let srprop + let srprop: string if (prop != null) { if (prop instanceof Array) prop = prop.join('|') if (typeof prop !== 'string') { - throw new Error(`invalid prop: ${prop} (${typeof prop}, expected string)`) + throw new Error(`invalid prop: ${String(prop)} (${typeof prop}, expected string)`) } - srprop = prop.toString() + srprop = prop } return buildUrl({ diff --git a/src/queries/get_entities.ts b/src/queries/get_entities.ts index fd3b5b04..45268346 100644 --- a/src/queries/get_entities.ts +++ b/src/queries/get_entities.ts @@ -29,7 +29,7 @@ export function getEntitiesFactory (buildUrl: BuildUrlFunction) { // Allow to pass ids as a single string ids = forceArray(ids) - ids.forEach(o => validate.entityId(o)) + ids.forEach(o => { validate.entityId(o) }) if (ids.length > 50) { console.warn(`getEntities accepts 50 ids max to match Wikidata API limitations: @@ -48,6 +48,8 @@ export function getEntitiesFactory (buildUrl: BuildUrlFunction) { format, } + // Only specify 'no' when explictly false. Eslint ignores the fact that redirects could be undefined too. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare if (redirects === false) query.redirects = 'no' if (languages) { diff --git a/src/queries/get_entities_from_sitelinks.ts b/src/queries/get_entities_from_sitelinks.ts index 4b676ac4..a39afa1d 100644 --- a/src/queries/get_entities_from_sitelinks.ts +++ b/src/queries/get_entities_from_sitelinks.ts @@ -58,6 +58,8 @@ export function getEntitiesFromSitelinksFactory (buildUrl: BuildUrlFunction) { query.props = props.join('|') } + // Only specify 'no' when explictly false. Eslint ignores the fact that redirects could be undefined too. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare if (redirects === false) query.redirects = 'no' return buildUrl(query) diff --git a/src/queries/get_reverse_claims.ts b/src/queries/get_reverse_claims.ts index 2c45726a..7db395d2 100644 --- a/src/queries/get_reverse_claims.ts +++ b/src/queries/get_reverse_claims.ts @@ -10,9 +10,12 @@ import type { Url } from '../types/options.js' // https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#WDQS_data_differences const itemsOnly = 'FILTER NOT EXISTS { ?subject rdf:type wikibase:Property . } ' +type Value = string | number +type Values = Value | readonly Value[] + export interface GetReverseClaimsOptions { - properties: PropertyId | PropertyId[] - values: string | number | string[] | number[] + properties: PropertyId | readonly PropertyId[] + values: Values limit?: number caseInsensitive?: boolean keepProperties?: boolean @@ -28,7 +31,7 @@ export const getReverseClaimsFactory = (sparqlEndpoint: Url) => { // Allow to request values for several properties at once properties = forceArray(properties) - properties.forEach(o => validate.propertyId(o)) + properties.forEach(o => { validate.propertyId(o) }) const valueBlock = getValueBlock(values, valueFn, properties, filter) let sparql = `SELECT DISTINCT ?subject WHERE { ${valueBlock} }` @@ -37,40 +40,38 @@ export const getReverseClaimsFactory = (sparqlEndpoint: Url) => { } } -const getValueBlock = (values, valueFn, properties, filter) => { - properties = properties.map(prefixifyProperty).join('|') +const getValueBlock = (values: Values, valueFn: ValueFn, properties: readonly PropertyId[], filter: string) => { + const propertiesString = properties.map(property => 'wdt:' + property).join('|') if (!(values instanceof Array)) { - return valueFn(properties, getValueString(values), filter) + return valueFn(propertiesString, getValueString(values), filter) } const valuesBlocks = values .map(getValueString) - .map(valStr => valueFn(properties, valStr, filter)) + .map(valStr => valueFn(propertiesString, valStr, filter)) return '{ ' + valuesBlocks.join('} UNION {') + ' }' } -const getValueString = value => { - if (isItemId(value)) { - value = `wd:${value}` - } else if (typeof value === 'string') { - value = `'${value}'` +const getValueString = (value: Value) => { + if (typeof value === 'string') { + return isItemId(value) ? `wd:${value}` : `'${value}'` } - return value + return String(value) } -const directValueQuery = (properties, value, filter) => { +type ValueFn = (properties: string, value: string, filter: string) => string + +const directValueQuery: ValueFn = (properties, value, filter) => { return `?subject ${properties} ${value} . ${filter}` } // Discussion on how to make this query optimal: // http://stackoverflow.com/q/43073266/3324977 -const caseInsensitiveValueQuery = (properties, value, filter) => { +const caseInsensitiveValueQuery: ValueFn = (properties, value, filter) => { return `?subject ${properties} ?value . FILTER (lcase(?value) = ${value.toLowerCase()}) ${filter}` } - -const prefixifyProperty = property => 'wdt:' + property diff --git a/src/queries/get_revisions.ts b/src/queries/get_revisions.ts index 341cbabd..54ca1124 100644 --- a/src/queries/get_revisions.ts +++ b/src/queries/get_revisions.ts @@ -1,8 +1,8 @@ import * as validate from '../helpers/validate.js' import { forceArray, rejectObsoleteInterface } from '../utils/utils.js' import type { EntityPageTitle } from '../types/entity.js' -import type { ApiQueryParameters, UrlResultFormat } from '../types/options.js' -import type { BuildUrlFunction } from '../utils/build_url.js' +import type { UrlResultFormat } from '../types/options.js' +import type { ApiQueryParameters, BuildUrlFunction } from '../utils/build_url.js' // See https://www.wikidata.org/w/api.php?action=help&modules=query+revisions @@ -22,7 +22,7 @@ export function getRevisionsFactory (buildUrl: BuildUrlFunction) { return function getRevisions ({ ids, format, limit, start, end, prop, user, excludeuser, tag }: GetRevisionsOptions) { rejectObsoleteInterface(arguments) ids = forceArray(ids) - ids.forEach(o => validate.entityPageTitle(o)) + ids.forEach(o => { validate.entityPageTitle(o) }) const uniqueId = ids.length === 1 const query: ApiQueryParameters = { diff --git a/src/queries/search_entities.ts b/src/queries/search_entities.ts index dcfc3f85..ea573880 100644 --- a/src/queries/search_entities.ts +++ b/src/queries/search_entities.ts @@ -28,7 +28,7 @@ export const searchEntitiesFactory = (buildUrl: BuildUrlFunction) => { uselang = uselang || language if (!(search && search.length > 0)) throw new Error("search can't be empty") - if (!isOfType(EntityTypes, type)) throw new Error(`invalid type: ${type}`) + if (!isOfType(EntityTypes, type)) throw new Error(`invalid type: ${String(type)}`) return buildUrl({ action: 'wbsearchentities', diff --git a/src/types/claim.ts b/src/types/claim.ts index 014b8a9f..005c3d95 100644 --- a/src/types/claim.ts +++ b/src/types/claim.ts @@ -1,9 +1,28 @@ import type { PropertyId } from './entity.js' -import type { parsers } from '../helpers/parse_claim.js' +import type { SnakValue } from './snakvalue.js' export type Rank = 'normal' | 'preferred' | 'deprecated' export type SnakType = 'value' | 'somevalue' | 'novalue' -export type DataType = keyof typeof parsers +export const DataTypes = [ + 'commonsMedia', + 'external-id', + 'geo-shape', + 'globe-coordinate', + 'math', + 'monolingualtext', + 'musical-notation', + 'quantity', + 'string', + 'tabular-data', + 'time', + 'url', + 'wikibase-form', + 'wikibase-item', + 'wikibase-lexeme', + 'wikibase-property', + 'wikibase-sense', +] as const +export type DataType = typeof DataTypes[number] export interface Claim { id: string @@ -11,7 +30,7 @@ export interface Claim { rank: Rank type: DataType qualifiers?: Qualifiers - 'qualifiers-order'?: string[] + 'qualifiers-order'?: PropertyId[] references?: Reference[] } @@ -24,61 +43,18 @@ export type Snaks = Record export interface Snak { // A mainsnak object won't have an id, as its already on the claim id?: string - datatype: string + datatype: DataType datavalue?: SnakValue hash: string property: string snaktype: SnakType } -export interface SnakValue { - type: DataType - value: unknown -} - -export interface ClaimSnakTimeValue extends SnakValue { - type: 'time' - value: { - after: number - before: number - calendermodel: string - precision: number - time: string - timezone: number - } -} - -export interface ClaimSnakQuantity extends SnakValue { - type: 'quantity' - value: { - amount: string - unit: string - upperBound?: string - lowerBound?: string - } -} - -export interface ClaimSnakString extends SnakValue { - type: 'string' - value: string -} - -export interface SnakEntityValue extends SnakValue { - type: 'wikibase-entityid' - value: { - id: string - 'numeric-id': number - 'entity-type': string - } -} - -export type ClaimSnakWikibaseItem = SnakEntityValue - -export interface Qualifier extends Snak { +export interface QualifierSnak extends Snak { id: string } -export type PropertyQualifiers = Qualifier[] +export type PropertyQualifiers = QualifierSnak[] export type Qualifiers = Record @@ -89,7 +65,7 @@ export interface ReferenceSnak extends Snak { export interface Reference { hash: string snaks: Record - 'snaks-order': string[] + 'snaks-order': PropertyId[] } export type References = Reference[] diff --git a/src/types/entity.ts b/src/types/entity.ts index 97d05f55..6a7be619 100644 --- a/src/types/entity.ts +++ b/src/types/entity.ts @@ -1,5 +1,5 @@ import type { Claims, DataType } from './claim.js' -import type { Forms, Senses, SimplifiedForms, SimplifiedSenses } from './lexeme.js' +import type { Form, Sense, SimplifiedForm, SimplifiedSense } from './lexeme.js' import type { SimplifiedClaims } from './simplify_claims.js' import type { SimplifiedSitelinks, Sitelinks } from './sitelinks.js' import type { Aliases, Descriptions, Labels, Lemmas, SimplifiedAliases, SimplifiedDescriptions, SimplifiedLabels, SimplifiedLemmas } from './terms.js' @@ -20,19 +20,19 @@ export type RevisionId = `${number}` export type PropertyClaimsId = `${EntityId}#${PropertyId}` export type EntityId = NonNestedEntityId | FormId | SenseId -export type NonNestedEntityId = ItemId | PropertyId | LexemeId | MediaInfoId -export type NamespacedEntityId = `Item:${ItemId}` | `Lexeme:${LexemeId}` | `Property:${PropertyId}` +export type NonNestedEntityId = ItemId | PropertyId | LexemeId | EntitySchemaId | MediaInfoId +export type NamespacedEntityId = `Item:${ItemId}` | `Lexeme:${LexemeId}` | `Property:${PropertyId}` | `EntitySchema:${EntitySchemaId}` export type Guid = string export type Hash = string -export type Entity = (Property | Item | Lexeme) +export type Entity = Property | Item | Lexeme export type EntityPageTitle = NamespacedEntityId | ItemId export type Entities = Record export interface Property extends EntityInfo { - id: PropertyId, - type: 'property', + id: PropertyId + type: 'property' datatype?: DataType labels?: Labels descriptions?: Descriptions @@ -41,8 +41,8 @@ export interface Property extends EntityInfo { } export interface Item extends EntityInfo { - id: ItemId, - type: 'item', + id: ItemId + type: 'item' labels?: Labels descriptions?: Descriptions aliases?: Aliases @@ -51,13 +51,14 @@ export interface Item extends EntityInfo { } export interface Lexeme extends EntityInfo { - id: LexemeId, - type: 'lexeme', + id: LexemeId + type: 'lexeme' lexicalCategory: ItemId language: ItemId lemmas?: Lemmas - forms?: Forms - senses?: Senses + claims?: Claims + forms?: Form[] + senses?: Sense[] } export interface EntityInfo { @@ -70,37 +71,38 @@ export interface EntityInfo { } export interface SimplifiedEntityInfo { - id: EntityId modified?: string } export interface SimplifiedItem extends SimplifiedEntityInfo { - type: 'item', + id: ItemId + type: 'item' labels?: SimplifiedLabels descriptions?: SimplifiedDescriptions aliases?: SimplifiedAliases claims?: SimplifiedClaims sitelinks?: SimplifiedSitelinks - lexicalCategory: string } export interface SimplifiedProperty extends SimplifiedEntityInfo { - type: 'property', - datatype: DataType, + id: PropertyId + type: 'property' + datatype: DataType labels?: SimplifiedLabels descriptions?: SimplifiedDescriptions aliases?: SimplifiedAliases claims?: SimplifiedClaims - lexicalCategory: string } export interface SimplifiedLexeme extends SimplifiedEntityInfo { - type: 'lexeme', + id: LexemeId + type: 'lexeme' lexicalCategory: ItemId language: ItemId lemmas?: SimplifiedLemmas - forms?: SimplifiedForms - senses?: SimplifiedSenses + claims?: SimplifiedClaims + forms?: SimplifiedForm[] + senses?: SimplifiedSense[] } export type SimplifiedEntity = SimplifiedProperty | SimplifiedItem | SimplifiedLexeme diff --git a/src/types/lexeme.ts b/src/types/lexeme.ts index dd6f81bc..5ea8632d 100644 --- a/src/types/lexeme.ts +++ b/src/types/lexeme.ts @@ -1,11 +1,8 @@ import type { Claims } from './claim.js' -import type { FormId, ItemId, PropertyId, SenseId } from './entity.js' +import type { FormId, ItemId, SenseId } from './entity.js' import type { SimplifiedClaims } from './simplify_claims.js' import type { Glosses, Representations, SimplifiedGlosses, SimplifiedRepresentations } from './terms.js' -export type Forms = Record -export type Senses = Record - export interface Form { id: FormId representations?: Representations @@ -19,9 +16,6 @@ export interface Sense { claims?: Claims } -export type SimplifiedForms = Record -export type SimplifiedSenses = Record - export interface SimplifiedForm { id: FormId representations?: SimplifiedRepresentations diff --git a/src/types/options.ts b/src/types/options.ts index 66667f81..c328e88b 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -11,8 +11,6 @@ export type Props = 'info' | 'sitelinks' | 'sitelinks/urls' | 'aliases' | 'label export type UrlResultFormat = 'xml' | 'json' export type WmLanguageCode = typeof languages[number] -export type ApiQueryParameters = Record - // export type Url = `http${string}` export type Url = string diff --git a/src/types/simplify_claims.ts b/src/types/simplify_claims.ts index 1e6b665f..4d1ab698 100644 --- a/src/types/simplify_claims.ts +++ b/src/types/simplify_claims.ts @@ -1,25 +1,26 @@ import type { DataType, Rank } from './claim.js' import type { Guid, PropertyId } from './entity.js' -import type { timeConverters } from '../helpers/parse_claim.js' +import type { TimeConverter, TimeConverterFn } from '../helpers/wikibase_time.js' export interface SimplifySnakOptions { entityPrefix?: string propertyPrefix?: string - keepRichValues?: boolean - keepTypes?: boolean - keepQualifiers?: boolean - keepReferences?: boolean - keepIds?: boolean + keepAll?: boolean keepHashes?: boolean + keepIds?: boolean + keepQualifiers?: boolean keepRanks?: boolean + keepReferences?: boolean + keepRichValues?: boolean keepSnaktypes?: boolean - keepAll?: boolean - timeConverter?: keyof typeof timeConverters + keepTypes?: boolean + timeConverter?: TimeConverter | TimeConverterFn novalueValue?: any somevalueValue?: any } export interface SimplifySnaksOptions extends SimplifySnakOptions { + areSubSnaks?: boolean keepNonTruthy?: boolean keepNonDeprecated?: boolean } @@ -40,14 +41,10 @@ export type SimplifiedSnaks = Record export type SimplifiedQualifier = SimplifiedSnak export type SimplifiedPropertyQualifiers = SimplifiedQualifier[] -export interface SimplifiedQualifiers { - [property: string]: SimplifiedPropertyQualifiers -} +export type SimplifiedQualifiers = Record export type SimplifiedReferenceSnak = SimplifiedSnak -export interface SimplifiedReference { - [property: string]: SimplifiedReferenceSnak -} +export type SimplifiedReference = Record export type SimplifiedReferences = SimplifiedReference[] export type SimplifiedSnak = string | number | CustomSimplifiedSnak diff --git a/src/types/sitelinks.ts b/src/types/sitelinks.ts index 97b84bfe..cced3b39 100644 --- a/src/types/sitelinks.ts +++ b/src/types/sitelinks.ts @@ -17,4 +17,4 @@ export interface Sitelink { export type Sitelinks = Partial> -export type SimplifiedSitelinks = Partial> +export type SimplifiedSitelinks = Partial> diff --git a/src/types/snakvalue.ts b/src/types/snakvalue.ts new file mode 100644 index 00000000..ae8f5cc3 --- /dev/null +++ b/src/types/snakvalue.ts @@ -0,0 +1,68 @@ +import type { EntityType } from './entity.js' +import type { WmLanguageCode } from './options.js' + +export type SnakValue = + | SnakEntityValue + | SnakGlobeCoordinateValue + | SnakMonolingualTextValue + | SnakQuantityValue + | SnakStringValue + | SnakTimeValue + +export interface SnakEntityValue { + readonly type: 'wikibase-entityid' + value: { + 'numeric-id': number + 'entity-type': EntityType + } | { + id: string + 'numeric-id'?: number + 'entity-type': EntityType + } +} + +export interface SnakGlobeCoordinateValue { + readonly type: 'globecoordinate' + value: { + latitude: number + longitude: number + altitude?: number + precision: number + globe: string + } +} + +export interface SnakMonolingualTextValue { + readonly type: 'monolingualtext' + value: { + language: WmLanguageCode + text: string + } +} + +export interface SnakQuantityValue { + readonly type: 'quantity' + value: { + amount: string + unit: string + upperBound?: string + lowerBound?: string + } +} + +export interface SnakStringValue { + readonly type: 'string' + value: string +} + +export interface SnakTimeValue { + readonly type: 'time' + value: { + after: number + before: number + calendarmodel: string + precision: number + time: string + timezone: number + } +} diff --git a/src/types/sparql.ts b/src/types/sparql.ts index 570b3d9d..262dff87 100644 --- a/src/types/sparql.ts +++ b/src/types/sparql.ts @@ -1,13 +1,22 @@ export type SparqlValueRaw = string | number | boolean export type SparqlValueType = SparqlValueRaw | Record +export interface SparqlValueObj { + readonly type: 'uri' | 'bnode' + readonly datatype?: string + readonly value: string +} + export interface SparqlResults { head: { vars: string[] } results: { - bindings: unknown[] + bindings: Array> } } -export type SimplifiedSparqlResults = Record[] | SparqlValueRaw[] +export type SimplifiedSparqlResultRecord = Array> +export type SimplifiedSparqlResultMinified = SparqlValueRaw[] + +export type SimplifiedSparqlResults = SimplifiedSparqlResultRecord | SimplifiedSparqlResultMinified diff --git a/src/types/terms.ts b/src/types/terms.ts index 327b2130..a36516fd 100644 --- a/src/types/terms.ts +++ b/src/types/terms.ts @@ -1,8 +1,8 @@ import type { WmLanguageCode } from './options.js' -type WmLanguageRecord = Partial>> +type WmLanguageRecord = Readonly>> -export type Term = { +export interface Term { readonly language: WmLanguageCode readonly value: string } diff --git a/src/types/wbgetentities.ts b/src/types/wbgetentities.ts index 7c87c95c..12a09087 100644 --- a/src/types/wbgetentities.ts +++ b/src/types/wbgetentities.ts @@ -1,13 +1,14 @@ import type { UrlResultFormat } from './options.js' -export type WbGetEntities = { - action: 'wbgetentities', - titles?: string, - sites?: string, - ids?: string, - format: UrlResultFormat, - normalize?: true, - languages?: string, - props?: string, - redirects?: 'yes' | 'no', // Default: yes +export interface WbGetEntities { + action: 'wbgetentities' + titles?: string + sites?: string + ids?: string + format: UrlResultFormat + normalize?: true + languages?: string + props?: string + /** Default: yes */ + redirects?: 'yes' | 'no' } diff --git a/src/utils/build_url.ts b/src/utils/build_url.ts index f3f0f3e6..d57f540e 100644 --- a/src/utils/build_url.ts +++ b/src/utils/build_url.ts @@ -1,12 +1,17 @@ -import type { ApiQueryParameters, Url } from '../types/options.js' +import type { Url } from '../types/options.js' const isBrowser = typeof location !== 'undefined' && typeof document !== 'undefined' +type ApiQueryValue = string | number | true +export type ApiQueryParameters = Record + +export type BuildUrlFunction = (options: Readonly>>) => Url + export function buildUrlFactory (instanceApiEndpoint: Url): BuildUrlFunction { - return function (queryObj: ApiQueryParameters): Url { + return queryObj => { // Request CORS headers if the request is made from a browser // See https://www.wikidata.org/w/api.php ('origin' parameter) - if (isBrowser) queryObj.origin = '*' + if (isBrowser) queryObj = { ...queryObj, origin: '*' } const queryEntries = Object.entries(queryObj) // Remove null or undefined parameters @@ -16,5 +21,3 @@ export function buildUrlFactory (instanceApiEndpoint: Url): BuildUrlFunction { return instanceApiEndpoint + '?' + query } } - -export type BuildUrlFunction = (options: ApiQueryParameters) => Url diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ad30c692..6ebc51bd 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -56,3 +56,14 @@ export function rejectObsoleteInterface (args: IArguments): void { export function isOfType (all: readonly T[], element: unknown): element is T { return typeof element === 'string' && (all as readonly string[]).includes(element) } + +/** key is a key on the object */ +export function isAKey (obj: Readonly>>, key: unknown): key is T { + return typeof key === 'string' && Object.keys(obj).includes(key) +} + +/** like Object.entries() but with typed key */ +export function typedEntries (input: Readonly>>): Array<[K, V]> { + // @ts-expect-error string is not assignable to K as K is more specific + return Object.entries(input) +} diff --git a/src/wikibase-sdk.ts b/src/wikibase-sdk.ts index 03bda2b8..119b7133 100644 --- a/src/wikibase-sdk.ts +++ b/src/wikibase-sdk.ts @@ -3,6 +3,7 @@ import * as parse from './helpers/parse_responses.js' import * as rankHelpers from './helpers/rank.js' import * as simplify from './helpers/simplify.js' import * as sitelinksHelpers from './helpers/sitelinks.js' +import * as timeHelpers from './helpers/wikibase_time.js' import { cirrusSearchPagesFactory } from './queries/cirrus_search.js' import { getEntitiesFactory } from './queries/get_entities.js' import { getEntitiesFromSitelinksFactory } from './queries/get_entities_from_sitelinks.js' @@ -24,11 +25,12 @@ const common = { simplify, parse, ...helpers, - ...sitelinksHelpers, ...rankHelpers, + ...sitelinksHelpers, + ...timeHelpers, } as const -type ApiQueries = { +interface ApiQueries { readonly searchEntities: ReturnType readonly cirrusSearchPages: ReturnType readonly getEntities: ReturnType @@ -37,11 +39,11 @@ type ApiQueries = { readonly getEntityRevision: ReturnType readonly getEntitiesFromSitelinks: ReturnType } -type SparqlQueries = { +interface SparqlQueries { readonly sparqlQuery: ReturnType readonly getReverseClaims: ReturnType } -type Instance = { +interface Instance { readonly root: Url readonly apiEndpoint: Url } diff --git a/tests/get_many_entities.ts b/tests/get_many_entities.ts index 3bb7313f..3b71ea38 100644 --- a/tests/get_many_entities.ts +++ b/tests/get_many_entities.ts @@ -6,7 +6,7 @@ import { parseUrlQuery } from './lib/utils.js' import type { ItemId } from '../src/types/entity.js' const getManyEntities = getManyEntitiesFactory(buildUrl) -const manyIds = range(1, 80).map(id => `Q${id}` as ItemId) +const manyIds = range(1, 80).map(id => `Q${id}`) describe('wikidata getManyEntities', () => { describe('general', () => { @@ -61,7 +61,7 @@ describe('wikidata getManyEntities', () => { it('should add a redirects parameter if false', () => { const urls = getManyEntities({ ids: [ 'Q535' ], redirects: false }) - const url = urls[0] as string + const url = urls[0] should(parseUrlQuery(url).redirects).equal('no') }) }) diff --git a/tests/helpers.ts b/tests/helpers.ts index 0c115ecc..02c130a7 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -16,119 +16,9 @@ import { isPropertyClaimsId, isPropertyId, isSenseId, - wikibaseTimeToEpochTime, - wikibaseTimeToISOString, - wikibaseTimeToSimpleDay, } from '../src/helpers/helpers.js' -import { readJsonFile } from './lib/utils.js' - -const Q970917 = readJsonFile('./tests/data/Q970917.json') describe('helpers', () => { - const ISOtime = '2014-05-14T00:00:00.000Z' - const wdTime = '+2014-05-14T00:00:00Z' - const epoch = 1400025600000 - const ISOnegativeTime = '-000044-03-15T00:00:00.000Z' - const negativeWdTime = '-0044-03-15T00:00:00Z' - const negativeEpoch = -63549360000000 - - describe('wikibaseTimeToEpochTime', () => { - it('env', () => { - should(new Date(epoch).toISOString()).equal(ISOtime) - should(new Date(negativeEpoch).toISOString()).equal(ISOnegativeTime) - }) - - it('should return a number (epoch time)', () => { - should(wikibaseTimeToEpochTime(wdTime)).be.a.Number() - }) - - it('should return a number for negative time', () => { - should(wikibaseTimeToEpochTime(negativeWdTime)).be.a.Number() - }) - - it('should return the right number', () => { - should(wikibaseTimeToEpochTime(wdTime)).equal(epoch) - }) - - it('should return the right number for negative time too', () => { - should(wikibaseTimeToEpochTime(negativeWdTime)).equal(negativeEpoch) - }) - - it('should accept a value object', () => { - should(wikibaseTimeToEpochTime(Q970917.claims.P569[0].mainsnak.datavalue.value)).equal(-3160944000000) - should(wikibaseTimeToEpochTime(Q970917.claims.P569[1].mainsnak.datavalue.value)).equal(657417600000) - should(wikibaseTimeToEpochTime(Q970917.claims.P569[2].mainsnak.datavalue.value)).equal(631152000000) - }) - }) - - describe('wikibaseTimeToISOString', () => { - it('should convert wikibase date to ISO date', () => { - should(wikibaseTimeToISOString('+1885-05-22T00:00:00Z')).equal('1885-05-22T00:00:00.000Z') - should(wikibaseTimeToISOString('+0180-03-17T00:00:00Z')).equal('0180-03-17T00:00:00.000Z') - should(wikibaseTimeToISOString('-0398-00-00T00:00:00Z')).equal('-000398-01-01T00:00:00.000Z') - should(wikibaseTimeToISOString('-34000-00-00T00:00:00Z')).equal('-034000-01-01T00:00:00.000Z') - should(wikibaseTimeToISOString('+34000-00-00T00:00:00Z')).equal('+034000-01-01T00:00:00.000Z') - }) - - it('should return a valid time for possible invalid dates', () => { - should(wikibaseTimeToISOString('+1953-00-00T00:00:00Z')).equal('1953-01-01T00:00:00.000Z') - should(wikibaseTimeToISOString('+1953-11-00T00:00:00Z')).equal('1953-11-01T00:00:00.000Z') - }) - - it('should return a valid time even for possible invalid negative date', () => { - should(wikibaseTimeToISOString('-1953-00-00T00:00:00Z')).equal('-001953-01-01T00:00:00.000Z') - should(wikibaseTimeToISOString('-1953-11-00T00:00:00Z')).equal('-001953-11-01T00:00:00.000Z') - }) - - it('should return a valid time for dates far in the past', () => { - should(wikibaseTimeToISOString('-13798000000-00-00T00:00:00Z')).equal('-13798000000-01-01T00:00:00Z') - should(wikibaseTimeToISOString('-13798000000-02-00T00:00:00Z')).equal('-13798000000-02-01T00:00:00Z') - should(wikibaseTimeToISOString('-13798000000-02-07T15:00:00Z')).equal('-13798000000-02-07T15:00:00Z') - }) - - it('should return a valid time for dates far in the future', () => { - should(wikibaseTimeToISOString('+13798000000-00-00T00:00:00Z')).equal('+13798000000-01-01T00:00:00Z') - should(wikibaseTimeToISOString('+13798000000-02-00T00:00:00Z')).equal('+13798000000-02-01T00:00:00Z') - should(wikibaseTimeToISOString('+13798000000-02-07T15:00:00Z')).equal('+13798000000-02-07T15:00:00Z') - }) - - it('should accept a value object', () => { - should(wikibaseTimeToISOString(Q970917.claims.P569[0].mainsnak.datavalue.value)).equal('1869-11-01T00:00:00.000Z') - should(wikibaseTimeToISOString(Q970917.claims.P569[1].mainsnak.datavalue.value)).equal('1990-11-01T00:00:00.000Z') - should(wikibaseTimeToISOString(Q970917.claims.P569[2].mainsnak.datavalue.value)).equal('1990-01-01T00:00:00.000Z') - }) - }) - - describe('wikibaseTimeToSimpleDay', () => { - it('should convert wikibase date with year precision to simple-day', () => { - should(wikibaseTimeToSimpleDay('+1953-00-00T00:00:00Z')).equal('1953') - should(wikibaseTimeToSimpleDay('-1953-00-00T00:00:00Z')).equal('-1953') - should(wikibaseTimeToSimpleDay('+13-00-00T00:00:00Z')).equal('13') - should(wikibaseTimeToSimpleDay('-13-00-00T00:00:00Z')).equal('-13') - should(wikibaseTimeToSimpleDay('-0100-00-00T00:00:00Z')).equal('-100') - }) - - it('should convert wikibase date with month precision to simple-day', () => { - should(wikibaseTimeToSimpleDay('+1953-01-00T00:00:00Z')).equal('1953-01') - should(wikibaseTimeToSimpleDay('-1953-01-00T00:00:00Z')).equal('-1953-01') - should(wikibaseTimeToSimpleDay('+13-01-00T00:00:00Z')).equal('13-01') - should(wikibaseTimeToSimpleDay('-13-01-00T00:00:00Z')).equal('-13-01') - should(wikibaseTimeToSimpleDay('-0044-03-00T00:00:00Z')).equal('-44-03') - }) - - it('should convert wikibase date with day precision or finer to simple-day', () => { - should(wikibaseTimeToSimpleDay('+1953-01-01T00:00:00Z')).equal('1953-01-01') - should(wikibaseTimeToSimpleDay('-1953-01-01T00:00:00Z')).equal('-1953-01-01') - should(wikibaseTimeToSimpleDay('+1953-01-01T13:45:00Z')).equal('1953-01-01') - should(wikibaseTimeToSimpleDay('-1953-01-01T13:45:00Z')).equal('-1953-01-01') - should(wikibaseTimeToSimpleDay('-0044-03-01T00:00:00Z')).equal('-44-03-01') - }) - - it('should accept a value object', () => { - should(wikibaseTimeToSimpleDay(Q970917.claims.P569[0].mainsnak.datavalue.value)).equal('1869-11') - }) - }) - describe('isEntityId', () => { it('should accept all supported entity types ids', () => { should(isEntityId('Q571')).be.true() diff --git a/tests/parse.ts b/tests/parse.ts index 1e44fe99..6bb9e60d 100644 --- a/tests/parse.ts +++ b/tests/parse.ts @@ -15,12 +15,10 @@ describe('parse', () => { it('should parse an entities response', () => { const entities = parse.entities(wbgetentitiesResponse) should(entities).be.an.Object() + if (entities.Q3235026.type !== 'item') throw new Error('should be an item') should(entities.Q3235026).be.an.Object() - // @ts-expect-error should(entities.Q3235026.labels).be.an.Object() - // @ts-expect-error should(entities.Q3235026.descriptions).be.an.Object() - // @ts-expect-error should(entities.Q3235026.claims).be.an.Object() }) }) diff --git a/tests/rank.ts b/tests/rank.ts index b8b799e7..75741166 100644 --- a/tests/rank.ts +++ b/tests/rank.ts @@ -2,8 +2,9 @@ import { cloneDeep } from 'lodash-es' import should from 'should' import { truthyClaims, truthyPropertyClaims } from '../src/helpers/rank.js' import { readJsonFile } from './lib/utils.js' +import type { Item } from '../src/types/entity.js' -const Q4115189 = readJsonFile('./tests/data/Q4115189.json') +const Q4115189 = readJsonFile('./tests/data/Q4115189.json') as Item describe('truthyClaims', () => { it('should filter-out non-truthy claims', () => { diff --git a/tests/simplify_claims.ts b/tests/simplify_claims.ts index 4bdfae4f..72c84728 100644 --- a/tests/simplify_claims.ts +++ b/tests/simplify_claims.ts @@ -3,20 +3,21 @@ import should from 'should' import { simplifyClaim, simplifyPropertyClaims, simplifyClaims } from '../src/helpers/simplify_claims.js' import { uniq } from '../src/utils/utils.js' import { readJsonFile } from './lib/utils.js' +import type { Item, Lexeme } from '../src/types/entity.js' import type { SimplifySnakOptions } from '../src/types/simplify_claims.js' -const L525 = readJsonFile('./tests/data/L525.json') -const Q1 = readJsonFile('./tests/data/Q1.json') -const Q2112 = readJsonFile('./tests/data/Q2112.json') -const Q217447 = readJsonFile('./tests/data/Q217447.json') -const Q22002395 = readJsonFile('./tests/data/Q22002395.json') -const Q271094 = readJsonFile('./tests/data/Q271094.json') -const Q275937 = readJsonFile('./tests/data/Q275937.json') -const Q328212 = readJsonFile('./tests/data/Q328212.json') -const Q4115189 = readJsonFile('./tests/data/Q4115189.json') -const Q4132785 = readJsonFile('./tests/data/Q4132785.json') -const Q571 = readJsonFile('./tests/data/Q571.json') -const Q646148 = readJsonFile('./tests/data/Q646148.json') +const L525 = readJsonFile('./tests/data/L525.json') as Lexeme +const Q1 = readJsonFile('./tests/data/Q1.json') as Item +const Q2112 = readJsonFile('./tests/data/Q2112.json') as Item +const Q217447 = readJsonFile('./tests/data/Q217447.json') as Item +const Q22002395 = readJsonFile('./tests/data/Q22002395.json') as Item +const Q271094 = readJsonFile('./tests/data/Q271094.json') as Item +const Q275937 = readJsonFile('./tests/data/Q275937.json') as Item +const Q328212 = readJsonFile('./tests/data/Q328212.json') as Item +const Q4115189 = readJsonFile('./tests/data/Q4115189.json') as Item +const Q4132785 = readJsonFile('./tests/data/Q4132785.json') as Item +const Q571 = readJsonFile('./tests/data/Q571.json') as Item +const Q646148 = readJsonFile('./tests/data/Q646148.json') as Item const emptyValues = readJsonFile('./tests/data/empty_values.json') const lexemeClaim = readJsonFile('./tests/data/lexeme_claim.json') const oldClaimFormat = readJsonFile('./tests/data/old_claim_format.json') diff --git a/tests/simplify_entity.ts b/tests/simplify_entity.ts index 9dc44903..d88ff46c 100644 --- a/tests/simplify_entity.ts +++ b/tests/simplify_entity.ts @@ -1,12 +1,12 @@ -// @ts-nocheck import { cloneDeep, pick } from 'lodash-es' import should from 'should' import { simplifyEntity, simplifyEntities } from '../src/helpers/simplify_entity.js' import { readJsonFile } from './lib/utils.js' +import type { Item, Lexeme, Property } from '../src/types/entity.js' -const L525 = readJsonFile('./tests/data/L525.json') -const P8098 = readJsonFile('./tests/data/P8098.json') -const Q571 = readJsonFile('./tests/data/Q571.json') +const L525 = readJsonFile('./tests/data/L525.json') as Lexeme +const P8098 = readJsonFile('./tests/data/P8098.json') as Property +const Q571 = readJsonFile('./tests/data/Q571.json') as Item describe('simplify.entity', () => { it('should be a function', () => { @@ -63,23 +63,29 @@ describe('simplify.entity', () => { it('should pass options down to subfunctions', () => { const Q571Clone = cloneDeep(Q571) const simplifiedEntity = simplifyEntity(Q571Clone, { keepQualifiers: true, keepIds: true, addUrl: true }) + if (simplifiedEntity.type !== 'item') throw new TypeError('should be item') should(simplifiedEntity.labels.fr).equal('livre') should(simplifiedEntity.descriptions.fr).equal('document écrit formé de pages reliées entre elles') should(simplifiedEntity.aliases.pl).be.an.Array() should(simplifiedEntity.aliases.pl[0]).equal('Tom') should(simplifiedEntity.claims.P279).be.an.Array() should(simplifiedEntity.claims.P279[0]).be.an.Object() + // @ts-expect-error should(simplifiedEntity.claims.P279[0].value).equal('Q2342494') should(simplifiedEntity.sitelinks.afwiki).be.an.Object() + // @ts-expect-error should(simplifiedEntity.sitelinks.afwiki.title).equal('Boek') + // @ts-expect-error should(simplifiedEntity.sitelinks.afwiki.url).equal('https://af.wikipedia.org/wiki/Boek') }) it('should accept partial entities', () => { const Q571Clone = cloneDeep(Q571) + // @ts-expect-error very very partial entity const emptyEntity = simplifyEntity({}) should(Object.keys(emptyEntity).length).equal(3) const partialEntity = simplifyEntity(pick(Q571Clone, 'id', 'type', 'labels')) + if (partialEntity.type !== 'item') throw new TypeError('should be item') should(Object.keys(partialEntity).length).equal(4) should(partialEntity.labels).be.an.Object() should(partialEntity.labels.fr).equal('livre') @@ -91,6 +97,7 @@ describe('simplify.entities', () => { const Q571Clone = cloneDeep(Q571) const entities = { Q571: Q571Clone } const simplifiedEntities = simplifyEntities(entities) + if (simplifiedEntities.Q571.type !== 'item') throw new TypeError('should be item') should(simplifiedEntities.Q571.labels.fr).equal('livre') should(simplifiedEntities.Q571.descriptions.fr).equal('document écrit formé de pages reliées entre elles') should(simplifiedEntities.Q571.aliases.pl).be.an.Array() diff --git a/tests/simplify_forms.ts b/tests/simplify_forms.ts index 7eca9460..e1ed741e 100644 --- a/tests/simplify_forms.ts +++ b/tests/simplify_forms.ts @@ -1,12 +1,13 @@ -// @ts-nocheck import should from 'should' import { simplifyForms, simplifyForm } from '../src/helpers/simplify_forms.js' import { readJsonFile } from './lib/utils.js' +import type { Lexeme } from '../src/types/entity.js' -const L525 = readJsonFile('./tests/data/L525.json') +const L525 = readJsonFile('./tests/data/L525.json') as Lexeme describe('simplify.form', () => { it('should reject an object that isnt a form', () => { + // @ts-expect-error not a form should(() => simplifyForm({})).throw('invalid form object') }) @@ -36,12 +37,13 @@ describe('simplify.forms', () => { it('should simplify forms', () => { const simplifiedForms = simplifyForms(L525.forms) should(simplifiedForms).be.an.Array() - should(simplifiedForms).deepEqual(L525.forms.map(simplifyForm)) + should(simplifiedForms).deepEqual(L525.forms.map(form => simplifyForm(form))) }) it('should pass down options', () => { const simplifiedForms = simplifyForms(L525.forms, { keepIds: true }) should(simplifiedForms).be.an.Array() + // @ts-expect-error simplified claims can be a lot of things should(simplifiedForms[0].claims.P443[0].id).equal('L525-F1$079bdca7-5130-4f9f-bac9-e8d032c38263') }) }) diff --git a/tests/simplify_qualifiers.ts b/tests/simplify_qualifiers.ts index e71c2d9c..7cdca6b5 100644 --- a/tests/simplify_qualifiers.ts +++ b/tests/simplify_qualifiers.ts @@ -1,10 +1,12 @@ import should from 'should' import { simplifyQualifier, simplifyPropertyQualifiers, simplifyQualifiers } from '../src/helpers/simplify_claims.js' import { readJsonFile } from './lib/utils.js' +import type { TimeConverter, TimeConverterFn } from '../src/helpers/wikibase_time.js' +import type { Item } from '../src/types/entity.js' -const Q19180293 = readJsonFile('./tests/data/Q19180293.json') -const Q2112 = readJsonFile('./tests/data/Q2112.json') -const Q571 = readJsonFile('./tests/data/Q571.json') +const Q19180293 = readJsonFile('./tests/data/Q19180293.json') as Item +const Q2112 = readJsonFile('./tests/data/Q2112.json') as Item +const Q571 = readJsonFile('./tests/data/Q571.json') as Item describe('simplifyQualifier', () => { it('should simplify a qualifier', () => { @@ -37,13 +39,18 @@ describe('simplifyQualifier', () => { describe('time', () => { it('should respect timeConverter for qualifiers claims', () => { const qualifier = Q571.claims.P1709[0].qualifiers.P813[0] - const timeClaim = timeConverter => simplifyQualifier(qualifier, { timeConverter }) + const timeClaim = (timeConverter: TimeConverter | TimeConverterFn) => simplifyQualifier(qualifier, { timeConverter }) should(timeClaim('iso')).equal('2015-06-11T00:00:00.000Z') should(timeClaim('epoch')).equal(1433980800000) should(timeClaim('simple-day')).equal('2015-06-11') should(timeClaim('none')).equal('+2015-06-11T00:00:00Z') - const timeConverterFn = ({ time, precision }) => `foo/${time}/${precision}/bar` - should(timeClaim(timeConverterFn)).equal('foo/+2015-06-11T00:00:00Z/11/bar') + should(timeClaim( + timeObj => { + if (typeof timeObj !== 'object') throw new Error('expect WikibaseTimeObject') + const { time, precision } = timeObj + return `foo/${time}/${precision}/bar` + }, + )).equal('foo/+2015-06-11T00:00:00Z/11/bar') }) }) }) diff --git a/tests/simplify_references.ts b/tests/simplify_references.ts index 5a64610f..6fd55361 100644 --- a/tests/simplify_references.ts +++ b/tests/simplify_references.ts @@ -1,8 +1,9 @@ import should from 'should' import { simplifyReferences } from '../src/helpers/simplify_claims.js' import { readJsonFile } from './lib/utils.js' +import type { Item } from '../src/types/entity.js' -const Q217447 = readJsonFile('./tests/data/Q217447.json') +const Q217447 = readJsonFile('./tests/data/Q217447.json') as Item describe('simplifyReferences', () => { it('should simplify references', () => { diff --git a/tests/simplify_senses.ts b/tests/simplify_senses.ts index 6b7b5afb..37045642 100644 --- a/tests/simplify_senses.ts +++ b/tests/simplify_senses.ts @@ -1,12 +1,13 @@ -// @ts-nocheck import should from 'should' import { simplifySenses, simplifySense } from '../src/helpers/simplify_senses.js' import { readJsonFile } from './lib/utils.js' +import type { Lexeme } from '../src/types/entity.js' -const L525 = readJsonFile('./tests/data/L525.json') +const L525 = readJsonFile('./tests/data/L525.json') as Lexeme describe('simplify.sense', () => { it('should reject an object that isnt a sense', () => { + // @ts-expect-error invalid sense should(() => simplifySense({})).throw('invalid sense object') }) @@ -34,12 +35,13 @@ describe('simplify.senses', () => { it('should simplify senses', () => { const simplifiedSenses = simplifySenses(L525.senses) should(simplifiedSenses).be.an.Array() - should(simplifiedSenses).deepEqual(L525.senses.map(simplifySense)) + should(simplifiedSenses).deepEqual(L525.senses.map(sense => simplifySense(sense))) }) it('should pass down options', () => { const simplifiedSenses = simplifySenses(L525.senses, { keepIds: true }) should(simplifiedSenses).be.an.Array() + // @ts-expect-error simplified claims can be a lot of things should(simplifiedSenses[0].claims.P5137[0].id).equal('L525-S1$66D20252-8CEC-4DB1-8B00-D713CFF42E48') }) }) diff --git a/tests/simplify_sitelinks.ts b/tests/simplify_sitelinks.ts index a4e7e863..9cd0bf84 100644 --- a/tests/simplify_sitelinks.ts +++ b/tests/simplify_sitelinks.ts @@ -1,9 +1,9 @@ -// @ts-nocheck import should from 'should' import { simplifySitelinks } from '../src/helpers/simplify_sitelinks.js' import { readJsonFile, objLenght } from './lib/utils.js' +import type { Item } from '../src/types/entity.js' -const Q571 = readJsonFile('./tests/data/Q571.json') +const Q571 = readJsonFile('./tests/data/Q571.json') as Item describe('simplify.sitelinks', () => { it('should simplify sitelinks', () => { @@ -15,16 +15,20 @@ describe('simplify.sitelinks', () => { it('should preserve badges if requested with keepBadges=true', () => { const simplifiedSitelinks = simplifySitelinks(Q571.sitelinks, { keepBadges: true }) + if (typeof simplifiedSitelinks.enwiki != 'object') throw new Error('has to be an object') should(simplifiedSitelinks.enwiki.title).equal('Book') should(simplifiedSitelinks.enwiki.badges).deepEqual([]) + if (typeof simplifiedSitelinks.lawiki != 'object') throw new Error('has to be an object') should(simplifiedSitelinks.lawiki.title).equal('Liber') should(simplifiedSitelinks.lawiki.badges).deepEqual([ 'Q17437796' ]) }) it('should preserve badges if requested with keepAll=true', () => { const simplifiedSitelinks = simplifySitelinks(Q571.sitelinks, { keepBadges: true }) + if (typeof simplifiedSitelinks.enwiki != 'object') throw new Error('has to be an object') should(simplifiedSitelinks.enwiki.title).equal('Book') should(simplifiedSitelinks.enwiki.badges).deepEqual([]) + if (typeof simplifiedSitelinks.lawiki != 'object') throw new Error('has to be an object') should(simplifiedSitelinks.lawiki.title).equal('Liber') should(simplifiedSitelinks.lawiki.badges).deepEqual([ 'Q17437796' ]) }) @@ -34,12 +38,13 @@ describe('simplify.sitelinks', () => { }) it('should return an object with a URL if requested ', () => { - should(simplifySitelinks(Q571.sitelinks, { addUrl: true }).enwiki.url).equal('https://en.wikipedia.org/wiki/Book') + const result = simplifySitelinks(Q571.sitelinks, { addUrl: true }).enwiki + if (typeof result != 'object') throw new Error('has to be an object') + should(result.url).equal('https://en.wikipedia.org/wiki/Book') }) it('should not throw when a sitelink is null ', () => { const sitelinks = { frwiki: null } - // @ts-expect-error should(simplifySitelinks(sitelinks)).deepEqual(sitelinks) }) }) diff --git a/tests/simplify_sparql_results.ts b/tests/simplify_sparql_results.ts index 191af689..fafc44e3 100644 --- a/tests/simplify_sparql_results.ts +++ b/tests/simplify_sparql_results.ts @@ -1,18 +1,18 @@ -// @ts-nocheck import { cloneDeep } from 'lodash-es' import should from 'should' import { isEntityId, isGuid } from '../src/helpers/helpers.js' import { simplifySparqlResults } from '../src/helpers/simplify_sparql_results.js' import { readJsonFile } from './lib/utils.js' +import type { SparqlResults } from '../src/types/sparql.js' -const multiVarsData = readJsonFile('./tests/data/multi_vars_sparql_results.json') -const noDatatypeData = readJsonFile('./tests/data/no_datatype_sparql_results.json') -const propertiesList = readJsonFile('./tests/data/properties_list.json') -const resultsWithLabelsDescriptionsAndAliases = readJsonFile('./tests/data/results_with_labels_descriptions_and_aliases.json') -const singleVarData = readJsonFile('./tests/data/single_var_sparql_results.json') -const sparqlResultsWithNestedAssociatedVariables = readJsonFile('./tests/data/sparql_results_with_nested_associated_variables.json') -const sparqlResultsWithOptionalValues = readJsonFile('./tests/data/sparql_results_with_optional_values.json') -const sparqlResultsWithStatements = readJsonFile('./tests/data/sparql_results_with_statements.json') +const multiVarsData = readJsonFile('./tests/data/multi_vars_sparql_results.json') as SparqlResults +const noDatatypeData = readJsonFile('./tests/data/no_datatype_sparql_results.json') as SparqlResults +const propertiesList = readJsonFile('./tests/data/properties_list.json') as SparqlResults +const resultsWithLabelsDescriptionsAndAliases = readJsonFile('./tests/data/results_with_labels_descriptions_and_aliases.json') as SparqlResults +const singleVarData = readJsonFile('./tests/data/single_var_sparql_results.json') as SparqlResults +const sparqlResultsWithNestedAssociatedVariables = readJsonFile('./tests/data/sparql_results_with_nested_associated_variables.json') as SparqlResults +const sparqlResultsWithOptionalValues = readJsonFile('./tests/data/sparql_results_with_optional_values.json') as SparqlResults +const sparqlResultsWithStatements = readJsonFile('./tests/data/sparql_results_with_statements.json') as SparqlResults describe('wikidata simplify SPARQL results', () => { describe('common', () => { @@ -23,8 +23,10 @@ describe('wikidata simplify SPARQL results', () => { it('should parse the input if passed a JSON string', () => { const json = JSON.stringify(singleVarData) + // @ts-expect-error json is a string and not in the object form should(simplifySparqlResults(json)).be.an.Array() const json2 = JSON.stringify(multiVarsData) + // @ts-expect-error json is a string and not in the object form should(simplifySparqlResults(json2)).be.an.Array() }) }) @@ -32,6 +34,7 @@ describe('wikidata simplify SPARQL results', () => { it('should return an array of results objects', () => { const output = simplifySparqlResults(multiVarsData) should(output[0]).be.an.Object() + if (typeof output[0].entity !== 'object') throw new Error('has to be an object') should(output[0].entity.value).equal('Q3731207') should(output[0].entity.label).equal('Ercole Patti') should(output[0].year).equal(1903) @@ -52,15 +55,15 @@ describe('wikidata simplify SPARQL results', () => { it('should return an array of results values, filtering out blank nodes', () => { const output = simplifySparqlResults(singleVarData, { minimize: true }) should(output[0]).equal('Q112983') - output.forEach(result => should(isEntityId(result)).be.true()) + output.forEach(result => should(typeof result === 'string' && isEntityId(result)).be.true()) }) it('should return an array of results value object', () => { - const output = simplifySparqlResults(singleVarData, { minimize: false }) + const output = simplifySparqlResults(singleVarData) should(output[0]).deepEqual({ genre: 'Q112983' }) output.forEach(result => { should(result).be.an.Object() - if (result.genre) should(isEntityId(result.genre)).be.true() + if (result.genre) should(typeof result.genre === 'string' && isEntityId(result.genre)).be.true() }) }) }) @@ -71,6 +74,7 @@ describe('wikidata simplify SPARQL results', () => { resultsWithLabelsDescriptionsAndAliases.results.bindings.forEach((rawResult, i) => { const simplified = results[i] should(simplified.item).be.an.Object() + if (typeof simplified.item != 'object') throw new Error('has to be an object') should(simplified.item.value).be.a.String() if (rawResult.itemLabel) should(simplified.item.label).be.a.String() if (rawResult.itemDescription) should(simplified.item.description).be.a.String() @@ -87,6 +91,7 @@ describe('wikidata simplify SPARQL results', () => { propertiesList.results.bindings.forEach((rawResult, i) => { const simplified = results[i] should(simplified.property).be.an.Object() + if (typeof simplified.property != 'object') throw new Error('has to be an object') should(simplified.property.value).be.a.String() if (rawResult.propertyType) should(simplified.property.type).be.a.String() should(simplified.propertyType).not.be.ok() @@ -101,6 +106,7 @@ describe('wikidata simplify SPARQL results', () => { rawResults.results.bindings.forEach((rawResult, i) => { const simplified = results[i] should(simplified.item).be.an.Object() + if (typeof simplified.item != 'object') throw new Error('has to be an object') should(simplified.item.value).be.a.String() if (rawResult.itemDescription) should(simplified.item.description).be.a.String() if (rawResult.itemAltLabel) should(simplified.item.aliases).be.a.String() @@ -135,7 +141,7 @@ describe('wikidata simplify SPARQL results', () => { it('should convert statement URIs into claims GUIDs', () => { const rawResults = cloneDeep(sparqlResultsWithStatements) const results = simplifySparqlResults(rawResults, { minimize: true }) - results.forEach(result => should(isGuid(result)).be.true()) + results.forEach(result => should(typeof result === 'string' && isGuid(result)).be.true()) }) }) }) diff --git a/tests/simplify_text_attributes.ts b/tests/simplify_text_attributes.ts index e539b8af..a3a44354 100644 --- a/tests/simplify_text_attributes.ts +++ b/tests/simplify_text_attributes.ts @@ -1,8 +1,9 @@ import should from 'should' import { simplifyAliases, simplifyDescriptions, simplifyLabels } from '../src/helpers/simplify_text_attributes.js' import { readJsonFile, objLenght } from './lib/utils.js' +import type { Item } from '../src/types/entity.js' -const Q571 = readJsonFile('./tests/data/Q571.json') +const Q571 = readJsonFile('./tests/data/Q571.json') as Item describe('simplifyLabels', () => { it('should simplify labels', () => { @@ -13,6 +14,7 @@ describe('simplifyLabels', () => { }) it('should create a different object', () => { + // @ts-expect-error types are also different should(simplifyLabels(Q571.labels) === Q571.labels).be.false() }) @@ -34,6 +36,7 @@ describe('simplifyDescriptions', () => { }) it('should create a different object', () => { + // @ts-expect-error types are also different should(simplifyLabels(Q571.descriptions) === Q571.descriptions).be.false() }) }) @@ -49,6 +52,7 @@ describe('simplifyAliases', () => { }) it('should create a different object', () => { + // @ts-expect-error types are also different should(simplifyAliases(Q571.aliases) === Q571.aliases).be.false() }) }) diff --git a/tests/time.ts b/tests/time.ts new file mode 100644 index 00000000..516d8396 --- /dev/null +++ b/tests/time.ts @@ -0,0 +1,119 @@ +import should from 'should' +import { wikibaseTimeToEpochTime, wikibaseTimeToISOString, wikibaseTimeToSimpleDay } from '../src/helpers/wikibase_time.js' +import { readJsonFile } from './lib/utils.js' +import type { Item } from '../src/types/entity.js' + +const Q970917 = readJsonFile('./tests/data/Q970917.json') as Item + +describe('time', () => { + const ISOtime = '2014-05-14T00:00:00.000Z' + const wdTime = '+2014-05-14T00:00:00Z' + const epoch = 1400025600000 + const ISOnegativeTime = '-000044-03-15T00:00:00.000Z' + const negativeWdTime = '-0044-03-15T00:00:00Z' + const negativeEpoch = -63549360000000 + + describe('wikibaseTimeToEpochTime', () => { + it('env', () => { + should(new Date(epoch).toISOString()).equal(ISOtime) + should(new Date(negativeEpoch).toISOString()).equal(ISOnegativeTime) + }) + + it('should return a number (epoch time)', () => { + should(wikibaseTimeToEpochTime(wdTime)).be.a.Number() + }) + + it('should return a number for negative time', () => { + should(wikibaseTimeToEpochTime(negativeWdTime)).be.a.Number() + }) + + it('should return the right number', () => { + should(wikibaseTimeToEpochTime(wdTime)).equal(epoch) + }) + + it('should return the right number for negative time too', () => { + should(wikibaseTimeToEpochTime(negativeWdTime)).equal(negativeEpoch) + }) + + it('should accept a value object', () => { + // @ts-expect-error + should(wikibaseTimeToEpochTime(Q970917.claims.P569[0].mainsnak.datavalue.value)).equal(-3160944000000) + // @ts-expect-error + should(wikibaseTimeToEpochTime(Q970917.claims.P569[1].mainsnak.datavalue.value)).equal(657417600000) + // @ts-expect-error + should(wikibaseTimeToEpochTime(Q970917.claims.P569[2].mainsnak.datavalue.value)).equal(631152000000) + }) + }) + + describe('wikibaseTimeToISOString', () => { + it('should convert wikibase date to ISO date', () => { + should(wikibaseTimeToISOString('+1885-05-22T00:00:00Z')).equal('1885-05-22T00:00:00.000Z') + should(wikibaseTimeToISOString('+0180-03-17T00:00:00Z')).equal('0180-03-17T00:00:00.000Z') + should(wikibaseTimeToISOString('-0398-00-00T00:00:00Z')).equal('-000398-01-01T00:00:00.000Z') + should(wikibaseTimeToISOString('-34000-00-00T00:00:00Z')).equal('-034000-01-01T00:00:00.000Z') + should(wikibaseTimeToISOString('+34000-00-00T00:00:00Z')).equal('+034000-01-01T00:00:00.000Z') + }) + + it('should return a valid time for possible invalid dates', () => { + should(wikibaseTimeToISOString('+1953-00-00T00:00:00Z')).equal('1953-01-01T00:00:00.000Z') + should(wikibaseTimeToISOString('+1953-11-00T00:00:00Z')).equal('1953-11-01T00:00:00.000Z') + }) + + it('should return a valid time even for possible invalid negative date', () => { + should(wikibaseTimeToISOString('-1953-00-00T00:00:00Z')).equal('-001953-01-01T00:00:00.000Z') + should(wikibaseTimeToISOString('-1953-11-00T00:00:00Z')).equal('-001953-11-01T00:00:00.000Z') + }) + + it('should return a valid time for dates far in the past', () => { + should(wikibaseTimeToISOString('-13798000000-00-00T00:00:00Z')).equal('-13798000000-01-01T00:00:00Z') + should(wikibaseTimeToISOString('-13798000000-02-00T00:00:00Z')).equal('-13798000000-02-01T00:00:00Z') + should(wikibaseTimeToISOString('-13798000000-02-07T15:00:00Z')).equal('-13798000000-02-07T15:00:00Z') + }) + + it('should return a valid time for dates far in the future', () => { + should(wikibaseTimeToISOString('+13798000000-00-00T00:00:00Z')).equal('+13798000000-01-01T00:00:00Z') + should(wikibaseTimeToISOString('+13798000000-02-00T00:00:00Z')).equal('+13798000000-02-01T00:00:00Z') + should(wikibaseTimeToISOString('+13798000000-02-07T15:00:00Z')).equal('+13798000000-02-07T15:00:00Z') + }) + + it('should accept a value object', () => { + // @ts-expect-error + should(wikibaseTimeToISOString(Q970917.claims.P569[0].mainsnak.datavalue.value)).equal('1869-11-01T00:00:00.000Z') + // @ts-expect-error + should(wikibaseTimeToISOString(Q970917.claims.P569[1].mainsnak.datavalue.value)).equal('1990-11-01T00:00:00.000Z') + // @ts-expect-error + should(wikibaseTimeToISOString(Q970917.claims.P569[2].mainsnak.datavalue.value)).equal('1990-01-01T00:00:00.000Z') + }) + }) + + describe('wikibaseTimeToSimpleDay', () => { + it('should convert wikibase date with year precision to simple-day', () => { + should(wikibaseTimeToSimpleDay('+1953-00-00T00:00:00Z')).equal('1953') + should(wikibaseTimeToSimpleDay('-1953-00-00T00:00:00Z')).equal('-1953') + should(wikibaseTimeToSimpleDay('+13-00-00T00:00:00Z')).equal('13') + should(wikibaseTimeToSimpleDay('-13-00-00T00:00:00Z')).equal('-13') + should(wikibaseTimeToSimpleDay('-0100-00-00T00:00:00Z')).equal('-100') + }) + + it('should convert wikibase date with month precision to simple-day', () => { + should(wikibaseTimeToSimpleDay('+1953-01-00T00:00:00Z')).equal('1953-01') + should(wikibaseTimeToSimpleDay('-1953-01-00T00:00:00Z')).equal('-1953-01') + should(wikibaseTimeToSimpleDay('+13-01-00T00:00:00Z')).equal('13-01') + should(wikibaseTimeToSimpleDay('-13-01-00T00:00:00Z')).equal('-13-01') + should(wikibaseTimeToSimpleDay('-0044-03-00T00:00:00Z')).equal('-44-03') + }) + + it('should convert wikibase date with day precision or finer to simple-day', () => { + should(wikibaseTimeToSimpleDay('+1953-01-01T00:00:00Z')).equal('1953-01-01') + should(wikibaseTimeToSimpleDay('-1953-01-01T00:00:00Z')).equal('-1953-01-01') + should(wikibaseTimeToSimpleDay('+1953-01-01T13:45:00Z')).equal('1953-01-01') + should(wikibaseTimeToSimpleDay('-1953-01-01T13:45:00Z')).equal('-1953-01-01') + should(wikibaseTimeToSimpleDay('-0044-03-01T00:00:00Z')).equal('-44-03-01') + }) + + it('should accept a value object', () => { + // @ts-expect-error + should(wikibaseTimeToSimpleDay(Q970917.claims.P569[0].mainsnak.datavalue.value)).equal('1869-11') + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index f6855e83..021159de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,11 +29,11 @@ "alwaysStrict": true, // "noImplicitAny": true, "noImplicitThis": true, - // "strictBindCallApply": true, - "strictFunctionTypes": true + "strictBindCallApply": true, + "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, - // "useUnknownInCatchVariables": true, + "useUnknownInCatchVariables": true, }, "include": ["src", "tests", "scripts"], "ts-node": {