providers_local-mappings-provider.js
import BaseProvider from "./base-provider.js"
import jskos from "jskos-tools"
import * as _ from "../utils/lodash.js"
import localforage from "localforage"
import { v4 as uuid } from "uuid"
import * as errors from "../errors/index.js"
const uriPrefix = "urn:uuid:"
/**
* Local Mappings.
*
* This class provides read-write access to mappings in the browser's local storage.
*
* To use it in a registry, specify `provider` as "LocalMappings":
* ```json
* {
* "uri": "http://coli-conc.gbv.de/registry/local-mappings",
* "provider": "LocalMappings"
* }
* ```
*
* Additionally, the following JSKOS properties can be provided: `prefLabel`, `notation`, `definition`
*
* @extends BaseProvider
* @category Providers
*/
export default class LocalMappingsProvider extends BaseProvider {
static get supports() {
return {
mappings: {
read: true,
create: true,
update: true,
delete: true,
},
}
}
/**
* @private
*/
_setup() {
this.queue = []
this.localStorageKey = "cocoda-mappings--" + this._path
let oldLocalStorageKey = "mappings"
// Function that adds URIs to all existing local mappings that don't yet have one
let addUris = () => {
return localforage.getItem(this.localStorageKey).then(mappings => {
mappings = mappings || []
let adjusted = 0
for (let mapping of mappings.filter(m => !m.uri || !m.uri.startsWith(uriPrefix))) {
if (mapping.uri) {
// Keep previous URI in identifier
if (!mapping.identifier) {
mapping.identifier = []
}
mapping.identifier.push(mapping.uri)
}
mapping.uri = `${uriPrefix}${uuid()}`
adjusted += 1
}
if (adjusted) {
console.warn(`URIs added to ${adjusted} local mappings.`)
}
return localforage.setItem(this.localStorageKey, mappings)
})
}
// Show warning if there are mappings in local storage that use the old local storage key.
localforage.getItem(oldLocalStorageKey).then(results => {
if (results) {
console.warn(`Warning: There is old data in local storage (or IndexedDB, depending on the ) with the key "${oldLocalStorageKey}". This data will not be used anymore. A manual export is necessary to get this data back.`)
}
})
// Put promise into queue so that getMappings requests are waiting for adjustments to finish
this.queue.push(
addUris().catch(error => {
console.warn("Error when adding URIs to local mappings:", error)
}),
)
}
isAuthorizedFor({ type, action }) {
// Allow all for mappings
if (type == "mappings" && action != "anonymous") {
return true
}
return false
}
/**
* Returns a Promise that returns an object { mappings, done } with the local mappings and a done function that is supposed to be called when the transaction is finished.
* This prevents conflicts when saveMapping is called multiple times simultaneously.
*
* TODO: There might be a better solution for this...
*
* @private
*/
_getMappingsQueue() {
let last = _.last(this.queue) || Promise.resolve()
return new Promise((resolve) => {
function defer() {
let res, rej
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
promise.resolve = res
promise.reject = rej
return promise
}
let promise = defer()
let done = () => {
promise.resolve()
}
this.queue.push(promise)
last.then(() => {
return localforage.getItem(this.localStorageKey)
}).then(mappings => {
resolve({ mappings, done })
})
})
}
/**
* Returns a single mapping.
*
* @param {Object} config
* @param {Object} config.mapping JSKOS mapping
* @returns {Object} JSKOS mapping object
*/
async getMapping({ mapping, ...config }) {
config._raw = true
if (!mapping || !mapping.uri) {
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
}
return (await this.getMappings({ ...config, uri: mapping.uri }))[0]
}
/**
* Returns a list of local mappings.
*
* TODO: Add support for sort (`created` or `modified`) and order (`asc` or `desc`).
* TODO: Clean up and use async/await
*
* @returns {Object[]} array of JSKOS mapping objects
*/
async getMappings({ from, fromScheme, to, toScheme, creator, type, partOf, offset, limit, direction, mode, identifier, uri } = {}) {
let params = {}
if (from) {
params.from = _.isString(from) ? from : from.uri
}
if (fromScheme) {
params.fromScheme = _.isString(fromScheme) ? { uri: fromScheme } : fromScheme
}
if (to) {
params.to = _.isString(to) ? to : to.uri
}
if (toScheme) {
params.toScheme = _.isString(toScheme) ? { uri: toScheme } : toScheme
}
if (creator) {
params.creator = _.isString(creator) ? creator : jskos.prefLabel(creator)
}
if (type) {
params.type = _.isString(type) ? type : type.uri
}
if (partOf) {
params.partOf = _.isString(partOf) ? partOf : partOf.uri
}
if (offset) {
params.offset = offset
}
if (limit) {
params.limit = limit
}
if (direction) {
params.direction = direction
}
if (mode) {
params.mode = mode
}
if (identifier) {
params.identifier = identifier
}
if (uri) {
params.uri = uri
}
return this._getMappingsQueue().catch(relatedError => {
throw new errors.CDKError({ message: "Could not get mappings from local storage", relatedError })
}).then(({ mappings, done }) => {
done()
// Check concept with param
let checkConcept = (concept, param) => concept.uri == param || (param && concept.notation && concept.notation[0].toLowerCase() == param.toLowerCase())
// Filter mappings according to params (support for from + to)
// TODO: - Support more parameters.
// TODO: - Move to its own things.
// TODO: - Clean all this up.
if (params.from || params.to) {
mappings = mappings.filter(mapping => {
let fromInFrom = null != jskos.conceptsOfMapping(mapping, "from").find(concept => checkConcept(concept, params.from))
let fromInTo = null != jskos.conceptsOfMapping(mapping, "to").find(concept => checkConcept(concept, params.from))
let toInFrom = null != jskos.conceptsOfMapping(mapping, "from").find(concept => checkConcept(concept, params.to))
let toInTo = null != jskos.conceptsOfMapping(mapping, "to").find(concept => checkConcept(concept, params.to))
if (params.direction == "backward") {
if (params.mode == "or") {
return (params.from && fromInTo) || (params.to && toInFrom)
} else {
return (!params.from || fromInTo) && (!params.to || toInFrom)
}
} else if (params.direction == "both") {
if (params.mode == "or") {
return (params.from && (fromInFrom || fromInTo)) || (params.to && (toInFrom || toInTo))
} else {
return ((!params.from || fromInFrom) && (!params.to || toInTo)) || ((!params.from || fromInTo) && (!params.to || toInFrom))
}
} else {
if (params.mode == "or") {
return (params.from && fromInFrom) || (params.to && toInTo)
} else {
return (!params.from || fromInFrom) && (!params.to || toInTo)
}
}
})
}
if (params.fromScheme || params.toScheme) {
mappings = mappings.filter(mapping => {
let fromInFrom = jskos.compare(mapping.fromScheme, params.fromScheme)
let fromInTo = jskos.compare(mapping.toScheme, params.fromScheme)
let toInFrom = jskos.compare(mapping.fromScheme, params.toScheme)
let toInTo = jskos.compare(mapping.toScheme, params.toScheme)
if (params.direction == "backward") {
if (params.mode == "or") {
return (params.fromScheme && fromInTo) || (params.toScheme && toInFrom)
} else {
return (!params.fromScheme || fromInTo) && (!params.toScheme || toInFrom)
}
} else if (params.direction == "both") {
if (params.mode == "or") {
return (params.fromScheme && (fromInFrom || fromInTo)) || (params.toScheme && (toInFrom || toInTo))
} else {
return ((!params.fromScheme || fromInFrom) && (!params.toScheme || toInTo)) || ((!params.fromScheme || fromInTo) && (!params.toScheme || toInFrom))
}
} else {
if (params.mode == "or") {
return (params.fromScheme && fromInFrom) || (params.toScheme && toInTo)
} else {
return (!params.fromScheme || fromInFrom) && (!params.toScheme || toInTo)
}
}
})
}
// creator
if (params.creator) {
let creators = params.creator.split("|")
mappings = mappings.filter(mapping => {
return (mapping.creator && mapping.creator.find(creator => creators.includes(jskos.prefLabel(creator)) || creators.includes(creator.uri))) != null
})
}
// type
if (params.type) {
mappings = mappings.filter(mapping => (mapping.type || [jskos.defaultMappingType.uri]).includes(params.type))
}
// concordance
if (params.partOf) {
mappings = mappings.filter(mapping => {
return mapping.partOf != null && mapping.partOf.find(partOf => jskos.compare(partOf, { uri: params.partOf })) != null
})
}
// identifier
if (params.identifier) {
mappings = mappings.filter(mapping => {
return params.identifier.split("|").map(identifier => {
return (mapping.identifier || []).includes(identifier) || mapping.uri == identifier
}).reduce((current, total) => current || total)
})
}
if (params.uri) {
mappings = mappings.filter(mapping => mapping.uri == params.uri)
}
let totalCount = mappings.length
// Sort mappings (default: modified/created date descending)
mappings = mappings.sort((a, b) => {
let aDate = a.modified || a.created
let bDate = b.modified || b.created
if (bDate == null) {
return -1
}
if (aDate == null) {
return 1
}
if (aDate > bDate) {
return -1
}
return 1
})
mappings = mappings.slice(params.offset || 0)
mappings = mappings.slice(0, params.limit)
mappings._totalCount = totalCount
return mappings
})
}
/**
* Creates a mapping.
*
* @param {Object} config
* @param {Object} config.mapping JSKOS mapping
* @returns {Object} JSKOS mapping object
*/
async postMapping({ mapping }) {
if (!mapping) {
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
}
let { mappings: localMappings, done } = await this._getMappingsQueue()
// Set URI if necessary
if (!mapping.uri || !mapping.uri.startsWith(uriPrefix)) {
if (mapping.uri) {
// Keep previous URI in identifier
if (!mapping.identifier) {
mapping.identifier = []
}
mapping.identifier.push(mapping.uri)
}
mapping.uri = `${uriPrefix}${uuid()}`
}
// Check if mapping already exists => throw error
if (localMappings.find(m => m.uri == mapping.uri)) {
done()
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "Duplicate URI" })
}
// Set created/modified
if (!mapping.created) {
mapping.created = (new Date()).toISOString()
}
if (!mapping.modified) {
mapping.modified = mapping.created
}
// Remove partOf (no concordances supported in local mappings)
delete mapping.partOf
// Add to local mappings
localMappings.push(mapping)
// Minify mappings before saving back to local storage
localMappings = localMappings.map(mapping => jskos.minifyMapping(mapping))
// Write local mappings
try {
await localforage.setItem(this.localStorageKey, localMappings)
done()
return mapping
} catch (error) {
done()
throw error
}
}
/**
* Overwrites a mapping.
*
* @param {Object} config
* @param {Object} config.mapping JSKOS mapping
* @returns {Object} JSKOS mapping object
*/
async putMapping({ mapping }) {
if (!mapping) {
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
}
let { mappings: localMappings, done } = await this._getMappingsQueue()
// Check if mapping already exists => throw error if it doesn't
const index = localMappings.findIndex(m => m.uri == mapping.uri)
if (index == -1) {
done()
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "Mapping not found" })
}
// Set created/modified
if (!mapping.created) {
mapping.created = localMappings[index].created
}
mapping.modified = (new Date()).toISOString()
// Remove partOf (no concordances supported in local mappings)
delete mapping.partOf
// Add to local mappings
localMappings[index] = mapping
// Minify mappings before saving back to local storage
localMappings = localMappings.map(mapping => jskos.minifyMapping(mapping))
// Write local mappings
try {
await localforage.setItem(this.localStorageKey, localMappings)
done()
return mapping
} catch (error) {
done()
throw error
}
}
/**
* Patches a mapping.
*
* @param {Object} config
* @param {Object} mapping JSKOS mapping (or part of mapping)
* @returns {Object} JSKOS mapping object
*/
async patchMapping({ mapping }) {
if (!mapping) {
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
}
let { mappings: localMappings, done } = await this._getMappingsQueue()
// Check if mapping already exists => throw error if it doesn't
const index = localMappings.findIndex(m => m.uri == mapping.uri)
if (index == -1) {
done()
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping", message: "Mapping not found" })
}
// Set created/modified
if (!mapping.created) {
mapping.created = localMappings[index].created
}
mapping.modified = (new Date()).toISOString()
// Remove partOf (no concordances supported in local mappings)
delete mapping.partOf
// Add to local mappings
localMappings[index] = Object.assign(localMappings[index], mapping)
// Minify mappings before saving back to local storage
localMappings = localMappings.map(mapping => jskos.minifyMapping(mapping))
// Write local mappings
try {
await localforage.setItem(this.localStorageKey, localMappings)
done()
return mapping
} catch (error) {
done()
throw error
}
}
/**
* Removes a mapping from local storage.
*
* @param {Object} config
* @param {Object} mapping JSKOS mapping
* @returns {boolean} boolean whether deleting the mapping was successful
*/
async deleteMapping({ mapping }) {
if (!mapping) {
throw new errors.InvalidOrMissingParameterError({ parameter: "mapping" })
}
let { mappings: localMappings, done } = await this._getMappingsQueue()
try {
// Remove by URI
localMappings = localMappings.filter(m => m.uri != mapping.uri)
// Minify mappings before saving back to local storage
localMappings = localMappings.map(mapping => jskos.minifyMapping(mapping))
await localforage.setItem(this.localStorageKey, localMappings)
done()
return true
} catch (error) {
done()
throw error
}
}
}
LocalMappingsProvider.providerName = "LocalMappings"
LocalMappingsProvider.stored = true