providers_mod-api-provider.js

import BaseProvider from "./base-provider.js"
/*
import jskos from "jskos-tools"
import jsonld from "jsonld"
import context_mod from "./contexts/context_mod.js"
import context_jskos from "./contexts/context_jskos.js"
*/
/**
 * MOD API.
 *
 * MOD (Metadata Object Description) is a service that provides access to metadata artifacts such as vocabularies, concept schemes, and related resources via a RESTful API.
 *
 * initialization example:
 * ```json
 * {
 *   provider: "ModApi",
 *   language: "en",           // language to use for labels and descriptions. if no language is given in mod, it defaults to "en"
 *   cleancontext: true,       // if true, the @context element will be cleaned up to remove unnecessary keys
 *   <feature removed> transformation: "manual", // "jsonld" for conversion via jsonld-concept or "manual" for manual conversion
 *   uri: "https://terminology.services.base4nfdi.de/api-gateway" // "http://localhost:8080/api-gateway" if api-gateway is running locally
 * }
 * ```
 *
 * @extends BaseProvider
 * @category Providers
 */
export default class ModApiProvider extends BaseProvider {

  // #### PROPERTIES ####

  // - providerName (This is how a provider is identified in a "registry" object in field `provider`.)
  static providerName = "ModApi"
  // - 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/en/node/20333"
  // - 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,
    top: false,
    data: false,
    concepts: true,
    narrower: false,
    ancestors: false,
    types: false,
    suggest: false,
    search: false,
    auth: false,
    mappings: false,
    concordances: false,
    annotations: false,
    occurrences: false,
  }

  // #### CUSTOM METHODS ####

  /**
   * Constructs the full API URL for a given endpoint.
   * @param {Array} parts - Array of api parts (e.g., "[artifacts, <schemeShort>]")
   * @param {Object} params - An object containing query parameters as key-value pairs.
   * @returns {string} The full URL. Returns undefined if any part is undefined.
   * @private
   */
  _getApiUrl(parts, params) {
    // result = URL + endpointA (+ artefactID)? (+ endpointB)? (+ paramsString)?
    let result = this.uri || ""
    // Ensure the base URL ends with a slash and the endpoint starts with a slash
    if (result.endsWith("/")) {
      result = result.slice(0, -1)
    }
    for (const part of parts) {
      if (part) {
        result += "/" + part
      } else {
        return
      }
    }

    // If params are provided, append them as query parameters
    if (params) {
      const paramString = Object.keys(params)
        .map((k) => `${k}=${encodeURIComponent(params[k])}`)
        .join("&")
      result += (result.includes("?") ? "&" : "?") + paramString
    }
    return result
  }
  
  /*
  _artefactToJSKOS(artefact) {
    switch (this._jskos.transformation) {
      //case "jsonld":
      //  return this._modToJskosJsonLD(artefact)
      case "manual":
        return this._modToJskosManual(artefact)
      default:
        // If no specific transformation is set, default to JSON-LD conversion
        return this._modToJskosJsonLD(artefact)
    }
  }

  _modToJskosJsonLD(artefact) {
    if (artefact["@id"]) {
      delete artefact["@id"]
    }

    artefact["@context"] = context_mod["@context"]

    return jsonld
      .expand(artefact)
      .then((expanded) => jsonld.compact(expanded, context_jskos))
      .then((compacted) => {
        jskos.clean(compacted)
        delete compacted["@context"]
        for (const key in compacted) {
          if (compacted[key]?.["@none"]) {
            compacted[key][this._language] = compacted[key]["@none"]
            delete compacted[key]["@none"]
          }
        }
        compacted = jskos.clean(compacted)
        compacted = this._repairJsonLD(compacted)

        return compacted
      })
  }

  _repairJsonLD(json){
    // This function is used to repair the JSON-LD context, translating erroneous keys
    const map = {
      narrower: "http://www.w3.org/2004/02/skos/core#narrower",
      altLabel: "http://www.w3.org/2004/02/skos/core#altLabel",
      definition: "http://www.w3.org/2004/02/skos/core#definition",
    }

    for (const key in map) {
      if (json[map[key]]) {
        json[key] = json[map[key]]
        delete json[map[key]]
      }
    }
    return json
  }
  */

  // _modToJskosManual(artefact) {
  _artefactToJSKOS(artefact) {
    const lan = artefact.language || this._language || "en"
    const concept = {}
    // artefact.rightsHolder
    // artefact.backend_type
    // artefact.createdWith
    // artefact.keywords
    // artefact.contactPoint
    if (artefact.subject) {
      concept.subject = [artefact.subject]
    }
    // artefact.obsolete // boolean
    // artefact.accrualMethod
    // artefact.accrualPeriodicity
    // artefact.status
    // artefact.bibliographicCitation
    // artefact.semanticArtefactRelation
    // artefact.coverage
    // artefact.competencyQuestion
    if (artefact.includedInDataCatalog) {
      concept.api = [artefact.includedInDataCatalog]
    }
    // artefact.accessRights

    // TYPES
    if (artefact["@type"]) {
      concept["@type"] = artefact["@type"]
    }
    if (artefact.type) {
      concept.type = [artefact.type]
    }

    // NOTATION
    if (artefact.source_name) {
      concept.notation = [artefact.source_name]
    }
    if (artefact.short_form){
      if (!concept.notation) {
        concept.notation = [artefact.short_form]
      } else {
        concept.notation.push(artefact.short_form)
      }
    }
    if (artefact.label){
      concept.prefLabel = {}
      concept.prefLabel[lan] = []
      concept.prefLabel[lan].push(artefact.label)
    }
    if (artefact.synonyms){
      concept.altLabel = {}
      concept.altLabel[lan] = artefact.synonyms
    }
    if (artefact.descriptions){
      concept.definition = {}
      concept.definition[lan] = artefact.descriptions
    }
    if (artefact.language){
      concept.languages = artefact.language
    }

    // URLS
    if (artefact["@id"]) {
      concept.uri = artefact["@id"]
    }
    if (artefact.iri) {
      concept.iri = artefact.iri
    }
    if (artefact.identifier) {
      concept.identifier = [artefact.identifier]
    }
    if (artefact.source) {
      concept.source = [artefact.source]
    }
    if (artefact.source_url) {
      concept.namespace = artefact.source_url
    }
    if (artefact.landingPage) {
      concept.url = artefact.landingPage
    }

    // METADATA
    if (artefact.version) {
      concept.version = artefact.version
    }
    // artefact.versionIRI
    if (artefact.modified) {
      concept.modified = artefact.modified
    }
    if (artefact.created) {
      concept.created = artefact.created
    }
    // concept.startDate
    if (artefact.hasFormat) {
      concept.format = artefact.hasFormat
    }
    if (artefact.license) {
      concept.license = [artefact.license]
    }
    if (artefact.creator) {
      concept.creator = artefact.creator
    }
    // artefact.wasGeneratedBy
    if (artefact.contributor){
      concept.contributor = {}
      concept.contributor.prefLabel = {}
      concept.contributor.prefLabel[lan] = artefact.contributor
    }
    if (artefact.publisher){
      concept.publisher = {}
      concept.publisher[lan] = artefact.publisher
    }
    // artefact.title
    if (artefact.released){
      concept.issued = artefact.released
    }
    // artefact.acronym
    if (artefact.children){
      concept.narrower = {}
      concept.narrower[lan] = artefact.children
    }

    return concept
  }

  // #### API REQUESTS ####

  async _request(url, ..._config) {
    if (!url) {
      return
    }
    const result = await this.axios({
      method: "get",
      url,
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      ..._config,
    })
    if (!result?._url || Object.keys(result).length != 1){
      return result
    }
  }

  // API REQUESTS SCHEMES

  async _getSchemesMod() {
    //https://terminology.services.base4nfdi.de/api-gateway/artefacts
    const url = this._getApiUrl(["artefacts"], null)
    return await this._request(url)
  }

  async _getSchemesModLimit(limit) {
    const artifacts = await this._getSchemesMod()
    if (limit && limit > 0) {
      return artifacts.slice(0, limit)
    }
    return artifacts
  }

  async _getSchemeMod(schemeParam) {
    //https://terminology.services.base4nfdi.de/api-gateway/artefacts/<schemeShort>
    // const schemeShort = await this._schemeShortFromObj(schemeParam)
    if (schemeParam.short) {
      return await this._getSchemeFromShort(schemeParam.short)
    } else if (schemeParam.uri) {
      return await this._getSchemeFromUri(schemeParam.uri)
    }
  }

  async _getSchemeFromShort(short) {
    const url = this._getApiUrl(["artefacts", short], null)
    return await this._request(url)
  }

  async _getSchemeFromUri(uri) {
    const schemesMod = await this._getSchemesMod()
    if (!schemesMod){
      return
    }
    for (const scheme of await schemesMod) {
      if (
        scheme.source == uri
        || scheme.source_url == uri
        || scheme.source_name == uri
        || scheme["@id"] == uri
        || scheme.iri == uri
        || scheme.includedInDataCatalog && scheme.includedInDataCatalog.includes(uri)
      ) {
        return await scheme
      }
    }
  }

  // API REQUESTS CONCEPTS

  async _getConceptsMod(scheme) {
    // https://terminology.services.base4nfdi.de/api-gateway/artefacts/<schemeShort>/resources/concepts
    let schemeShort = await this._schemeShortFromObj(scheme)
    if (!schemeShort) {
      return []
    }

    // pull page 1
    const url = this._getApiUrl(["artefacts", schemeShort, "resources", "concepts"], null)
    const pageOne = await this._request(url)
    if (!pageOne){
      return []
    }
    const {page, totalPages, member: conceptsOne} = pageOne

    let concepts = []
    for (const concept of conceptsOne) {
      if (concept) {
        concepts.push(concept)
      }
    }

    // pull remaining pages
    for (let p = page+1; p <= totalPages; p++) {
      const urlPage = this._getApiUrl(["artefacts", schemeShort, "resources", "concepts"], {page: p})
      const pageP = await this._request(urlPage)
      if (!pageP){
        break
      }
      const {member: conceptsNew} = pageP
      for (const concept of conceptsNew) {
        if (concept) {
          concepts.push(concept)
        }
      }
    }
    return concepts
  }

  async _getConceptsModLimit(scheme, limit) {
    let concepts = await this._getConceptsMod(scheme)
    if (limit && limit > 0) {
      return concepts.slice(0, limit)
    }
    return concepts
  }

  async _getConceptMod(concept) {
    // https://terminology.services.base4nfdi.de/api-gateway/artefacts/<schemeShort>/resources/concepts/<conceptNotation>
    const {conceptNotation, schemeShort} = await this._conceptNotationFromObj(concept)
    const url = this._getApiUrl(["artefacts", schemeShort, "resources", "concepts", conceptNotation], null)
    return await this._request(url)
  }

  // UTILITIES

  _containsString(obj, searchString) {
    for (const key in obj) {
      const value = obj[key]

      if (typeof value === "string" && value.includes(searchString)) {
        return true
      }

      if (typeof value === "object" && value !== null) {
        if (this.containsString(value, searchString)) {
          return true
        }
      }
    }
    return false
  }

  async _getSchemesContaining(partstring) {
    let schemes = []
    const schemesMod = await this._getSchemesMod()
    for (const scheme of schemesMod) {
      if (this.containsString(scheme, partstring)) {
        schemes.push(scheme)
      }
    }
    return schemes
  }

  async _getSchemeShort(uri) {
    const schemeMod = await this._getSchemeFromUri(uri)
    if (schemeMod) {
      return await schemeMod.short_form.toLowerCase()
    }
  }

  _getconceptNotation(uri) {
    return uri.split("/").pop()
  }

  async _schemeShortFromObj(scheme) {
    if (scheme.short){
      return scheme.short
    } else if (scheme.uri) {
      return await this._getSchemeShort(scheme.uri)
    }
  }

  async _conceptNotationFromObj(concept) {
    if (!concept.inScheme || !concept.inScheme[0]) {
      return
    }
    let schemeShort = await this._schemeShortFromObj(concept.inScheme[0])
    let conceptNotation = concept.notation
    if (!conceptNotation){
      conceptNotation = await this._getconceptNotation(concept.uri)
    }
    return {conceptNotation: conceptNotation, schemeShort: schemeShort}
  }

  // #### 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() {}

  /**
   * Retrieves all concept schemes from the MOD API.
   *
   * @param {Object} [params={}] - Optional parameters for the request.
   * @returns {Promise<Array>} An array of JSKOS concept schemes.
   * @async
   */
  async getSchemes({schemes, limit, ..._config}) {
    let schemes_results = []
    let artefacts = []
    if (schemes) {
      for (const s of schemes) {
        let sc = await this._getSchemeMod(s)
        if (sc) {
          artefacts.push(sc)
        }
      }
    } else {
      artefacts = await this._getSchemesModLimit(limit)
    }

    for (const artefact of artefacts) {
      let scheme = await this._artefactToJSKOS(artefact)
      if (scheme) {
        schemes_results.push(scheme)
      } else {
        console.warn("JSKOS transformation failed for artefact: ", artefact)
      }
    }
    return schemes_results
  }

  /**
 * Retrieves all concepts from the MOD API.
 *
 * @param {Object} params - The options object.
 * @param {string[]} params.concepts - List of concept objects to request specific concepts.
 * @param {string} params.scheme - A scheme object to request concepts from a specific scheme.
 * @param {number} [params.limit] - Optional limit for results when requesting concepts from a scheme.
 * @param {Object} [params._config] - Additional config options.
 * @returns {Promise<Array>} An array of JSKOS concepts.
 * @async
 */
  async getConcepts({concepts, scheme, limit, ..._config}) {
    let concept_results = []
    if (concepts) {
      for (const concept of concepts) {
        let conceptMod = await this._getConceptMod(concept)
        if (conceptMod) {
          const concept = await this._artefactToJSKOS(conceptMod)
          if (concept) {
            concept_results.push(concept)
          } else {
            console.warn("JSKOS transformation failed for concept: ", conceptMod)
          }
        }
      }
    } else if (scheme) {
      const conceptsMod = await this._getConceptsModLimit(scheme, limit)
      for (const conceptMod of conceptsMod) {
        const conceptJ = await this._artefactToJSKOS(conceptMod)
        if (conceptJ) {
          concept_results.push(conceptJ)
        } else {
          console.warn("JSKOS transformation failed for concept: ", conceptMod)
        }
      }
    }
    return concept_results
  }

  /**
   * @private
   */
  get _language() {
    return this._jskos.language || this.languages[0] || this._defaultLanguages[0] || "en"
  }

  /**
   * Retrieves an array of mappings.
   * @returns {Array} An array containing mapping objects.
   */
  getMappings() {
    const mappings = []
    return mappings
  }
}