providers_mappings-api-provider.js

import BaseProvider from "./base-provider.js"
import jskos from "jskos-tools"
import * as errors from "../errors/index.js"
import { concatUrl } from "../utils/index.js"

// TODO: Check capabilities (`this.has`) and authorization (`this.isAuthorizedFor`) before actions.

/**
 * JSKOS Mappings API.
 *
 * This class provides access to concept mappings via JSKOS API in [JSKOS format](https://gbv.github.io/jskos/).
 * See [jskos-server](https://github.com/gbv/jskos-server) for a JSKOS API reference implementation
 *
 * To use it in a registry, specify `provider` as "MappingsApi" and provide the API base URL as `api`:
 * ```json
 * {
 *  "uri": "http://coli-conc.gbv.de/registry/coli-conc-mappings",
 *  "provider": "MappingsApi",
 *  "api": "https://coli-conc.gbv.de/api/"
 * }
 * ```
 *
 * If the `/status` endpoint can be queried, the remaining API methods will be taken from that. As a fallback, the default endpoints will be appended to `api`.
 *
 * Alternatively, you can provide the endpoints separately: `status`, `mappings`, `concordances`, `annotations`
 *
 * Additionally, the following JSKOS properties can be provided: `prefLabel`, `notation`, `definition`
 *
 * @extends BaseProvider
 * @category Providers
 */
export default class MappingsApiProvider extends BaseProvider {
  static supports = {
    mappings: true,
    concordances: true,
    annotations: true,
  }

  /**
   * @private
   */
  _prepare() {
    // Set status endpoint only
    if (this._api.api && this._api.status === undefined) {
      this._api.status = concatUrl(this._api.api, "/status")
    }
  }

  /**
   * @private
   */
  _setup() {
    // Fill `this._api` if necessary
    if (this._api.api) {
      const endpoints = {
        mappings: "/mappings",
        concordances: "/concordances",
        annotations: "/annotations",
      }
      for (let key of Object.keys(endpoints)) {
        if (this._api[key] === undefined) {
          this._api[key] = concatUrl(this._api.api, endpoints[key])
        }
      }
    }
    this.has.mappings = this._api.mappings ? {} : false
    if (this.has.mappings) {
      this.has.mappings.read = !!(this._config?.mappings?.read ?? true)
      this.has.mappings.create = !!this._config?.mappings?.create
      this.has.mappings.update = !!this._config?.mappings?.update
      this.has.mappings.delete = !!this._config?.mappings?.delete
      this.has.mappings.anonymous = !!this._config?.mappings?.anonymous
    }
    this.has.concordances = this._api.concordances ? {} : false
    if (this.has.concordances) {
      this.has.concordances.read = !!this._config?.concordances?.read
      this.has.concordances.create = !!this._config?.concordances?.create
      this.has.concordances.update = !!this._config?.concordances?.update
      this.has.concordances.delete = !!this._config?.concordances?.delete
    }
    this.has.annotations = this._api.annotations ? {} : false
    if (this.has.annotations) {
      this.has.annotations.read = !!this._config?.annotations?.read
      this.has.annotations.create = !!this._config?.annotations?.create
      this.has.annotations.update = !!this._config?.annotations?.update
      this.has.annotations.delete = !!this._config?.annotations?.delete
    }
    this.has.auth = this._config?.auth?.key != null
    this._defaultParams = {
      properties: "annotations",
    }
  }

