@eluvio/elv-utils-js
Version:
Utilities for the Eluvio Content Fabric
203 lines (171 loc) • 6.54 kB
JavaScript
const kindOf = require('kind-of')
const objectPath = require('object-path')
const R = require('@eluvio/ramda-fork')
const isString = require('@eluvio/elv-js-helpers/Boolean/isString')
const {fabricItemDesc} = require('../helpers')
const Client = require('./Client')
const Edit = require('./Edit')
const JSON = require('./JSON')
const Logger = require('./Logger')
const pathRegex = /^(\/[^/]+)+$/
const arrayToPath = arr => `/${arr.join('/')}`
const pathDesc = path => path ? `path '${path}' ` : ''
const pathExists = ({metadata, path}) => objectPath.has(metadata, pathToArray({path}))
const pathPieceIsInt = str => parseInt(str, 10).toString() === str
// convert path in slash format to an array for use with object-path
// numbers are assumed to be array indexes rather than map keys
// path must start with "/"
const pathToArray = ({path}) => {
if (!path) throw Error('Metadata.pathToArray(): path not supplied')
if (!isString(path)) throw Error('Metadata.pathToArray(): path must be a string')
if (path.slice(0, 1) !== '/') throw Error('Metadata.pathToArray(): path must start with \'/\'')
let result = path.split('/')
// remove empty string at beginning (should always be present)
result.shift()
// if we have an empty string at end, remove (path ended with '/')
if (result.slice(-1)[0] === '') result.pop()
return result
}
const pretty = ({obj}) => JSON.stringify(obj, null, 2)
const skeleton = () => Object({public: {}})
const validatePathExists = ({metadata, path}) => {
if (!pathExists({metadata, path})) throw Error(`'${path}' not found in metadata`)
}
const validatePathFormat = ({path}) => {
if (!validPathFormat({path})) throw Error(`'${path}' is not in valid format for a metadata path (make sure it starts with '/')`)
}
// Check that path can be set (creating parents if needed)
// Throw error otherwise
// (does not check if data already exists at the full targetPath - only
// checks that writing to targetPath is not blocked by a non-object value earlier in object)
const validateTargetPath = ({metadata, path}) => {
const pathArr = pathToArray({path})
if (objectPath.has(metadata, pathArr)) return true
while (pathArr.length > 0) {
const key = pathArr.pop()
if (objectPath.has(metadata, pathArr)) {
// get parent value (if it exists)
const value = objectPath.get(metadata, pathArr)
// check that key is valid
switch (kindOf(value)) {
case 'object':
case 'undefined': // not expected, but allow it
return true
case 'array':
if (pathPieceIsInt(key)) {
const i = parseInt(key, 10)
if (i < 0 || i > value.length) throw Error(`${path} is not a valid target metadata path, ${arrayToPath(pathArr)} contains an array of length ${value.length} (${key} is not a valid index for setting or adding a value)`)
} else {
throw Error(`${path} is not a valid target metadata path, ${arrayToPath(pathArr)} contains an array and '${key}' is not a valid array index`)
}
return true // array index is valid
default:
throw Error(`${path} is not a valid target metadata path, ${arrayToPath(pathArr)} contains a value that cannot have children added`)
}
}
}
}
const validPathFormat = ({path}) => path === '/' || path.match(pathRegex) && path.match(pathRegex)[0] === path
// Makes sure all attributes along object path are objects or undefined, and that path ends at an undefined attribute
const validTargetPath = ({metadata, targetPath}) => {
let pathArr = pathToArray({path: targetPath})
let currentSubtree = R.clone(metadata)
for (const key of pathArr) {
if (currentSubtree === undefined) {
// reached end of tree, all the rest of keys in targetPath can be created under this point
return true
}
if (kindOf(currentSubtree) !== 'object') {
break
}
currentSubtree = currentSubtree[key]
}
// Make sure end is undefined
return currentSubtree === undefined
}
const valueAtPath = ({metadata, path}) => objectPath.get(metadata, pathToArray({path}))
const blueprint = {
name: 'Metadata',
concerns: [Logger, Client, Edit]
}
const New = context => {
const logger = context.concerns.Logger
const checkTargetPath = ({force, metadata, targetPath}) => {
if (!validTargetPath({metadata, targetPath})) {
const existingExcerpt = JSON.shortString({
obj: valueAtPath({
metadata,
path: targetPath
})
})
if (force) {
logger.warn(`Data already exists at '${targetPath}', --force specified, replacing...\nOverwritten data: ${existingExcerpt}`)
} else {
throw new Error(`Metadata path '${targetPath}' is invalid (already exists, use --force to replace). Existing data: ${existingExcerpt}`)
}
}
}
const commitInfo = async ({libraryId, objectId, versionHash, writeToken}) => {
// logger.log(`Retrieving commit info for ${fabricItemDesc({objectId, versionHash, writeToken})}...`)
return await get({
libraryId,
objectId,
subtree: '/commit',
versionHash,
writeToken
})
}
const del = async({commitMessage, libraryId, subtree, objectId, writeToken}) => {
logger.log(`Deleting metadata ${pathDesc(subtree)}from ${fabricItemDesc({objectId, writeToken})}...`)
return await context.concerns.Edit.deleteMetadata({
commitMessage,
libraryId,
metadataSubtree: subtree,
objectId,
writeToken
})
}
const get = async ({libraryId, subtree, objectId, versionHash, writeToken}) => {
const client = await context.concerns.Client.get()
logger.log(`Retrieving metadata ${pathDesc(subtree)}from ${fabricItemDesc({objectId, versionHash, writeToken})}...`)
return await client.ContentObjectMetadata({
libraryId,
metadataSubtree: subtree,
objectId,
versionHash,
writeToken
})
}
const write = async ({commitMessage, libraryId, metadata, noWait, objectId, subtree, writeToken}) => {
return await context.concerns.Edit.writeMetadata({
commitMessage,
libraryId,
metadata,
metadataSubtree: subtree,
noWait,
objectId,
writeToken,
})
}
return {
checkTargetPath,
commitInfo,
del,
get,
write
}
}
module.exports = {
blueprint,
pathExists,
pathToArray,
pretty,
New,
skeleton,
validatePathExists,
validatePathFormat,
validateTargetPath,
validPathFormat,
validTargetPath,
valueAtPath
}