providers_mycore-provider.js
import BaseProvider from "./base-provider.js"
import * as _ from "../utils/lodash.js"
import * as errors from "../errors/index.js"
import jskos from "jskos-tools"
import FlexSearch from "flexsearch"
// Holds all scheme data (filed by scheme URI as key)
const data = {}
/**
* MyCoRe Classification API
*
* See also: https://github.com/gbv/cocoda-sdk/issues/50
*
* To use it in a registry, specify `provider` as "MyCoRe" and provide the API URL as `api`:
* ```json
* {
* "uri": "http://coli-conc.gbv.de/registry/mycore-shbsg",
* "provider": "MyCoRe",
* "api": "https://bibliographie.schleswig-holstein.de/api/v2/classifications/shbib_sachgruppen.json"
* }
* ```
*
* Specifying `schemes` is currently not required and it will not be used. Currently supports only one vocabulary per registry.
*
*/
export default class MyCoReProvider extends BaseProvider {
static supports = {
schemes: true,
top: true,
data: true,
concepts: true,
narrower: true,
ancestors: true,
suggest: true,
search: true,
}
_setup() {
this._scheme = null
}
/**
* Used by `registryForScheme` (see src/lib/CocodaSDK.js) to determine a provider config for a concept schceme.
*
* @param {Object} options
* @param {Object} options.url API URL for BARTOC instance
* @param {Object} options.scheme scheme for which the config is requested
* @returns {Object} provider configuration
*/
static _registryConfigForBartocApiConfig({ url, scheme } = {}) {
if (!url || !scheme) {
return null
}
return {
api: url,
}
}
/**
* Converts scheme info (full scheme data that comes from the API) to a JSKOS scheme
*/
_schemeInfoToJSKOS(schemeInfo) {
const uri = schemeInfo.labels.find(l => l.lang === "x-uri").text
const prefLabel = {}
schemeInfo.labels.filter(l => !l.lang.startsWith("x-")).forEach(l => {
prefLabel[l.lang] = l.text
})
const scheme = {
uri,
prefLabel,
}
if (schemeInfo.categories && schemeInfo.categories.length) {
scheme.topConcepts = [null]
}
// ? Is this accurate?
if (schemeInfo.category && schemeInfo.category.length) {
scheme.concepts = [null]
}
return scheme
}
/**
* Converts a category to a JSKOS concept.
* - Also saves that concept in data
* - Also adds the concept's prefLabels to the search index
*
* ? Question: Should scopeNotes be part of the search index?
*/
_categoryToJSKOS(category, { scheme, broader = [] }) {
if (!category || !scheme) {
return null
}
const id = category.ID
const uri = `${scheme.uri}/${id}`
if (data[scheme.uri].concepts[uri]) {
return data[scheme.uri].concepts[uri]
}
const prefLabel = {}
category.labels.filter(l => !l.lang.startsWith("x-") && l.text).forEach(l => {
// Remove ID from label
prefLabel[l.lang] = l.text.replace(`${id} `, "")
// Add prefLabel to search index
data[scheme.uri].searchIndex.add(uri, prefLabel[l.lang])
})
const scopeNote = {}
category.labels.filter(l => !l.lang.startsWith("x-") && l.description).forEach(l => {
if (!scopeNote[l.lang]) {
scopeNote[l.lang] = []
}
scopeNote[l.lang].push(l.description)
})
data[scheme.uri].concepts[uri] = {
uri,
notation: [id],
prefLabel,
scopeNote,
inScheme: [{ uri: scheme.uri }],
narrower: (category.categories || []).map(c => ({ uri: `${scheme.uri}/${c.ID}`})),
broader,
}
return data[scheme.uri].concepts[uri]
}
/**
* Helper function that replaces `narrower` key with [null] if it has values. Use this before returning concepts.
*/
_removeNarrower(concept) {
if (!concept) {
return concept
}
return Object.assign({}, concept, { narrower: (concept.narrower && concept.narrower.length) ? [null] : []})
}
/**
* Loads the data from the API. Only called from getSchemes and only called once.
*/
async _loadSchemeData(config) {
const schemeInfo = await this.axios({
...config,
method: "get",
url: this._api.api,
_skipAdditionalParameters: true,
})
this._scheme = this._schemeInfoToJSKOS(schemeInfo)
const uri = this._scheme.uri
data[uri] = {
schemeInfo,
searchIndex: FlexSearch.create({
tokenize: "full",
}),
concepts: {},
}
// Recursively go through all concepts and convert them to JSKOS
const dealWithCategory = (category, { broader = [] } = {}) => {
const concept = this._categoryToJSKOS(category, { scheme: this._scheme, broader })
;(category.categories || []).forEach(c => dealWithCategory(c, { broader: [{ uri: concept.uri }] }))
}
schemeInfo.categories.forEach(category => dealWithCategory(category))
data[uri].topConcepts = schemeInfo.categories.map(category => this._categoryToJSKOS(category, { scheme: this._scheme }))
}
async getSchemes(config = {}) {
if (!this._api.api) {
throw new errors.MissingApiUrlError()
}
if (!this._scheme) {
// Make sure data is only loaded once
if (!this._loadSchemeDataPromise) {
this._loadSchemeDataPromise = this._loadSchemeData(config)
}
await this._loadSchemeDataPromise
}
return [this._scheme]
}
async getTop({ scheme, ...config }) {
if (!scheme || !scheme.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "scheme", message: "Missing scheme URI" })
}
if (!this._scheme) {
await this.getSchemes(config)
}
if (!jskos.compare(scheme, this._scheme)) {
throw new errors.InvalidOrMissingParameterError({ parameter: "scheme", message: "Requested vocabulary seems to be unsupported by this API." })
}
return data[this._scheme.uri].topConcepts.map(this._removeNarrower)
}
async getConcepts({ concepts, ...config }) {
if (!_.isArray(concepts)) {
concepts = [concepts]
}
if (!this._scheme) {
await this.getSchemes(config)
}
return concepts.map(c => data[this._scheme.uri].concepts[c.uri]).map(this._removeNarrower)
}
async getAncestors({ concept, ...config }) {
if (!concept || !concept.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "concept" })
}
if (concept.ancestors && concept.ancestors[0] !== null) {
return concept.ancestors
}
if (!this._scheme) {
await this.getSchemes(config)
}
concept = data[this._scheme.uri].concepts[concept.uri]
const broader = concept && concept.broader && concept.broader[0]
if (!broader) {
return []
}
return [broader].concat(await this.getAncestors({ concept: broader, ...config }))
}
async getNarrower({ concept, ...config }) {
if (!concept || !concept.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "concept" })
}
if (concept.narrower && concept.narrower[0] !== null) {
return concept.narrower
}
if (!this._scheme) {
await this.getSchemes(config)
}
concept = data[this._scheme.uri].concepts[concept.uri]
return (concept && concept.narrower || []).map(c => data[this._scheme.uri].concepts[c.uri]).map(this._removeNarrower)
}
/**
* Returns concept search results.
*
* @param {Object} config
* @param {string} config.search search string
* @param {Object} [config.scheme] concept scheme to search in
* @param {number} [config.limit=100] maximum number of search results
* @returns {Array} array of JSKOS concept objects
*/
async search({ search, scheme, limit = 100 }) {
if (!scheme || !scheme.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "scheme" })
}
if (!search) {
throw new errors.InvalidOrMissingParameterError({ parameter: "search" })
}
if (!scheme || !scheme.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "scheme", message: "Missing scheme URI" })
}
if (!this._scheme) {
await this.getSchemes()
}
if (!jskos.compare(scheme, this._scheme)) {
throw new errors.InvalidOrMissingParameterError({ parameter: "scheme", message: "Requested vocabulary seems to be unsupported by this API." })
}
// Use Flexsearch to get result URIs from index
const result = await data[this._scheme.uri].searchIndex.search(search)
return result.map(uri => data[this._scheme.uri].concepts[uri]).map(this._removeNarrower).slice(0, limit)
}
/**
* Returns suggestion result in OpenSearch Suggest Format.
*
* @param {Object} config
* @param {string} config.search search string
* @param {Object} [config.scheme] concept scheme to search in
* @param {number} [config.limit=100] maximum number of search results
* @returns {Array} result in OpenSearch Suggest Format
*/
async suggest(config) {
config._raw = true
const concepts = await this.search(config)
const result = [config.search, [], [], []]
for (let concept of concepts) {
const notation = jskos.notation(concept)
const label = jskos.prefLabel(concept)
result[1].push((notation ? notation + " " : "") + label)
result[2].push("")
result[3].push(concept.uri)
}
if (concepts._totalCount != undefined) {
result._totalCount = concepts._totalCount
} else {
result._totalCount = concepts.length
}
return result
}
}
MyCoReProvider.providerName = "MyCoRe"
MyCoReProvider.providerType = "http://bartoc.org/api-type/mycore"