  /**
   * Returns a single mapping.
   *
   * @param {Object} config
   * @param {Object} config.mapping JSKOS mapping
   * @returns {Object} JSKOS mapping object
   */
  async getMapping({ mapping, ...config }) {
    if (!mapping) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
    }
    if (!mapping.uri || !mapping.uri.startsWith(this._api.mappings)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "URI doesn't seem to be part of this registry." })
    }
    try {
      return await this.axios({
        ...config,
        url: mapping.uri,
        params: {
          ...this._defaultParams,
          ...(config.params || {}),
        },
      })
    } catch (error) {
      if (error?.response?.status == 404) {
        return null
      }
      throw error
    }
  }

  /**
   * Returns a list of mappings.
   *
   * @param {Object} config request config with parameters
   * @returns {Object[]} array of JSKOS mapping objects
   */
  async getMappings({ from, fromScheme, to, toScheme, creator, type, partOf, offset, limit, direction, mode, identifier, cardinality, annotatedBy, annotatedFor, annotatedWith, sort, order, ...config }) {
    let params = {}, url = this._api.mappings
    if (from) {
      params.from = typeof from === "string" ? from : from.uri
    }
    if (fromScheme) {
      params.fromScheme = typeof fromScheme === "string" ? fromScheme : fromScheme.uri
    }
    if (to) {
      params.to = typeof to === "string" ? to : to.uri
    }
    if (toScheme) {
      params.toScheme = typeof toScheme === "string" ? toScheme : toScheme.uri
    }
    if (creator) {
      params.creator = typeof creator === "string" ? creator : jskos.prefLabel(creator)
    }
    if (type) {
      params.type = typeof type === "string" ? type : type.uri
    }
    if (partOf) {
      params.partOf = typeof partOf === "string" ? partOf : partOf.uri
    }
    if (offset) {
      params.offset = offset
    }
    if (limit) {
      params.limit = limit
    }
    if (direction) {
      params.direction = direction
    }
    if (cardinality) {
      params.cardinality = cardinality
    }
    if (annotatedBy) {
      params.annotatedBy = annotatedBy
    }
    if (annotatedFor) {
      params.annotatedFor = annotatedFor
    }
    if (annotatedWith) {
      params.annotatedWith = annotatedWith
    }
    if (mode) {
      params.mode = mode
    }
    if (identifier) {
      params.identifier = identifier
    }
    if (sort) {
      params.sort = sort
    }
    if (order) {
      params.order = order
    }
    return this.axios({
      ...config,
      method: "get",
      url,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
        ...params,
      },
    })
  }

  /**
   * Creates a mapping.
   *
   * @param {Object} config
   * @param {Object} config.mapping JSKOS mapping
   * @returns {Object} JSKOS mapping object
   */
  async postMapping({ mapping, ...config }) {
    if (!mapping) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
    }
    mapping = jskos.minifyMapping(mapping)
    mapping = jskos.addMappingIdentifiers(mapping)
    return this.axios({
      ...config,
      method: "post",
      url: this._api.mappings,
      data: mapping,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
      },
    })
  }

  /**
   * Overwrites a mapping.
   *
   * @param {Object} config
   * @param {Object} config.mapping JSKOS mapping
   * @returns {Object} JSKOS mapping object
   */
  async putMapping({ mapping, ...config }) {
    if (!mapping) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
    }
    mapping = jskos.minifyMapping(mapping)
    mapping = jskos.addMappingIdentifiers(mapping)
    const uri = mapping.uri
    if (!uri || !uri.startsWith(this._api.mappings)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "URI doesn't seem to be part of this registry." })
    }
    return this.axios({
      ...config,
      method: "put",
      url: uri,
      data: mapping,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
      },
    })
  }

  /**
   * Patches a mapping.
   *
   * @param {Object} config
   * @param {Object} config.mapping JSKOS mapping (or part of mapping)
   * @returns {Object} JSKOS mapping object
   */
  async patchMapping({ mapping, ...config }) {
    if (!mapping) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
    }
    if (!mapping.uri?.startsWith(this._api.mappings)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "URI doesn't seem to be part of this registry." })
    }
    const { uri, ...data } = mapping
    return this.axios({
      ...config,
      method: "patch",
      url: uri,
      data,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
      },
    })
  }

  /**
   * Deletes a mapping.
   *
   * @param {Object} config
   * @param {Object} config.mapping JSKOS mapping
   * @returns {boolean} `true` if deletion was successful
   */
  async deleteMapping({ mapping, ...config }) {
    if (!mapping) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
    }
    const uri = mapping.uri
    if (!uri || !uri.startsWith(this._api.mappings)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "URI doesn't seem to be part of this registry." })
    }
    await this.axios({
      ...config,
      method: "delete",
      url: uri,
    })
    return true
  }

  /**
   * Returns a list of annotations.
   *
   * @param {Object} config
   * @param {string} [config.target] target URI
   * @returns {Object[]} array of JSKOS annotation objects
   */
  async getAnnotations({ target, ...config }) {
    if (target) {
      config.params ||= {}
      config.params.target = target
    }
    return this.axios({
      ...config,
      method: "get",
      url: this._api.annotations,
    })
  }

  /**
   * Creates an annotation.
   *
   * @param {Object} config
   * @param {Object} config.annotation JSKOS annotation
   * @returns {Object} JSKOS annotation object
   */
  async postAnnotation({ annotation, ...config }) {
    return this.axios({
      ...config,
      method: "post",
      url: this._api.annotations,
      data: annotation,
    })
  }

  /**
   * Overwrites an annotation.
   *
   * @param {Object} config
   * @param {Object} config.annotation JSKOS annotation
   * @returns {Object} JSKOS annotation object
   */
  async putAnnotation({ annotation, ...config }) {
    const uri = annotation.id
    if (!uri || !uri.startsWith(this._api.annotations)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "annotation", message: "URI doesn't seem to be part of this registry." })
    }
    return this.axios({
      ...config,
      method: "put",
      url: uri,
      data: annotation,
    })
  }

  /**
   * Patches an annotation.
   *
   * @param {Object} config
   * @param {Object} config.annotation JSKOS annotation
   * @returns {Object} JSKOS annotation object
   */
  async patchAnnotation({ annotation, ...config }) {
    const uri = annotation.id
    if (!uri || !uri.startsWith(this._api.annotations)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "annotation", message: "URI doesn't seem to be part of this registry." })
    }
    return this.axios({
      ...config,
      method: "patch",
      url: uri,
      data: annotation,
    })
  }

  /**
   * Deletes an annotation.
   *
   * @param {Object} config
   * @param {Object} config.annotation JSKOS annotation
   * @returns {boolean} `true` if deletion was successful
   */
  async deleteAnnotation({ annotation, ...config }) {
    const uri = annotation.id
    if (!uri || !uri.startsWith(this._api.annotations)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "annotation", message: "URI doesn't seem to be part of this registry." })
    }
    await this.axios({
      ...config,
      method: "delete",
      url: uri,
    })
    return true
  }

  /**
   * Returns a list of concordances.
   *
   * @param {Object} config
   * @returns {Object[]} array of JSKOS concordance objects
   */
  async getConcordances(config) {
    return this.axios({
      ...config,
      method: "get",
      url: this._api.concordances,
    })
  }

  /**
   * Creates a concordance.
   *
   * @param {Object} config
   * @param {Object} config.concordance JSKOS concordance
   * @returns {Object} JSKOS concordance object
   */
  async postConcordance({ concordance, ...config }) {
    if (!concordance) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance" })
    }
    return this.axios({
      ...config,
      method: "post",
      url: this._api.concordances,
      data: concordance,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
      },
    })
  }

  /**
   * Overwrites a concordance.
   *
   * @param {Object} config
   * @param {Object} config.concordance JSKOS concordance
   * @returns {Object} JSKOS concordance object
   */
  async putConcordance({ concordance, ...config }) {
    if (!concordance) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance" })
    }
    const uri = concordance.uri
    if (!uri || !uri.startsWith(this._api.concordances)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance", message: "URI doesn't seem to be part of this registry." })
    }
    return this.axios({
      ...config,
      method: "put",
      url: uri,
      data: concordance,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
      },
    })
  }

  /**
   * Patches a concordance.
   *
   * @param {Object} config
   * @param {Object} config.concordance JSKOS concordance (or part of concordance)
   * @returns {Object} JSKOS concordance object
   */
  async patchConcordance({ concordance, ...config }) {
    if (!concordance) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance" })
    }
    if (!concordance.uri?.startsWith(this._api.concordances)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance", message: "URI doesn't seem to be part of this registry." })
    }
    const { uri, ...data } = concordance
    return this.axios({
      ...config,
      method: "patch",
      url: uri,
      data,
      params: {
        ...this._defaultParams,
        ...(config.params || {}),
      },
    })
  }

  /**
   * Deletes a concordance.
   *
   * @param {Object} config
   * @param {Object} config.concordance JSKOS concordance
   * @returns {boolean} `true` if deletion was successful
   */
  async deleteConcordance({ concordance, ...config }) {
    if (!concordance) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance" })
    }
    const uri = concordance.uri
    if (!uri || !uri.startsWith(this._api.concordances)) {
      throw new errors.InvalidOrMissingParameterError({ parameter: "concordance", message: "URI doesn't seem to be part of this registry." })
    }
    await this.axios({
      ...config,
      method: "delete",
      url: uri,
    })
    return true
  }

}

MappingsApiProvider.providerName = "MappingsApi"
MappingsApiProvider.stored = true