diff --git a/src/providers/index.js b/src/providers/index.js index 48747d6..7cdd459 100644 --- a/src/providers/index.js +++ b/src/providers/index.js @@ -10,6 +10,7 @@ import LocApiProvider from "./loc-api-provider.js" import SkohubProvider from "./skohub-provider.js" import LobidApiProvider from "./lobid-api-provider.js" import MyCoReProvider from "./mycore-provider.js" +import NoTApiProvider from "./not-api-provider.js" export { BaseProvider, @@ -24,4 +25,5 @@ export { SkohubProvider, LobidApiProvider, MyCoReProvider, + NoTApiProvider, } diff --git a/src/providers/not-api-provider.js b/src/providers/not-api-provider.js new file mode 100644 index 0000000..f7fc494 --- /dev/null +++ b/src/providers/not-api-provider.js @@ -0,0 +1,182 @@ +import BaseProvider from "./base-provider.js" +import * as errors from "../errors/index.js" +import { listOfCapabilities } from "../utils/index.js" +import axios from "axios" +import jskos from "jskos-tools" + +/** + * TODOs: + * - [ ] Clean up conversion to JSKOS + * - [ ] Notations? (might be possible if NoT provided URI namespace) + * - [ ] Source languages (see https://github.com/netwerk-digitaal-erfgoed/network-of-terms/issues/1105) + * - [ ] Clean up GraphQL query strings + * - [ ] Better error handling + * - [ ] Implement getTop (if possible) + * - [ ] Implement getNarrower and getAncestors (already returned by getConcepts, but methods should be implemented nonetheless) + * - [ ] Decide on providerType URI + * - [ ] More testing required + */ + +/** + * Add this entry to registries: +{ + "provider": "NoTApi", + "uri": "http://coli-conc.gbv.de/registry/not-api", + "api": "https://termennetwerk-api.netwerkdigitaalerfgoed.nl/graphql", + "notation": [ + "NoT" + ] +} + */ + +const cache = { + schemes: [], +} + +export default class NoTApiProvider extends BaseProvider { + + _prepare() { + this.has.schemes = true + this.has.top = false + this.has.data = true + this.has.concepts = true + this.has.narrower = false + this.has.ancestors = false + this.has.suggest = true + this.has.search = true + // Explicitly set other capabilities to false + listOfCapabilities.filter(c => !this.has[c]).forEach(c => { + this.has[c] = false + }) + } + + /** + * 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 server + * @returns {Object} provider configuration + */ + static _registryConfigForBartocApiConfig({ url } = {}) { + if (!url) { + return null + } + return { + api: url, + } + } + + async getSchemes() { + if (!cache.schemes.length) { + const result = await axios.post(this._api.api, { + query: "query sources { sources { name uri description alternateName } }", + operationName: "sources", + }) + const schemes = result?.data?.data?.sources || [] + if (schemes.length) { + cache.schemes = schemes.map(scheme => { + const jskos = { + uri: scheme.uri, + prefLabel: { und: scheme.name }, + } + if (scheme.desciption) { + jskos.description = { und: [scheme.description] } + } + if (scheme.alternateName) { + jskos.notation = [scheme.alternateName] + } + return jskos + }) + } else { + return [] + } + } + return cache.schemes + } + + // async getTop() { + // } + + async getConcepts({ concepts }) { + if (!concepts) { + throw new errors.InvalidOrMissingParameterError({ parameter: "concepts" }) + } + if (!Array.isArray(concepts)) { + concepts = [concepts] + } + const result = await axios.post(this._api.api, { + query: `query { lookup( uris: [${concepts.map(c => `"${c.uri}"`)}], ) { uri source { ... on Source { uri } } result { ... on Term { uri prefLabel scopeNote altLabel broader { uri } narrower { uri } } } } }`, + }) + return (result.data?.data?.lookup || []).map(entry => { + const concept = { + uri: entry.uri, + inScheme: [cache.schemes.find(scheme => jskos.compare(scheme, { uri: entry.source.uri }))], + } + if (entry.result?.prefLabel?.[0]) { + concept.prefLabel = { und: entry.result.prefLabel[0] } + } + if (entry.result?.altLabel?.[0]) { + concept.altLabel = { und: entry.result.altLabel } + } + if (entry.result?.scopeNote?.[0]) { + concept.scopeNote = { und: entry.result.scopeNote } + } + if (entry.result?.broader?.length) { + concept.broader = entry.result.broader + } + if (entry.result?.narrower?.length) { + concept.narrower = entry.result.narrower + } + return concept + }) + } + + // async getNarrower({ concept }) { + // } + + // async getAncestors({ concept }) { + // } + + 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({ scheme, search }) { + if (!search) { + throw new errors.InvalidOrMissingParameterError({ parameter: "search" }) + } + if (!scheme || !jskos.isContainedIn(scheme, cache.schemes)) { + throw new errors.InvalidOrMissingParameterError({ parameter: "scheme" }) + } + const result = await axios.post(this._api.api, { + query: `query { terms( sources: ["${scheme.uri}"] query: "${search}" ) { source { uri } result { ... on Terms { terms { uri prefLabel scopeNote } } } }}`, + }) + return (result.data?.data?.terms?.[0]?.result?.terms || []).map(concept => { + const jskos = { + uri: concept.uri, + inScheme: [scheme], + } + if (concept.prefLabel?.[0]) { + jskos.prefLabel = { und: concept.prefLabel[0] } + } + if (concept.altLabel?.[0]) { + jskos.altLabel = { und: concept.altLabel } + } + if (concept.scopeNote?.[0]) { + jskos.scopeNote = { und: concept.scopeNote[0] } + } + return jskos + }) + } + +} + +NoTApiProvider.providerName = "NoTApi" +// NoTApiProvider.providerType = "http://bartoc.org/api-type/not"