diff --git a/controllers/bulk.js b/controllers/bulk.js index 9fb78e6..4b7eaa4 100644 --- a/controllers/bulk.js +++ b/controllers/bulk.js @@ -7,7 +7,8 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { configureRerumOptions } from '../versioning.js' +import { isDeleted } from '../predicates.js' import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' @@ -72,7 +73,7 @@ const bulkCreate = async function (req, res, next) { if(Object.keys(d).length === 0) continue const providedID = d?._id const id = isValidID(providedID) ? providedID : ObjectID() - d = utils.configureRerumOptions(generatorAgent, d) + d = configureRerumOptions(generatorAgent, d) // id is also protected in this case, so it can't be set. if(_contextid(d["@context"])) delete d.id d._id = id @@ -159,10 +160,10 @@ const bulkUpdate = async function (req, res, next) { return } if (null === originalObject) continue - if (utils.isDeleted(originalObject)) continue + if (isDeleted(originalObject)) continue id = ObjectID() let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {} - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } delete objectReceived["__rerum"] delete objectReceived["_id"] delete objectReceived["@id"] diff --git a/controllers/crud.js b/controllers/crud.js index 6bf7000..8d88ef4 100644 --- a/controllers/crud.js +++ b/controllers/crud.js @@ -5,7 +5,9 @@ * @author Claude Sonnet 4, cubap, thehabes */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +// helpers used by many controllers have been split into focused modules +import { configureWebAnnoHeadersFor, configureLDHeadersFor, configureLastModifiedHeader } from '../headers.js' +import { configureRerumOptions } from '../versioning.js' import config from '../config/index.js' import { _contextid, idNegotiation, generateSlugId, ObjectID, createExpressError, getAgentClaim, parseDocumentID } from './utils.js' @@ -31,7 +33,7 @@ const create = async function (req, res, next) { let generatorAgent = getAgentClaim(req, next) let context = req.body["@context"] ? { "@context": req.body["@context"] } : {} let provided = JSON.parse(JSON.stringify(req.body)) - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, provided, false, false)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, provided, false, false)["__rerum"] } if(slug){ rerumProp.__rerum.slug = slug } @@ -47,7 +49,7 @@ const create = async function (req, res, next) { console.log("CREATE") try { let result = await db.insertOne(newObject) - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) @@ -82,7 +84,7 @@ const query = async function (req, res, next) { try { let matches = await db.find(props).limit(limit).skip(skip).toArray() matches = matches.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(matches)) + res.set(configureLDHeadersFor(matches)) res.json(matches) } catch (error) { next(createExpressError(error)) @@ -100,11 +102,11 @@ const id = async function (req, res, next) { try { let match = await db.findOne({"$or": [{"_id": id}, {"__rerum.slug": id}]}) if (match) { - res.set(utils.configureWebAnnoHeadersFor(match)) + res.set(configureWebAnnoHeadersFor(match)) //Support built in browser caching res.set("Cache-Control", "max-age=86400, must-revalidate") //Support requests with 'If-Modified_Since' headers - res.set(utils.configureLastModifiedHeader(match)) + res.set(configureLastModifiedHeader(match)) // Include current version for optimistic locking const currentVersion = match.__rerum?.isOverwritten ?? "" res.set('Current-Overwritten-Version', currentVersion) diff --git a/controllers/delete.js b/controllers/delete.js index 2e9737b..bdbde7c 100644 --- a/controllers/delete.js +++ b/controllers/delete.js @@ -5,7 +5,7 @@ * @author Claude Sonnet 4, cubap, thehabes */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted, isReleased, isGenerator } from '../predicates.js' import config from '../config/index.js' import { createExpressError, getAgentClaim, parseDocumentID, getAllVersions, getAllDescendants } from './utils.js' @@ -40,19 +40,19 @@ const deleteObj = async function(req, res, next) { } if (null !== originalObject) { let safe_original = JSON.parse(JSON.stringify(originalObject)) - if (utils.isDeleted(safe_original)) { + if (isDeleted(safe_original)) { err = Object.assign(err, { message: `The object you are trying to delete is already deleted. ${err.message}`, status: 403 }) } - else if (utils.isReleased(safe_original)) { + else if (isReleased(safe_original)) { err = Object.assign(err, { message: `The object you are trying to delete is released. Fork to make changes. ${err.message}`, status: 403 }) } - else if (!utils.isGenerator(safe_original, agentRequestingDelete)) { + else if (!isGenerator(safe_original, agentRequestingDelete)) { err = Object.assign(err, { message: `You are not the generating agent for this object and so are not authorized to delete it. ${err.message}`, status: 401 diff --git a/controllers/gog.js b/controllers/gog.js index 895ccab..6a6cb11 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -7,7 +7,7 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { configureLDHeadersFor } from '../headers.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** @@ -133,7 +133,7 @@ const _gog_fragments_from_manuscript = async function (req, res, next) { // console.log(witnessFragments.length+" fragments found for this Manuscript") // const end = Date.now() // console.log(`Total Execution time: ${end - start} ms`) - res.set(utils.configureLDHeadersFor(witnessFragments)) + res.set(configureLDHeadersFor(witnessFragments)) res.json(witnessFragments) } catch (error) { @@ -295,7 +295,7 @@ const _gog_glosses_from_manuscript = async function (req, res, next) { // console.log(glosses.length+" Glosses found for this Manuscript") // const end = Date.now() // console.log(`Total Execution time: ${end - start} ms`) - res.set(utils.configureLDHeadersFor(glosses)) + res.set(configureLDHeadersFor(glosses)) res.json(glosses) } catch (error) { diff --git a/controllers/history.js b/controllers/history.js index 0ea13df..2591fb5 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -7,7 +7,7 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { configureLDHeadersFor } from '../headers.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, getAllVersions, getAllAncestors, getAllDescendants } from './utils.js' /** @@ -42,7 +42,7 @@ const since = async function (req, res, next) { let descendants = getAllDescendants(all, obj, []) descendants = descendants.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(descendants)) + res.set(configureLDHeadersFor(descendants)) res.json(descendants) } @@ -79,7 +79,7 @@ const history = async function (req, res, next) { let ancestors = getAllAncestors(all, obj, []) ancestors = ancestors.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(ancestors)) + res.set(configureLDHeadersFor(ancestors)) res.json(ancestors) } diff --git a/controllers/overwrite.js b/controllers/overwrite.js index 5371f58..dd30e41 100644 --- a/controllers/overwrite.js +++ b/controllers/overwrite.js @@ -7,7 +7,8 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted, isReleased, isGenerator } from '../predicates.js' +import { configureWebAnnoHeadersFor } from '../headers.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** @@ -38,19 +39,19 @@ const overwrite = async function (req, res, next) { status: 404 }) } - else if (utils.isDeleted(originalObject)) { + else if (isDeleted(originalObject)) { err = Object.assign(err, { message: `The object you are trying to overwrite is deleted. ${err.message}`, status: 403 }) } - else if (utils.isReleased(originalObject)) { + else if (isReleased(originalObject)) { err = Object.assign(err, { message: `The object you are trying to overwrite is released. Fork with /update to make changes. ${err.message}`, status: 403 }) } - else if (!utils.isGenerator(originalObject, agentRequestingOverwrite)) { + else if (!isGenerator(originalObject, agentRequestingOverwrite)) { err = Object.assign(err, { message: `You are not the generating agent for this object. You cannot overwrite it. Fork with /update to make changes. ${err.message}`, status: 401 @@ -93,7 +94,7 @@ const overwrite = async function (req, res, next) { } // Include current version in response headers for future optimistic locking res.set('Current-Overwritten-Version', rerumProp["__rerum"].isOverwritten) - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) diff --git a/controllers/patchSet.js b/controllers/patchSet.js index 1cf8234..c85128a 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -7,7 +7,9 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted } from '../predicates.js' +import { configureRerumOptions } from '../versioning.js' +import { configureWebAnnoHeadersFor } from '../headers.js' import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' @@ -44,7 +46,7 @@ const patchSet = async function (req, res, next) { status: 501 }) } - else if (utils.isDeleted(originalObject)) { + else if (isDeleted(originalObject)) { err = Object.assign(err, { message: `The object you are trying to update is deleted. ${err.message}`, status: 403 @@ -72,7 +74,7 @@ const patchSet = async function (req, res, next) { if (Object.keys(objectReceived).length === 0) { //Then you aren't actually changing anything...there are no new properties //Just hand back the object. The resulting of setting nothing is the object from the request body. - res.set(utils.configureWebAnnoHeadersFor(originalObject)) + res.set(configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) @@ -82,7 +84,7 @@ const patchSet = async function (req, res, next) { } const id = ObjectID() let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } delete patchedObject["__rerum"] delete patchedObject["_id"] delete patchedObject["@id"] @@ -92,7 +94,7 @@ const patchSet = async function (req, res, next) { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { //Success, the original object has been updated. - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index 28ba597..71dd372 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -7,7 +7,9 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted } from '../predicates.js' +import { configureRerumOptions } from '../versioning.js' +import { configureWebAnnoHeadersFor } from '../headers.js' import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' @@ -43,7 +45,7 @@ const patchUnset = async function (req, res, next) { status: 501 }) } - else if (utils.isDeleted(originalObject)) { + else if (isDeleted(originalObject)) { err = Object.assign(err, { message: `The object you are trying to update is deleted. ${err.message}`, status: 403 @@ -74,7 +76,7 @@ const patchUnset = async function (req, res, next) { if (Object.keys(objectReceived).length === 0) { //Then you aren't actually changing anything...no properties in the request body were removed from the original object. //Just hand back the object. The resulting of unsetting nothing is the object. - res.set(utils.configureWebAnnoHeadersFor(originalObject)) + res.set(configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) @@ -84,7 +86,7 @@ const patchUnset = async function (req, res, next) { } const id = ObjectID() let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } delete patchedObject["__rerum"] delete patchedObject["_id"] delete patchedObject["@id"] @@ -97,7 +99,7 @@ const patchUnset = async function (req, res, next) { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { //Success, the original object has been updated. - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index c0863e7..0387491 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -7,7 +7,9 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted } from '../predicates.js' +import { configureRerumOptions } from '../versioning.js' +import { configureWebAnnoHeadersFor } from '../headers.js' import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' @@ -42,7 +44,7 @@ const patchUpdate = async function (req, res, next) { status: 501 }) } - else if (utils.isDeleted(originalObject)) { + else if (isDeleted(originalObject)) { err = Object.assign(err, { message: `The object you are trying to update is deleted. ${err.message}`, status: 403 @@ -73,7 +75,7 @@ const patchUpdate = async function (req, res, next) { if (Object.keys(objectReceived).length === 0) { //Then you aren't actually changing anything...only @id came through //Just hand back the object. The resulting of patching nothing is the object unchanged. - res.set(utils.configureWebAnnoHeadersFor(originalObject)) + res.set(configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) @@ -83,7 +85,7 @@ const patchUpdate = async function (req, res, next) { } const id = ObjectID() let context = patchedObject["@context"] ? { "@context": patchedObject["@context"] } : {} - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } delete patchedObject["__rerum"] delete patchedObject["_id"] delete patchedObject["@id"] @@ -96,7 +98,7 @@ const patchUpdate = async function (req, res, next) { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { //Success, the original object has been updated. - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) diff --git a/controllers/putUpdate.js b/controllers/putUpdate.js index 3c42deb..e256de0 100644 --- a/controllers/putUpdate.js +++ b/controllers/putUpdate.js @@ -7,7 +7,9 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted } from '../predicates.js' +import { configureRerumOptions } from '../versioning.js' +import { configureWebAnnoHeadersFor } from '../headers.js' import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' @@ -46,7 +48,7 @@ const putUpdate = async function (req, res, next) { status: 404 }) } - else if (utils.isDeleted(originalObject)) { + else if (isDeleted(originalObject)) { err = Object.assign(err, { message: `The object you are trying to update is deleted. ${err.message}`, status: 403 @@ -55,7 +57,7 @@ const putUpdate = async function (req, res, next) { else { id = ObjectID() let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {} - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } delete objectReceived["__rerum"] delete objectReceived["_id"] delete objectReceived["@id"] @@ -69,7 +71,7 @@ const putUpdate = async function (req, res, next) { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { //Success, the original object has been updated. - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) @@ -114,7 +116,7 @@ async function _import(req, res, next) { let generatorAgent = getAgentClaim(req, next) const id = ObjectID() let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {} - let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, objectReceived, false, true)["__rerum"] } + let rerumProp = { "__rerum": configureRerumOptions(generatorAgent, objectReceived, false, true)["__rerum"] } delete objectReceived["__rerum"] delete objectReceived["_id"] delete objectReceived["@id"] @@ -126,7 +128,7 @@ async function _import(req, res, next) { console.log("IMPORT") try { let result = await db.insertOne(newObject) - res.set(utils.configureWebAnnoHeadersFor(newObject)) + res.set(configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) diff --git a/controllers/release.js b/controllers/release.js index 5d7f5f8..957081f 100644 --- a/controllers/release.js +++ b/controllers/release.js @@ -7,7 +7,8 @@ */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' +import { isDeleted, isReleased, isGenerator } from '../predicates.js' +import { configureWebAnnoHeadersFor } from '../headers.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, generateSlugId, establishReleasesTree, healReleasesTree } from './utils.js' /** @@ -49,19 +50,19 @@ const release = async function (req, res, next) { let previousReleasedID = safe_original.__rerum.releases.previous let nextReleases = safe_original.__rerum.releases.next - if (utils.isDeleted(safe_original)) { + if (isDeleted(safe_original)) { err = Object.assign(err, { message: `The object you are trying to release is deleted. ${err.message}`, status: 403 }) } - if (utils.isReleased(safe_original)) { + if (isReleased(safe_original)) { err = Object.assign(err, { message: `The object you are trying to release is already released. ${err.message}`, status: 403 }) } - if (!utils.isGenerator(safe_original, agentRequestingRelease)) { + if (!isGenerator(safe_original, agentRequestingRelease)) { err = Object.assign(err, { message: `You are not the generating agent for this object. You cannot release it. ${err.message}`, status: 401 @@ -109,7 +110,7 @@ const release = async function (req, res, next) { if (result.modifiedCount == 0) { //result didn't error out, the action was not performed. Sometimes, this is a neutral thing. Sometimes it is indicative of an error. } - res.set(utils.configureWebAnnoHeadersFor(releasedObject)) + res.set(configureWebAnnoHeadersFor(releasedObject)) console.log(releasedObject._id+" has been released") releasedObject = idNegotiation(releasedObject) releasedObject.new_obj_state = JSON.parse(JSON.stringify(releasedObject)) diff --git a/controllers/search.js b/controllers/search.js index dc5ec75..2b4d1fb 100644 --- a/controllers/search.js +++ b/controllers/search.js @@ -5,7 +5,7 @@ * @author thehabes */ import { db } from '../database/client.js' -import utils from '../utils.js' +import { configureLDHeadersFor } from '../headers.js' import { idNegotiation, createExpressError } from './utils.js' /** @@ -283,7 +283,7 @@ const searchAsWords = async function (req, res, next) { const merged = mergeSearchResults(resultsPresi3, resultsPresi2) let results = merged.slice(skip, skip + limit) results = results.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(results)) + res.set(configureLDHeadersFor(results)) res.json(results) } catch (error) { console.error(error) @@ -371,7 +371,7 @@ const searchAsPhrase = async function (req, res, next) { const merged = mergeSearchResults(resultsPresi3, resultsPresi2) let results = merged.slice(skip, skip + limit) results = results.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(results)) + res.set(configureLDHeadersFor(results)) res.json(results) } catch (error) { console.error(error) @@ -451,7 +451,7 @@ const searchFuzzily = async function (req, res, next) { const merged = mergeSearchResults(resultsPresi3, resultsPresi2) let results = merged.slice(skip, skip + limit) results = results.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(results)) + res.set(configureLDHeadersFor(results)) res.json(results) } catch (error) { console.error(error) @@ -548,7 +548,7 @@ const searchWildly = async function (req, res, next) { const merged = mergeSearchResults(resultsPresi3, resultsPresi2) let results = merged.slice(skip, skip + limit) results = results.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(results)) + res.set(configureLDHeadersFor(results)) res.json(results) } catch (error) { console.error(error) @@ -682,7 +682,7 @@ const searchAlikes = async function (req, res, next) { // Apply pagination after merging let results = merged.slice(skip, skip + limit) results = results.map(o => idNegotiation(o)) - res.set(utils.configureLDHeadersFor(results)) + res.set(configureLDHeadersFor(results)) res.json(results) } catch (error) { console.error(error) diff --git a/controllers/utils.js b/controllers/utils.js index 5d383de..c643abd 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -5,7 +5,6 @@ * @author Claude Sonnet 4, cubap, thehabes */ import { newID, isValidID, db } from '../database/client.js' -import utils from '../utils.js' import config from '../config/index.js' const ObjectID = newID diff --git a/headers.js b/headers.js new file mode 100644 index 0000000..e96fbe2 --- /dev/null +++ b/headers.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +import utilsPred from './predicates.js' + +/** + * Mint the HTTP response headers required by REST best practices and/or Web Annotation standards. + * return a JSON object. keys are header names, values are header values. + */ +const configureWebAnnoHeadersFor = function(obj){ + let headers = {} + if(utilsPred.isLD(obj)){ + headers["Content-Type"] = "application/ld+json;charset=utf-8;profile=\"http://www.w3.org/ns/anno.jsonld\"" + } + if(utilsPred.isContainerType(obj)){ + headers["Link"] = "application/ld+json;charset=utf-8;profile=\"http://www.w3.org/ns/anno.jsonld\"" + } + else{ + headers["Link"] = "; rel=\"type\"" + } + headers["Allow"] = "GET,OPTIONS,HEAD,PUT,PATCH,DELETE,POST" + return headers +} + +/** + * Mint the HTTP response headers required by REST best practices and/or Linked Data standards. + * This is specifically for responses that are not Web Annotation compliant (getByProperties, getAllDescendants, getAllAncestors) + * They respond with Arrays (which have no @context), but they still need the JSON-LD support headers. + * return a JSON object. keys are header names, values are header values. + */ +const configureLDHeadersFor = function(obj){ + //Note that the optimal situation would be to be able to detect the LD-ness of this object + //What we have are the arrays returned from the aformentioned getters (/query, /since, /history) + //We know we want them to be LD and that they likely contain LD things, but the arrays don't have an @context + let headers = {} + /** + if(isLD(obj)){ + headers["Content-Type"] = 'application/ld+json;charset=utf-8;profile="http://www.w3.org/ns/anno.jsonld"' + } + else { + // This breaks Web Annotation compliance, but allows us to return requested + // objects without misrepresenting the content. + headers["Content-Type"] = "application/json;charset=utf-8;" + } + */ + headers["Allow"] = "GET,OPTIONS,HEAD,PUT,PATCH,DELETE,POST" + headers["Content-Type"] = 'application/ld+json;charset=utf-8;profile="http://www.w3.org/ns/anno.jsonld"' + headers["Link"] = '; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' + return headers +} + +/** + * Mint the Last-Modified header for /v1/id/ responses. + * It should be displayed like Mon, 14 Mar 2022 22:44:42 GMT + * The data knows it like 2022-03-14T17:44:42.721 + * return a JSON object. keys are header names, values are header values. + */ +const configureLastModifiedHeader = function(obj){ + let date = "" + if(obj.__rerum){ + if(obj.__rerum.isOverwritten !== ""){ + date = obj.__rerum.isOverwritten + } + else{ + date = obj.__rerum.createdAt + } + } + else if(obj.__deleted){ + date = obj.__deleted.time + } + //Note that dates like 2021-05-26T10:39:19.328 have been rounded to 2021-05-26T10:39:19 in browser headers. Account for that here. + if(typeof date === "string" && date.includes(".")){ + //If-Modified-Since and Last-Modified headers are rounded. Wed, 26 May 2021 10:39:19.629 GMT becomes Wed, 26 May 2021 10:39:19 GMT. + date = date.split(".")[0] + } + return {"Last-Modified":new Date(date).toUTCString()} +} + +export { + configureWebAnnoHeadersFor, + configureLDHeadersFor, + configureLastModifiedHeader +} + +export default { + configureWebAnnoHeadersFor, + configureLDHeadersFor, + configureLastModifiedHeader +} diff --git a/predicates.js b/predicates.js new file mode 100644 index 0000000..f3ad58c --- /dev/null +++ b/predicates.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +/** + * Check this object for deleted status. deleted objects in RERUM look like {"@id":"{some-id}", __deleted:{object properties}} + */ +const isDeleted = function(obj){ + return obj.hasOwnProperty("__deleted") +} + +/** + * Check this object for released status. Released objects in RERUM look like {"@id":"{some-id}", __rerum:{"isReleased" : "ISO-DATE-TIME"}} + */ +const isReleased = function(obj){ + let bool = + (obj.hasOwnProperty("__rerum") && + obj.__rerum.hasOwnProperty("isReleased") && + obj.__rerum.isReleased !== "") + return bool +} + +/** + * Check to see if the agent from the request (req.user had decoded token) matches the generating agent of the object in mongodb. + */ +const isGenerator = function(origObj, changeAgent){ + //If the object in mongo does not have a generator, something wrong. however, there is no permission to check, no generator is the same as any generator. + const generatingAgent = origObj.__rerum.generatedBy ?? changeAgent + //bots get a free pass through + return generatingAgent === changeAgent +} + +/** + * Check if this object is of a known container type. + * If so, it requires a different header than a stand-alone resource object. + * return boolean + */ +const isContainerType = function(obj){ + let answer = false + let typestring = obj["@type"] ?? obj.type ?? "" + const knownContainerTypes = [ + "ItemList", + "AnnotationPage", + "AnnotationList", + "AnnotationCollection", + "Sequence", + "Range", + "Canvas", + "List", + "Set", + "Collection" + ] + for(const t of knownContainerTypes){ + //Dang those pesky prefixes...circumventing exact match for now + if(typestring.includes(t)){ + answer = true + break + } + } + return answer +} + +/** + * Check if this object is a Linked Data object. + * If so, it will have an @context -(TODO) that resolves! + * return boolean + */ +const isLD = function(obj){ + //Note this is always false if obj is an array, like /since, /history or /query provide as a return. + return Array.isArray(obj) ? false : obj["@context"] ? true : false +} + +export { + isDeleted, + isReleased, + isGenerator, + isContainerType, + isLD +} + +export default { + isDeleted, + isReleased, + isGenerator, + isContainerType, + isLD +} diff --git a/utils.js b/utils.js index a20121c..8b09eb1 100644 --- a/utils.js +++ b/utils.js @@ -1,253 +1,25 @@ #!/usr/bin/env node -/** - * This module is general utilities. It should not respond to clients or manipulate the - * http request/response. - * - * @author thehabes - */ -import config from './config/index.js' +// The original utils.js was becoming overly large. Many of its helper functions +// have been split into focused modules in the project root. This file now +// simply re‑exports them so existing imports (`import utils from '../utils.js'`) +// continue to work. -/** - * Add the __rerum properties object to a given JSONObject.If __rerum already exists, it will be overwritten because this method is only called on new objects. Properties for consideration are: -APIversion —1.1.0 -history.prime —if it has an @id, import from that, else "root" -history.next —always [] -history.previous —if it has an @id, @id -releases.previous —if it has an @id, import from that, else "" -releases.next —always [] -releases.replaces —always "" -generatedBy —set to the @id of the public agent of the API Key. -createdAt —DateTime of right now. -isOverwritten —always "" -isReleased —always "" - * - * @param received A potentially optionless JSONObject from the Mongo Database (not the user). This prevents tainted __rerum's - * @param update A trigger for special handling from update actions - * @return configuredObject The same object that was recieved but with the proper __rerum options. This object is intended to be saved as a new object (@see versioning) - */ -const configureRerumOptions = function(generator, received, update, extUpdate){ - let configuredObject = JSON.parse(JSON.stringify(received)) - let received_options = received.__rerum ? JSON.parse(JSON.stringify(received.__rerum)) : {} - let history = {} - let releases = {} - let rerumOptions = {} - let history_prime = "" - let history_previous = "" - let releases_previous = "" - if(extUpdate){ - //We are "importing" an external object as a new object in RERUM (via an update). It can knows its previous external self, but is a root for its existence in RERUM. - received_options = {} - history_prime = "root" - history_previous = received["@id"] ?? received.id ?? "" - } - else{ - //We are either updating an existing RERUM object or creating a new one. - if(received_options.hasOwnProperty("history")){ - history = received_options.history - if(update){ - //This means we are configuring from the update action and we have passed in a clone of the originating object (with its @id) that contained a __rerum.history - if(history.prime === "root"){ - //Hitting this case means we are updating from the prime object, so we can't pass "root" on as the prime value - history_prime = received["@id"] ?? received.id ?? "" - } - else{ - //Hitting this means we are updating an object that already knows its prime, so we can pass on the prime value - history_prime = history.prime - } - //Either way, we know the previous value shold be the @id of the object received here. - history_previous = received["@id"] ?? received.id ?? "" - } - else{ - //Hitting this means we are saving a new object and found that __rerum.history existed. We don't trust it, act like it doesn't have it. - history_prime = "root" - history_previous = "" - } - } - else{ - //Hitting this means we are are saving an object that did not have __rerum history. This is normal - history_prime = "root" - history_previous = "" - } - if(received_options.hasOwnProperty("releases")){ - releases = received_options.releases - releases_previous = releases.previous - } - else{ - releases_previous = "" - } - } - releases.next = [] - releases.previous = releases_previous - releases.replaces = "" - history.next = [] - history.previous = history_previous - history.prime = history_prime - rerumOptions["@context"] = config.RERUM_CONTEXT - rerumOptions.alpha = true - rerumOptions.APIversion = config.RERUM_API_VERSION - //It is important for the cache workflow that these be properly formatted. - let creationDateTime = new Date(Date.now()).toISOString().replace("Z", "") - rerumOptions.createdAt = creationDateTime - rerumOptions.isOverwritten = "" - rerumOptions.isReleased = "" - rerumOptions.history = history - rerumOptions.releases = releases - rerumOptions.generatedBy = generator - configuredObject.__rerum = rerumOptions - return configuredObject //The mongo save/update has not been called yet. The object returned here will go into mongo.save or mongo.update -} - -/** - * Check this object for deleted status. deleted objects in RERUM look like {"@id":"{some-id}", __deleted:{object properties}} - */ -const isDeleted = function(obj){ - return obj.hasOwnProperty("__deleted") -} - -/** - * Check this object for released status. Released objects in RERUM look like {"@id":"{some-id}", __rerum:{"isReleased" : "ISO-DATE-TIME"}} - */ -const isReleased = function(obj){ - let bool = - (obj.hasOwnProperty("__rerum") && - obj.__rerum.hasOwnProperty("isReleased") && - obj.__rerum.isReleased !== "") - return bool -} - -/** - * Check to see if the agent from the request (req.user had decoded token) matches the generating agent of the object in mongodb. - */ -const isGenerator = function(origObj, changeAgent){ - //If the object in mongo does not have a generator, something wrong. however, there is no permission to check, no generator is the same as any generator. - const generatingAgent = origObj.__rerum.generatedBy ?? changeAgent - //bots get a free pass through - return generatingAgent === changeAgent -} +import versioning from './versioning.js' +import headers from './headers.js' +import predicates from './predicates.js' -/** - * Mint the HTTP response headers required by REST best practices and/or Web Annotation standards. - * return a JSON object. keys are header names, values are header values. - */ -const configureWebAnnoHeadersFor = function(obj){ - let headers = {} - if(isLD(obj)){ - headers["Content-Type"] = "application/ld+json;charset=utf-8;profile=\"http://www.w3.org/ns/anno.jsonld\"" - } - if(isContainerType(obj)){ - headers["Link"] = "application/ld+json;charset=utf-8;profile=\"http://www.w3.org/ns/anno.jsonld\"" - } - else{ - headers["Link"] = "; rel=\"type\"" - } - headers["Allow"] = "GET,OPTIONS,HEAD,PUT,PATCH,DELETE,POST" - return headers -} - -/** - * Mint the HTTP response headers required by REST best practices and/or Linked Data standards. - * This is specifically for responses that are not Web Annotation compliant (getByProperties, getAllDescendants, getAllAncestors) - * They respond with Arrays (which have no @context), but they still need the JSON-LD support headers. - * return a JSON object. keys are header names, values are header values. - */ -const configureLDHeadersFor = function(obj){ - //Note that the optimal situation would be to be able to detect the LD-ness of this object - //What we have are the arrays returned from the aformentioned getters (/query, /since, /history) - //We know we want them to be LD and that they likely contain LD things, but the arrays don't have an @context - let headers = {} - /** - if(isLD(obj)){ - headers["Content-Type"] = 'application/ld+json;charset=utf-8;profile="http://www.w3.org/ns/anno.jsonld"' - } - else { - // This breaks Web Annotation compliance, but allows us to return requested - // objects without misrepresenting the content. - headers["Content-Type"] = "application/json;charset=utf-8;" - } - */ - headers["Allow"] = "GET,OPTIONS,HEAD,PUT,PATCH,DELETE,POST" - headers["Content-Type"] = 'application/ld+json;charset=utf-8;profile="http://www.w3.org/ns/anno.jsonld"' - headers["Link"] = '; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' - return headers -} - -/** - * Check if this object is of a known container type. - * If so, it requires a different header than a stand-alone resource object. - * return boolean - */ -const isContainerType = function(obj){ - let answer = false - let typestring = obj["@type"] ?? obj.type ?? "" - const knownContainerTypes = [ - "ItemList", - "AnnotationPage", - "AnnotationList", - "AnnotationCollection", - "Sequence", - "Range", - "Canvas", - "List", - "Set", - "Collection" - ] - for(const t of knownContainerTypes){ - //Dang those pesky prefixes...circumventing exact match for now - if(typestring.includes(t)){ - answer = true - break - } - } - return answer - //return knownContainerTypes.includes(typestring) -} +export default { + // versioning helpers + ...versioning, -/** - * Check if this object is a Linked Data object. - * If so, it will have an @context -(TODO) that resolves! - * return boolean - */ -const isLD = function(obj){ - //Note this is always false if obj is an array, like /since, /history or /query provide as a return. - return Array.isArray(obj) ? false : obj["@context"] ? true : false -} + // predicates and checks + ...predicates, -/** - * Mint the Last-Modified header for /v1/id/ responses. - * It should be displayed like Mon, 14 Mar 2022 22:44:42 GMT - * The data knows it like 2022-03-14T17:44:42.721 - * return a JSON object. keys are header names, values are header values. - */ -const configureLastModifiedHeader = function(obj){ - let date = "" - if(obj.__rerum){ - if(!obj.__rerum.isOverwritten === ""){ - date = obj.__rerum.isOverwritten - } - else{ - date = obj.__rerum.createdAt - } - } - else if(obj.__deleted){ - date = obj.__deleted.time - } - //Note that dates like 2021-05-26T10:39:19.328 have been rounded to 2021-05-26T10:39:19 in browser headers. Account for that here. - if(typeof date === "string" && date.includes(".")){ - //If-Modified-Since and Last-Modified headers are rounded. Wed, 26 May 2021 10:39:19.629 GMT becomes Wed, 26 May 2021 10:39:19 GMT. - date = date.split(".")[0] - } - return {"Last-Modified":new Date(date).toUTCString()} + // header constructors + ...headers } -export default { - configureRerumOptions, - isDeleted, - isReleased, - isGenerator, - configureWebAnnoHeadersFor, - configureLDHeadersFor, - isContainerType, - isLD, - configureLastModifiedHeader -} \ No newline at end of file +export { configureRerumOptions } from './versioning.js' +export { configureWebAnnoHeadersFor, configureLDHeadersFor, configureLastModifiedHeader } from './headers.js' +export { isDeleted, isReleased, isGenerator, isContainerType, isLD } from './predicates.js' diff --git a/versioning.js b/versioning.js new file mode 100644 index 0000000..06dbed2 --- /dev/null +++ b/versioning.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +import config from './config/index.js' + +/** + * Add the __rerum properties object to a given JSONObject. If __rerum already exists, it will be overwritten + * because this method is only called on new objects. Properties for consideration are: + * APIversion —1.1.0 + * history.prime —if it has an @id, import from that, else "root" + * history.next —always [] + * history.previous —if it has an @id, @id + * releases.previous —if it has an @id, import from that, else "" + * releases.next —always [] + * releases.replaces —always "" + * generatedBy —set to the @id of the public agent of the API Key. + * createdAt —DateTime of right now. + * isOverwritten —always "" + * isReleased —always "" + * + * @param received A potentially optionless JSONObject from the Mongo Database (not the user). This prevents tainted __rerum's + * @param update A trigger for special handling from update actions + * @return configuredObject The same object that was recieved but with the proper __rerum options. This object is intended to be saved as a new object (@see versioning) + */ +const configureRerumOptions = function(generator, received, update, extUpdate){ + let configuredObject = JSON.parse(JSON.stringify(received)) + let received_options = received.__rerum ? JSON.parse(JSON.stringify(received.__rerum)) : {} + let history = {} + let releases = {} + let rerumOptions = {} + let history_prime = "" + let history_previous = "" + let releases_previous = "" + if(extUpdate){ + //We are "importing" an external object as a new object in RERUM (via an update). It can knows its previous external self, but is a root for its existence in RERUM. + received_options = {} + history_prime = "root" + history_previous = received["@id"] ?? received.id ?? "" + } + else{ + //We are either updating an existing RERUM object or creating a new one. + if(received_options.hasOwnProperty("history")){ + history = received_options.history + if(update){ + //This means we are configuring from the update action and we have passed in a clone of the originating object (with its @id) that contained a __rerum.history + if(history.prime === "root"){ + //Hitting this case means we are updating from the prime object, so we can't pass "root" on as the prime value + history_prime = received["@id"] ?? received.id ?? "" + } + else{ + //Hitting this means we are updating an object that already knows its prime, so we can pass on the prime value + history_prime = history.prime + } + //Either way, we know the previous value shold be the @id of the object received here. + history_previous = received["@id"] ?? received.id ?? "" + } + else{ + //Hitting this means we are saving a new object and found that __rerum.history existed. We don't trust it, act like it doesn't have it. + history_prime = "root" + history_previous = "" + } + } + else{ + //Hitting this means we are are saving an object that did not have __rerum history. This is normal + history_prime = "root" + history_previous = "" + } + if(received_options.hasOwnProperty("releases")){ + releases = received_options.releases + releases_previous = releases.previous + } + else{ + releases_previous = "" + } + } + releases.next = [] + releases.previous = releases_previous + releases.replaces = "" + history.next = [] + history.previous = history_previous + history.prime = history_prime + rerumOptions["@context"] = config.RERUM_CONTEXT + rerumOptions.alpha = true + rerumOptions.APIversion = config.RERUM_API_VERSION + //It is important for the cache workflow that these be properly formatted. + let creationDateTime = new Date(Date.now()).toISOString().replace("Z", "") + rerumOptions.createdAt = creationDateTime + rerumOptions.isOverwritten = "" + rerumOptions.isReleased = "" + rerumOptions.history = history + rerumOptions.releases = releases + rerumOptions.generatedBy = generator + configuredObject.__rerum = rerumOptions + return configuredObject //The mongo save/update has not been called yet. The object returned here will go into mongo.save or mongo.update +} + +export { + configureRerumOptions +} + +export default { + configureRerumOptions +}