hyperpubee
Version:
Self-publishing over the decentralised internet
300 lines (254 loc) • 7.5 kB
JavaScript
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
}