providers_mesh-api-provider.js
import BaseProvider from "./base-provider.js"
import * as errors from "../errors/index.js"
import jskos from "jskos-tools"
/**
* MESH API.
*
* MESH (Medical Subject Headings) RDF vocabulary.
*
* initialization example:
* ```json
* {
* provider: "MeshApi",
* uri: "https://id.nlm.nih.gov/mesh/sparql" // optional, default is "https://id.nlm.nih.gov/mesh/sparql"
* }
* ```
*
* @extends BaseProvider
* @category Providers
*/
export default class MeshApiProvider extends BaseProvider {
// #### PROPERTIES ####
// - providerName (This is how a provider is identified in a "registry" object in field `provider`.)
static providerName = "MeshApi"
// - providerType (Optional BARTOC API type URI. Supported types: https://github.com/gbv/bartoc.org/blob/main/data/bartoc-api-types.concepts.csv, the URI prefix is "http://bartoc.org/api-type/".)
static providerType = "http://bartoc.org/api-type/mesh" // "http://bartoc.org/en/node/391"
// - supports (Optional object of supported capabilities. The keys should be values from this list: https://github.com/gbv/cocoda-sdk/blob/9145952398d6828004beb395c1d392a4d24e9288/src/utils/index.js#L159-L174; values should be a boolean. `false` values can be left out. They will be used to initialize `this.has` (see below). Alternatively, `this.has` can be filled in `_prepare` or `_setup`.)
static supports = {
schemes: true,
concepts: true,
narrower: true,
suggest: true,
search: true,
data: false, // previously registered as supported but no data endpoint is available
top: false,
ancestors: false,
types: false,
auth: false,
mappings: false,
concordances: false,
annotations: false,
occurrences: false,
}
_defaultParams = {
format: "JSON",
limit: 100,
offset: 0,
inference: true,
uri: "https://id.nlm.nih.gov/mesh/sparql",
}
_defaultHeaders = {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
}
// application/sparql-results+json;charset=utf-8
mesh = {
uri: "http://id.nlm.nih.gov/mesh",
identifier: [
"http://bartoc.org/en/node/391",
"http://www.wikidata.org/entity/Q199897",
],
notation: ["MeSH"],
prefLabel: {
en: "Medical Subject Headings",
},
languages: ["en"],
concepts: [null],
topConcepts: [],
}
// #### CUSTOM METHODS ####
/**
* Constructs SPARQL Query for concepts. The query is constructed from the `where` parameter, which should be a valid SPARQL WHERE clause.
* @param {Array} where - An array of SPARQL WHERE clauses to filter the concepts.
* @returns {string} - A SPARQL query string to retrieve concepts from the MeSH API.
* @private
*/
_buildConceptQuery({ where }) {
// TODO: Ordering
// ?d a meshv:Descriptor .
return `
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX meshv: <http://id.nlm.nih.gov/mesh/vocab#>
SELECT ?d ?name ?dateCreated ?dateRevised ?identifier (GROUP_CONCAT(?broaderDescriptor;SEPARATOR=" ") as ?broader)
FROM <http://id.nlm.nih.gov/mesh>
WHERE {
?d rdfs:label ?name .
?d meshv:identifier ?identifier .
${where}
OPTIONAL { ?d meshv:dateCreated ?dateCreated } .
OPTIONAL { ?d meshv:dateRevised ?dateRevised } .
OPTIONAL { ?d meshv:broaderDescriptor ?broaderDescriptor } .
}
GROUP BY ?d ?name ?dateCreated ?dateRevised ?identifier
ORDER BY ?d
`
}
/*
* Converts the query result to a list of concepts.
* @param {Object} result - The result object returned from the MeSH API query.
* @returns {Array} - A list of concept objects.
* @private
*/
_queryResultToConcepts(result) {
if (!result || !result.results || !result.results.bindings) {
return []
}
return result.results.bindings.map(c => {
const concept = {
inScheme: [this.mesh],
uri: c.d.value,
notation: [c.identifier.value],
prefLabel: {
[c.name["xml:lang"]]: c.name.value,
},
broader: (c.broader.value || "").split(" ").filter(b => b.trim() !== "").map(b => ({ uri: b })),
}
if (c.dateCreated && c.dateCreated.value) {
concept.created = c.dateCreated.value
}
if (c.dateRevised && c.dateRevised.value) {
concept.modified = c.dateRevised.value
}
jskos.clean(concept)
return concept
})
}
async _query({query, limit, offset, ...config}) {
if (!query) {
throw new errors.InvalidOrMissingParameterError({ parameter: "query" })
}
limit = limit || this._defaultParams.limit
offset = offset || this._defaultParams.offset
let url = this._api.api || this.uri || this._jskos.uri || this._jskos.url || this._jskos.api || this._defaultParams.uri
let result = await this.axios({
method: "get",
url: url,
params: {
...this._defaultParams,
...(config.params || {}),
query,
limit,
offset,
},
headers: this._defaultHeaders,
})
try {
return this._queryResultToConcepts(result)
} catch (error) {
console.error(error)
return []
}
}
/**
* Used by `registryForScheme` (see src/lib/CocodaSDK.js) to determine a provider config for a concept scheme.
*
* @param {Object} options
* @param {Object} options.url API URL for server
* @returns {Object} provider configuration
*/
static _registryConfigForBartocApiConfig({ url, scheme } = {}) {
if (!url || !scheme) {
return null
}
return {
api: url,
schemes: [scheme],
}
}
async _search({ scheme, search, limit, offset, ...config }) {
if (!search) {
throw new errors.InvalidOrMissingParameterError({ parameter: "search" })
}
if (!scheme || !jskos.compare(scheme, this.mesh)) {
throw new errors.InvalidOrMissingParameterError({ parameter: "scheme" })
}
limit = limit || this._jskos.suggestResultLimit || this._defaultParams.limit
offset = offset || this._defaultParams.offset
// TODO: Do we need regexp escapaing?
search = search.replace(/"/g, "\\\"")
const query = this._buildConceptQuery({ where: `FILTER(REGEX(?name,"${search}","i"))` })
return await this._query({ query, limit, offset, ...config })
}
// #### OVERRIDE METHODS ####
/**
* will be called before the registry is initialized (i.e. it's `/status` endpoint is queries if necessasry)
* @private
*/
_prepare() {}
/**
* Sets up provider-specific properties.
* Enables support for mappings in this provider.
* will be called after registry is initialized (i.e. it's `/status` endpoint is queries if necessary), should be used to set properties on this.has and custom preparations
* @private
*/
_setup() {}
async getSchemes() {
return [this.mesh]
}
async getTop() {
return []
}
async getConcepts({ concepts, ...config }) {
if (!concepts) {
throw new errors.InvalidOrMissingParameterError({ parameter: "concepts" })
}
if (!Array.isArray(concepts)) {
concepts = [concepts]
}
const query = this._buildConceptQuery({ where: `VALUES ?d { ${concepts.map(c => `<${c.uri}>`).join(" ")} } .` })
return await this._query({ query, ...config })
}
async getNarrower({ concept, ...config }) {
if (!concept || !concept.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "concept" })
}
const query = this._buildConceptQuery({ where: `?d meshv:broaderDescriptor <${concept.uri}> .` })
return await this._query({ query, ...config })
}
async suggest(config) {
const search = config.search
const results = await this._search(config)
return [
search,
results.map(r => jskos.prefLabel(r, { fallbackToUri: false })),
[],
results.map(r => r.uri),
]
}
async search(config) {
return this._search(config)
}
}