Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions controllers/crud.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
}
Expand All @@ -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"])
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions controllers/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions headers.js
Original file line number Diff line number Diff line change
@@ -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"] = "<http://www.w3.org/ns/ldp#Resource>; 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"] = '<http://store.rerum.io/v1/context.json>; 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
}
85 changes: 85 additions & 0 deletions predicates.js
Original file line number Diff line number Diff line change
@@ -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
}
Loading