UNPKG

hyperpubee

Version:

Self-publishing over the decentralised internet

300 lines (254 loc) 7.5 kB
const utils = require('./utils') const hexlexi = require('./hexlexi') const { CONTENT, TITLE, STRUCTURE, ROOT, EMBEDDING, EMPTY, KEYNAME_REGEX, METADATA, LINK } = require('./constants') const { validateEmbedding } = require('./embedding') const { validateLink } = require('./link') const { ValidationError, NotUnpackableError, EmbeddingValidationError } = require('../exceptions') async function ensureSubIsIndexedConsecutively ({ sub, subName, canBeEmpty = false } = {}) { let expectedIntI = 0 for await (const { key } of sub.createReadStream()) { let intI try { intI = hexlexi.unpack(key) } catch (e) { if (e instanceof NotUnpackableError) { const msg = `${subName} with invalid key: '${key}' (must be lexicographic hexadecimal)` throw new ValidationError(msg) } else { throw e } } if (intI !== expectedIntI) { const msg = `Non-consecutive indexes in ${subName}: expected ${expectedIntI} ` + `but observed ${intI} (lexicographically: ${key})` throw new ValidationError(msg) } expectedIntI = expectedIntI + 1 } if (expectedIntI === 0 && !canBeEmpty) { const msg = `Found no entries in sub ${subName}` throw new ValidationError(msg) } } async function ensureEachNonRootElemContainedInExactlyOneStructure ( subSpace, flattenedStructureValues, bee ) { const rootKey = utils.getAbsoluteKey(STRUCTURE, ROOT) for await (const { key } of bee.createReadStream({ gt: subSpace, lt: subSpace + '\x01' // The char just after the separator \x00 })) { if (key !== rootKey) { const firstIndex = flattenedStructureValues.indexOf(key) if (firstIndex < 0) { const msg = `The ${subSpace} at index '${key}' is not included in any structure` throw new ValidationError(msg) } const lastIndex = flattenedStructureValues.lastIndexOf(key) if (lastIndex !== firstIndex) { const msg = [ `The ${subSpace} at index '${key}' is included in multiple structures:`, `flattened indices ${firstIndex} and ${lastIndex}` ].join(' ') throw new ValidationError(msg) } } } } async function extractAndValidateStructureItems (bee) { const structure = bee.sub(STRUCTURE) const allStructureValues = [] let previousStructSubName = '' for await (const { key, value: referencedKeys } of structure.createReadStream()) { const currentStructSubName = utils.splitKeyInComponents(key)[0] if (currentStructSubName !== previousStructSubName) { // Only ROOT is not indexed if (currentStructSubName !== ROOT) { const structSub = structure.sub(currentStructSubName) await ensureSubIsIndexedConsecutively({ sub: structSub, subName: utils.getAbsoluteKey(STRUCTURE, currentStructSubName) }) } previousStructSubName = currentStructSubName } if (!Array.isArray(referencedKeys)) { const msg = `Structure '${key}' has no array as value` throw new ValidationError(msg) } if (referencedKeys.length === 0) { const msg = `Structure '${key}' contains no elements` throw new ValidationError(msg) } for (const referencedKey of referencedKeys) { const resolvedKey = await bee.get(referencedKey) if (resolvedKey === null) { const msg = `Structure '${key}' references a non-existing key: ${referencedKey}` throw new ValidationError(msg) } } allStructureValues.push(referencedKeys) } return allStructureValues } async function validateTitle (bee) { const structure = bee.sub(STRUCTURE) const { value: titleStructure } = (await structure.get(TITLE)) || { value: null } // Title is optional if (titleStructure) { if (titleStructure.length > 1) { throw new ValidationError( 'The title structure contains more than 1 content element' ) } } } async function validateContent (bee) { const content = bee.sub(CONTENT) if (content) { // Empty content is acceptable await ensureSubIsIndexedConsecutively({ sub: content, subName: CONTENT, canBeEmpty: true }) for await (const { key, value } of content.createReadStream()) { if (value.length === 0) { const msg = `Content at index ${key} is empty` throw new ValidationError(msg) } } } } async function validateEmbeddings (bee) { // This validates the embedding is formally correct, // but there are no semantic checks // (e.g. the bee which an embedding references might not exist, // or might not be a pubee) const embeddingSub = bee.sub(EMBEDDING) await ensureSubIsIndexedConsecutively({ sub: embeddingSub, subName: EMBEDDING, canBeEmpty: true }) for await (const { key, value: embedding } of embeddingSub.createReadStream()) { try { validateEmbedding(embedding) } catch (e) { if (e instanceof EmbeddingValidationError) { const msg = `Embedding at index '${key}': ${e.message}` throw new ValidationError(msg) } else { throw e } } } } async function validateLinks (bee) { // No semantic checks (e.g. the linked work might not exist or be invalid) const linkSub = bee.sub(LINK) await ensureSubIsIndexedConsecutively({ sub: linkSub, subName: LINK, canBeEmpty: true }) for await (const { key, value: link } of linkSub.createReadStream()) { try { validateLink(link) } catch (e) { if (e instanceof ValidationError) { const msg = `Link at index '${key}': ${e.message}` throw new ValidationError(msg) } else { throw e } } } } async function validateKeyNames (bee) { for await (const { key } of bee.createReadStream()) { const keyComponents = key.split(EMPTY) for (const component of keyComponents) { if (component.match(KEYNAME_REGEX) === null) { throw new Error( `Structure key must match ${KEYNAME_REGEX} regex ('${component}')` ) } } } } async function ensureIsValidPubee (bee) { if (!bee) { throw new ValidationError( 'A bee must be an object--received null or equivalent' ) } if (bee.valueEncoding.name !== 'json') { const msg = 'Invalid valueEncoding: must be json, ' + `but observed ${bee.valueEncoding.name}` throw new ValidationError(msg) } const structure = bee.sub(STRUCTURE) const root = await structure.get(ROOT) if (!root) { throw new ValidationError('A work must contain a root structure element') } const allStructureValues = await extractAndValidateStructureItems(bee) const validatorPromises = [] for (const key of [CONTENT, STRUCTURE, EMBEDDING, LINK]) { validatorPromises.push( ensureEachNonRootElemContainedInExactlyOneStructure( key, allStructureValues.flat(), bee ) ) } validatorPromises.push(validateTitle(bee)) validatorPromises.push(validateContent(bee)) validatorPromises.push(validateEmbeddings(bee)) validatorPromises.push(validateLinks(bee)) validatorPromises.push(validateKeyNames(bee)) await Promise.all(validatorPromises) const metadata = await bee.get(METADATA) if (!metadata) { throw new ValidationError('A pubee must have a metadata key') } return true } module.exports = { ensureIsValidPubee }