Skip to content
This repository was archived by the owner on May 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { GetManyEntitiesOptions } from './queries/get_many_entities.js'
import type { GetReverseClaimsOptions } from './queries/get_reverse_claims.js'
import type { GetRevisionsOptions } from './queries/get_revisions.js'
import type { SearchEntitiesOptions } from './queries/search_entities.js'
import type { Entities } from './types/entity.js'
import type { ClientOptions } from './types/options.js'
import type { SearchResponse } from './types/search.js'
import type { SparqlResults } from './types/sparql.js'
Expand All @@ -34,7 +33,7 @@ export interface ClientUrlBuilders {
getReverseClaims: GetReverseClaims
}

async function fetchJson<T> (url: string, clientOptions?: ClientOptions): Promise<T> {
export async function fetchJson<T> (url: string, clientOptions?: ClientOptions): Promise<T> {
Comment thread
maxlath marked this conversation as resolved.
// Format sdk options to `fetch` RequestInit
const requestInit: RequestInit = {
headers: {
Expand Down Expand Up @@ -62,25 +61,40 @@ export function buildClient (urlBuilders: ClientUrlBuilders, clientOptions?: Cli
const fetch = <T>(url: string) => fetchJson<T>(url, clientOptions)

return {
searchEntities: (options: SearchEntitiesOptions) => fetch<SearchResponse>(searchEntities(options)),
cirrusSearchPages: (options: CirrusSearchPagesOptions) => fetch<CirrusSearchPagesResponse>(cirrusSearchPages(options)),
getEntities: (options: GetEntitiesOptions) => fetch<WbGetEntitiesResponse>(getEntities(options)),
getManyEntities: async (options: GetManyEntitiesOptions) => {
searchEntities (options: SearchEntitiesOptions) {
return fetch<SearchResponse>(searchEntities(options))
},
cirrusSearchPages (options: CirrusSearchPagesOptions) {
return fetch<CirrusSearchPagesResponse>(cirrusSearchPages(options))
},
getEntities (options: GetEntitiesOptions) {
return fetch<WbGetEntitiesResponse>(getEntities(options))
},
async getManyEntities (options: GetManyEntitiesOptions) {
const urls = getManyEntities(options)
const responses = await Promise.all(urls.map(url => fetch<WbGetEntitiesResponse>(url)))
return responses.reduce<WbGetManyEntitiesResponse>(
(acc, { entities, error }) => ({
entities: { ...acc.entities, ...entities },
errors: error ? [ ...acc.errors, error ] : acc.errors,
}),
{ entities: {} as Entities, errors: [] }
)
const aggregatedResponse: WbGetManyEntitiesResponse = { entities: {}, errors: [] }
for (const url of urls) {
const { entities, error } = await fetch<WbGetEntitiesResponse>(url)
Object.assign(aggregatedResponse.entities, entities)
if (error) aggregatedResponse.errors.push(error)
}
return aggregatedResponse
},
getRevisions (options: GetRevisionsOptions) {
return fetch<RevisionsResponse>(getRevisions(options))
},
getEntityRevision (options: GetEntityRevisionOptions) {
return fetch<WbGetEntitiesResponse>(getEntityRevision(options))
},
getEntitiesFromSitelinks (options: GetEntitiesFromSitelinksOptions) {
return fetch<WbGetEntitiesResponse>(getEntitiesFromSitelinks(options))
},
sparqlQuery (sparql: string) {
return fetch<SparqlResults>(sparqlQuery(sparql))
},
getReverseClaims (options: GetReverseClaimsOptions) {
return fetch<SparqlResults>(getReverseClaims(options))
},
getRevisions: (options: GetRevisionsOptions) => fetch<RevisionsResponse>(getRevisions(options)),
getEntityRevision: (options: GetEntityRevisionOptions) => fetch<WbGetEntitiesResponse>(getEntityRevision(options)),
getEntitiesFromSitelinks: (options: GetEntitiesFromSitelinksOptions) => fetch<WbGetEntitiesResponse>(getEntitiesFromSitelinks(options)),
sparqlQuery: (sparql: string) => fetch<SparqlResults>(sparqlQuery(sparql)),
getReverseClaims: (options: GetReverseClaimsOptions) => fetch<SparqlResults>(getReverseClaims(options)),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from './types/wbgetentities.js'

export type { WbGetEntitiesResponse, WbGetManyEntitiesResponse, CirrusSearchResult, RevisionsResponse } from './helpers/parse_responses.js'
export type { WbkClient } from './client.js'
export type { WbkSimpleClient } from './simple_client.js'
Comment thread
maxlath marked this conversation as resolved.
55 changes: 55 additions & 0 deletions src/simple_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { buildClient } from './client.js'
import { pagesTitles } from './helpers/parse_responses.js'
import { simplifyEntities } from './helpers/simplify_entity.js'
import { simplifySparqlResults } from './helpers/simplify_sparql_results.js'
import type { ClientUrlBuilders } from './client.js'
import type { CirrusSearchPagesOptions } from './queries/cirrus_search.js'
import type { GetEntitiesFromSitelinksOptions } from './queries/get_entities_from_sitelinks.js'
import type { GetEntityRevisionOptions } from './queries/get_entity_revision.js'
import type { GetManyEntitiesOptions } from './queries/get_many_entities.js'
import type { GetReverseClaimsOptions } from './queries/get_reverse_claims.js'
import type { GetRevisionsOptions } from './queries/get_revisions.js'
import type { SearchEntitiesOptions } from './queries/search_entities.js'
import type { ClientOptions, SimplifyEntityOptions } from './types/options.js'

export function buildSimpleClient (urlBuilders: ClientUrlBuilders, clientOptions?: ClientOptions, simplifyEntityOptions?: SimplifyEntityOptions) {
const client = buildClient(urlBuilders, clientOptions)
const simplify = (entities: Parameters<typeof simplifyEntities>[0]) => simplifyEntities(entities, simplifyEntityOptions)

return {
async searchEntities (options: SearchEntitiesOptions) {
const { search } = await client.searchEntities(options)
return search
},
async cirrusSearchPages (options: CirrusSearchPagesOptions) {
const results = await client.cirrusSearchPages(options)
return pagesTitles(results)
},
async getEntities (options: GetManyEntitiesOptions) {
const { entities } = await client.getManyEntities(options)
return simplify(entities)
},
async getRevisions (options: GetRevisionsOptions) {
const results = await client.getRevisions(options)
return results.query.pages
},
async getEntityRevision (options: GetEntityRevisionOptions) {
const { entities } = await client.getEntityRevision(options)
return Object.values(simplify(entities))[0]
},
async getEntitiesFromSitelinks (options: GetEntitiesFromSitelinksOptions) {
const { entities } = await client.getEntitiesFromSitelinks(options)
return simplify(entities)
},
async sparqlQuery (sparql: string) {
const results = await client.sparqlQuery(sparql)
return simplifySparqlResults(results)
},
async getReverseClaims (options: GetReverseClaimsOptions) {
const results = await client.getReverseClaims(options)
return simplifySparqlResults(results)
},
}
}

export type WbkSimpleClient = ReturnType<typeof buildSimpleClient>
6 changes: 5 additions & 1 deletion src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export interface ClientOptions {
userAgent?: string
}

export type Config = InstanceConfig & ClientOptions
export interface SimpleClientOptions {
simplifyEntityOptions?: SimplifyEntityOptions
Comment thread
maxlath marked this conversation as resolved.
}

export type Config = InstanceConfig & ClientOptions & SimpleClientOptions

export type Props = 'info' | 'sitelinks' | 'sitelinks/urls' | 'aliases' | 'labels' | 'descriptions' | 'claims' | 'datatype'
export type UrlResultFormat = 'xml' | 'json'
Expand Down
10 changes: 8 additions & 2 deletions src/wikibase-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import { getReverseClaimsFactory } from './queries/get_reverse_claims.js'
import { getRevisionsFactory } from './queries/get_revisions.js'
import { searchEntitiesFactory } from './queries/search_entities.js'
import { sparqlQueryFactory } from './queries/sparql_query.js'
import { buildSimpleClient } from './simple_client.js'
import { buildUrlFactory, type Url } from './utils/build_url.js'
import { isPlainObject } from './utils/utils.js'
import type { WbkClient } from './client.js'
import type { WbkSimpleClient } from './simple_client.js'
import type { ClientOptions, Config } from './types/options.js'

const tip = `Tip: if you just want to access functions that don't need an instance or a sparqlEndpoint,
Expand Down Expand Up @@ -49,11 +51,11 @@ interface Instance {
readonly root: Url
readonly apiEndpoint: Url
}
export type Wbk = { readonly instance: Instance, readonly client: WbkClient } & ApiQueries & SparqlQueries & typeof common
export type Wbk = { readonly instance: Instance, readonly client: WbkClient, readonly simpleClient: WbkSimpleClient } & ApiQueries & SparqlQueries & typeof common

export function WBK (config: Config): Wbk {
if (!isPlainObject(config)) throw new Error('invalid config')
const { instance, sparqlEndpoint, userAgent } = config
const { instance, sparqlEndpoint, userAgent, simplifyEntityOptions } = config
Comment thread
ogroppo marked this conversation as resolved.
let { wgScriptPath = 'w' } = config

wgScriptPath = wgScriptPath.replace(/^\//, '')
Expand Down Expand Up @@ -122,6 +124,10 @@ export function WBK (config: Config): Wbk {
...wikibaseApiFunctions,
...wikibaseQueryServiceFunctions,
}, clientOptions),
simpleClient: buildSimpleClient({
...wikibaseApiFunctions,
...wikibaseQueryServiceFunctions,
}, clientOptions, simplifyEntityOptions),
...common,
...wikibaseApiFunctions,
...wikibaseQueryServiceFunctions,
Expand Down
3 changes: 2 additions & 1 deletion tests/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import should from 'should'
import { WBK } from '../src/wikibase-sdk.js'
import type { ItemId } from '../src/index.js'

const wdk = WBK({
instance: 'https://www.wikidata.org',
Expand All @@ -23,7 +24,7 @@ describe('client (integration)', function () {

describe('getManyEntities', () => {
it('fetches entities in batches and returns a merged response', async () => {
const ids = [ 'Q135519449', 'Q135519450', 'Q135519451' ] as [`Q${number}`, ...`Q${number}`[]]
const ids = [ 'Q135519449', 'Q135519450', 'Q135519451' ] as ItemId[]
const res = await wdk.client.getManyEntities({ ids })
should(res).be.an.Object()
should(res.entities).be.an.Object()
Expand Down
155 changes: 155 additions & 0 deletions tests/simple_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import should from 'should'
import { WBK } from '../src/wikibase-sdk.js'
import type { ItemId } from '../src/index.js'

const wdk = WBK({
instance: 'https://www.wikidata.org',
sparqlEndpoint: 'https://query.wikidata.org/sparql',
userAgent: 'wikibase-sdk-tests/integration',
})

const wdkWithOptions = WBK({
instance: 'https://www.wikidata.org',
sparqlEndpoint: 'https://query.wikidata.org/sparql',
userAgent: 'wikibase-sdk-tests/integration',
simplifyEntityOptions: { keepQualifiers: true },
})

describe('simpleClient (integration)', function () {
// Network requests — give each test up to 10 seconds
this.timeout(10_000)

describe('getEntities', () => {
it('fetches entities in batches and returns merged simplified entities', async () => {
const ids = [ 'Q135519449', 'Q135519450', 'Q135519451' ] as ItemId[]
const res = await wdk.simpleClient.getEntities({ ids })
should(res).be.an.Object()
const entity = res['Q135519449']
should(entity?.id).equal('Q135519449')
// labels are simplified: { en: 'some label' } not { en: { language, value } }
if (entity && 'labels' in entity && entity.labels) {
const firstLabel = Object.values(entity.labels)[0]
should(firstLabel).be.a.String()
}
})
})

describe('searchEntities', () => {
it('returns a search response with results', async () => {
const res = await wdk.simpleClient.searchEntities({ search: 'Douglas Adams', language: 'en', limit: 3 })
should(res).be.an.Array()
should(res.length).be.above(0)
should(res[0]).have.property('id')
should(res[0]).have.property('label')
})
})

describe('cirrusSearchPages', () => {
it('returns an array of page title strings', async () => {
const res = await wdk.simpleClient.cirrusSearchPages({ haswbstatement: 'P31=Q5', limit: 3 })
should(res).be.an.Array()
should(res.length).be.above(0)
should(res[0]).be.a.String()
})
})

describe('getRevisions', () => {
it('returns revision data for an entity', async () => {
const res = await wdk.simpleClient.getRevisions({ ids: 'Q135519449', limit: 2 })
should(res).be.an.Object()
should(res).be.an.Object()
const pages = Object.values(res)
should(pages.length).be.above(0)
should(pages[0]?.revisions).be.an.Array()
})
})

describe('getEntityRevision', () => {
it('fetches a specific revision and returns simplified entities', async () => {
const revisionsRes = await wdk.simpleClient.getRevisions({ ids: 'Q135519449', limit: 1 })
const page = Object.values(revisionsRes)[0]
const revid = page?.revisions[0]?.revid
should(revid).be.a.Number()

const entity = await wdk.simpleClient.getEntityRevision({ id: 'Q135519449', revision: `${revid}` })
should(entity).be.an.Object()
should(entity?.id).equal('Q135519449')
})
})

describe('getEntitiesFromSitelinks', () => {
it('looks up entities from Wikipedia page titles and returns simplified data', async () => {
const res = await wdk.simpleClient.getEntitiesFromSitelinks({
titles: 'Douglas Adams',
sites: 'enwiki',
languages: 'en',
})
should(res).be.an.Object()
const entities = Object.values(res)
should(entities.length).be.above(0)
should(entities[0]?.id).be.a.String()
})
})

describe('sparqlQuery', () => {
it('executes a SPARQL query and returns simplified results', async () => {
const sparql = 'SELECT ?item WHERE { wd:Q135519449 wdt:P31 ?item } LIMIT 3'
const res = await wdk.simpleClient.sparqlQuery(sparql)
should(res).be.an.Array()
should(res.length).be.above(0)
// simplified: entity URIs are converted to IDs like 'Q5'
should(res[0]).have.property('item')
should(res[0]?.item).be.a.String()
})
})

describe('getReverseClaims', () => {
it('returns simplified SPARQL results for a property-value pair', async () => {
const res = await wdk.simpleClient.getReverseClaims({ properties: 'P31', values: 'Q5', limit: 3 })
should(res).be.an.Array()
should(res.length).be.above(0)
should(res[0]).have.property('subject')
// simplified: URI converted to entity ID string
should(res[0]?.subject).be.a.String()
})
})
})

describe('simpleClient with simplifyEntityOptions (integration)', function () {
this.timeout(10_000)

describe('simplifyEntityOptions', () => {
it('passes keepQualifiers option through to simplifyEntities', async () => {
// Q2 (Earth) has claims with qualifiers
const withoutQualifiers = await wdk.simpleClient.getEntities({ ids: [ 'Q2' ] as ItemId[] })
const withQualifiers = await wdkWithOptions.simpleClient.getEntities({ ids: [ 'Q2' ] as ItemId[] })

const entityWithout = withoutQualifiers['Q2']
const entityWith = withQualifiers['Q2']

should(entityWithout).be.an.Object()
should(entityWith).be.an.Object()

// Both Q2 results are items — narrow via type guard so TS sees `.claims`
if (!entityWithout || !('claims' in entityWithout) || !entityWith || !('claims' in entityWith)) return

const claimsWithout = entityWithout.claims ?? {}
const claimsWith = entityWith.claims ?? {}

// With keepQualifiers, claim values are objects with a `qualifiers` key
const firstPropertyWith = Object.values(claimsWith)[0] as unknown[]
should(firstPropertyWith).be.an.Array()
if (firstPropertyWith && firstPropertyWith.length > 0) {
should(firstPropertyWith[0]).be.an.Object()
should(firstPropertyWith[0]).have.property('qualifiers')
}

// Without keepQualifiers, claim values are plain strings/numbers/objects without `qualifiers`
const firstPropertyWithout = Object.values(claimsWithout)[0] as unknown[]
should(firstPropertyWithout).be.an.Array()
if (firstPropertyWithout && firstPropertyWithout.length > 0) {
should(firstPropertyWithout[0]).not.have.property('qualifiers')
}
})
})
})