diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 088e6175..20bcbd5d 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -120,6 +120,19 @@ describe('Check to see that all /v1/api/ route patterns exist.', () => { expect(exists).toBe(true) }) + it('/v1/api/bulkUpdate -- mounted ', () => { + let exists = false + for (const middleware of api_stack) { + if (middleware.regexp + && middleware.regexp.toString().includes("/api") + && middleware.regexp.toString().includes("/bulkUpdate")){ + exists = true + break + } + } + expect(exists).toBe(true) + }) + it('/v1/api/patch -- mounted ', () => { let exists = false for (const middleware of api_stack) { diff --git a/database/index.js b/database/index.js index 282bc473..cf8d374a 100644 --- a/database/index.js +++ b/database/index.js @@ -4,6 +4,7 @@ dotenv.config() const client = new MongoClient(process.env.MONGO_CONNECTION_STRING) const newID = () => new ObjectId().toHexString() +const isValidID = (id) => ObjectId.isValid(id) const connected = async function () { // Send a ping to confirm a successful connection await client.db("admin").command({ ping: 1 }).catch(err => err) @@ -50,6 +51,7 @@ function isValidURL(url) { export { newID, + isValidID, connected, db } diff --git a/db-controller.js b/db-controller.js index 8ced4977..62b63685 100644 --- a/db-controller.js +++ b/db-controller.js @@ -9,7 +9,7 @@ * * @author thehabes */ -import { newID, db } from './database/index.js' +import { newID, isValidID, db } from './database/index.js' import utils from './utils.js' const ObjectID = newID @@ -21,6 +21,59 @@ const index = function (req, res, next) { }) } +/** + * Check if a @context value contains a known @id-id mapping context + * + * @param contextInput An Array of string URIs or a string URI. + * @return A boolean + */ +function _contextid(contextInput) { + if(!Array.isArray(contextInput) && typeof contextInput !== "string") return false + let bool = false + let contextURI = typeof contextInput === "string" ? contextInput : "unknown" + const contextCheck = (c) => contextURI.includes(c) + const knownContexts = [ + "store.rerum.io/v1/context.json", + "iiif.io/api/presentation/3/context.json", + "www.w3.org/ns/anno.jsonld", + "www.w3.org/ns/oa.jsonld" + ] + if(Array.isArray(contextInput)) { + for(const c of contextInput) { + contextURI = c + bool = knownContexts.some(contextCheck) + if(bool) break + } + } + else { + bool = knownContexts.some(contextCheck) + } + return bool +} + +/** + * Modify the JSON of an Express response body by performing _id, id, and @id negotiation. + * This ensures the JSON has the appropriate _id, id, and/or @id value on the way out to the client. + * Make sure the first property is @context and the second property is the negotiated @id/id. + * + * @param resBody A JSON object representing an Express response body + * @return JSON with the appropriate modifications around the 'id;, '@id', and '_id' properties. + */ +const idNegotiation = function (resBody) { + if(!resBody) return + const _id = resBody._id + delete resBody._id + if(!resBody["@context"]) return resBody + let modifiedResBody = JSON.parse(JSON.stringify(resBody)) + const context = { "@context": resBody["@context"] } + if(_contextid(resBody["@context"])) { + delete resBody["@id"] + delete resBody["@context"] + modifiedResBody = Object.assign(context, { "id": process.env.RERUM_ID_PREFIX + _id }, resBody) + } + return modifiedResBody +} + /** * Check if an object with the proposed custom _id already exists. * If so, this is a 409 conflict. It will be detected downstream if we continue one by returning the proposed Slug. @@ -69,17 +122,20 @@ const create = async function (req, res, next) { slug = slug_json.slug_id } } - const id = ObjectID() + 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"] } rerumProp.__rerum.slug = slug - delete provided["_rerum"] - delete provided["_id"] + const providedID = provided._id + const id = isValidID(providedID) ? providedID : ObjectID() + delete provided["__rerum"] delete provided["@id"] - delete provided["id"] + // id is also protected in this case, so it can't be set. + if(_contextid(provided["@context"])) delete provided.id delete provided["@context"] + let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, provided, rerumProp, { "_id": id }) console.log("CREATE") try { @@ -87,7 +143,7 @@ const create = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) res.status(201) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) } @@ -235,10 +291,13 @@ const putUpdate = async function (req, res, next) { id = ObjectID() let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {} let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } - delete objectReceived["_rerum"] + delete objectReceived["__rerum"] delete objectReceived["_id"] delete objectReceived["@id"] + // id is also protected in this case, so it can't be set. + if(_contextid(objectReceived["@context"])) delete objectReceived.id delete objectReceived["@context"] + let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) console.log("UPDATE") try { @@ -248,7 +307,7 @@ const putUpdate = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) res.status(200) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) return @@ -291,11 +350,13 @@ async function _import(req, res, next) { const id = ObjectID() let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {} let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, objectReceived, false, true)["__rerum"] } - delete objectReceived["_rerum"] + delete objectReceived["__rerum"] delete objectReceived["_id"] delete objectReceived["@id"] - delete objectReceived["id"] + // id is also protected in this case, so it can't be set. + if(_contextid(objectReceived["@context"])) delete objectReceived.id delete objectReceived["@context"] + let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) console.log("IMPORT") try { @@ -303,7 +364,7 @@ async function _import(req, res, next) { res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) res.status(200) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) } @@ -327,8 +388,9 @@ const patchUpdate = async function (req, res, next) { let objectReceived = JSON.parse(JSON.stringify(req.body)) let patchedObject = {} let generatorAgent = getAgentClaim(req, next) - if (objectReceived["@id"]) { - let id = parseDocumentID(objectReceived["@id"]) + const receivedID = objectReceived["@id"] ?? objectReceived.id + if (receivedID) { + let id = parseDocumentID(receivedID) let originalObject try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) @@ -355,6 +417,8 @@ const patchUpdate = async function (req, res, next) { delete objectReceived.__rerum //can't patch this delete objectReceived._id //can't patch this delete objectReceived["@id"] //can't patch this + // id is also protected in this case, so it can't be set. + if(_contextid(objectReceived["@context"])) delete objectReceived.id //A patch only alters existing keys. Remove non-existent keys from the object received in the request body. for (let k in objectReceived) { if (originalObject.hasOwnProperty(k)) { @@ -376,7 +440,7 @@ const patchUpdate = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(originalObject)) res.location(originalObject["@id"]) res.status(200) - delete originalObject._id + originalObject = idNegotiation(originalObject) originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) res.json(originalObject) return @@ -384,9 +448,11 @@ 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"] } - delete patchedObject["_rerum"] + delete patchedObject["__rerum"] delete patchedObject["_id"] delete patchedObject["@id"] + // id is also protected in this case, so it can't be set. + if(_contextid(patchedObject["@context"])) delete patchedObject.id delete patchedObject["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) console.log("PATCH UPDATE") @@ -397,7 +463,7 @@ const patchUpdate = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) res.status(200) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) return @@ -417,7 +483,7 @@ const patchUpdate = async function (req, res, next) { else { //The http module will not detect this as a 400 on its own err = Object.assign(err, { - message: `Object in request body must have the property '@id'. ${err.message}`, + message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, status: 400 }) } @@ -436,10 +502,12 @@ const patchSet = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") let objectReceived = JSON.parse(JSON.stringify(req.body)) + let originalContext let patchedObject = {} let generatorAgent = getAgentClaim(req, next) - if (objectReceived["@id"]) { - let id = parseDocumentID(objectReceived["@id"]) + const receivedID = objectReceived["@id"] ?? objectReceived.id + if (receivedID) { + let id = parseDocumentID(receivedID) let originalObject try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) @@ -463,7 +531,14 @@ const patchSet = async function (req, res, next) { } else { patchedObject = JSON.parse(JSON.stringify(originalObject)) + if(_contextid(originalObject["@context"])) { + // If the original object has a context that needs id protected, make sure you don't set it. + delete objectReceived.id + delete originalObject.id + delete patchedObject.id + } //A set only adds new keys. If the original object had the key, it is ignored here. + delete objectReceived._id for (let k in objectReceived) { if (originalObject.hasOwnProperty(k)) { //Note the possibility of notifying the user that these keys were not processed. @@ -479,7 +554,7 @@ const patchSet = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(originalObject)) res.location(originalObject["@id"]) res.status(200) - delete originalObject._id + originalObject = idNegotiation(originalObject) originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) res.json(originalObject) return @@ -487,7 +562,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"] } - delete patchedObject["_rerum"] + delete patchedObject["__rerum"] delete patchedObject["_id"] delete patchedObject["@id"] delete patchedObject["@context"] @@ -499,7 +574,7 @@ const patchSet = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) res.status(200) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) return @@ -519,7 +594,7 @@ const patchSet = async function (req, res, next) { else { //The http module will not detect this as a 400 on its own err = Object.assign(err, { - message: `Object in request body must have the property '@id'. ${err.message}`, + message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, status: 400 }) } @@ -540,8 +615,9 @@ const patchUnset = async function (req, res, next) { let objectReceived = JSON.parse(JSON.stringify(req.body)) let patchedObject = {} let generatorAgent = getAgentClaim(req, next) - if (objectReceived["@id"]) { - let id = parseDocumentID(objectReceived["@id"]) + const receivedID = objectReceived["@id"] ?? objectReceived.id + if (receivedID) { + let id = parseDocumentID(receivedID) let originalObject try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) @@ -568,6 +644,9 @@ const patchUnset = async function (req, res, next) { delete objectReceived._id //can't unset this delete objectReceived.__rerum //can't unset this delete objectReceived["@id"] //can't unset this + // id is also protected in this case, so it can't be unset. + if(_contextid(originalObject["@context"])) delete objectReceived.id + /** * unset does not alter an existing key. It removes an existing key. * The request payload had {key:null} to flag keys to be removed. @@ -588,7 +667,7 @@ const patchUnset = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(originalObject)) res.location(originalObject["@id"]) res.status(200) - delete originalObject._id + originalObject = idNegotiation(originalObject) originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) res.json(originalObject) return @@ -596,9 +675,11 @@ 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"] } - delete patchedObject["_rerum"] + delete patchedObject["__rerum"] delete patchedObject["_id"] delete patchedObject["@id"] + // id is also protected in this case, so it can't be set. + if(_contextid(patchedObject["@context"])) delete patchedObject.id delete patchedObject["@context"] let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) console.log("PATCH UNSET") @@ -609,7 +690,7 @@ const patchUnset = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) res.status(200) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) return @@ -629,7 +710,7 @@ const patchUnset = async function (req, res, next) { else { //The http module will not detect this as a 400 on its own err = Object.assign(err, { - message: `Object in request body must have the property '@id'. ${err.message}`, + message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, status: 400 }) } @@ -647,9 +728,10 @@ const overwrite = async function (req, res, next) { res.set("Content-Type", "application/json; charset=utf-8") let objectReceived = JSON.parse(JSON.stringify(req.body)) let agentRequestingOverwrite = getAgentClaim(req, next) - if (objectReceived["@id"]) { + const receivedID = objectReceived["@id"] ?? objectReceived.id + if (receivedID) { console.log("OVERWRITE") - let id = parseDocumentID(objectReceived["@id"]) + let id = parseDocumentID(receivedID) let originalObject try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) @@ -688,9 +770,11 @@ const overwrite = async function (req, res, next) { const id = originalObject["_id"] //Get rid of them so we can enforce the order delete objectReceived["@id"] - delete objectReceived["@context"] delete objectReceived["_id"] delete objectReceived["__rerum"] + // id is also protected in this case, so it can't be set. + if(_contextid(objectReceived["@context"])) delete objectReceived.id + delete objectReceived["@context"] let newObject = Object.assign(context, { "@id": originalObject["@id"] }, objectReceived, rerumProp, { "_id": id }) let result try { @@ -703,7 +787,7 @@ const overwrite = async function (req, res, next) { } res.set(utils.configureWebAnnoHeadersFor(newObject)) res.location(newObject["@id"]) - delete newObject._id + newObject = idNegotiation(newObject) newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) res.json(newObject) return @@ -712,7 +796,7 @@ const overwrite = async function (req, res, next) { else { //This is a custom one, the http module will not detect this as a 400 on its own err = Object.assign(err, { - message: `Object in request body must have the property '@id'. ${err.message}`, + message: `Object in request body must have the property '@id' or 'id'. ${err.message}`, status: 400 }) } @@ -819,7 +903,7 @@ const release = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(releasedObject)) res.location(releasedObject["@id"]) console.log(releasedObject._id+" has been released") - delete releasedObject._id + releasedObject = idNegotiation(releasedObject) releasedObject.new_obj_state = JSON.parse(JSON.stringify(releasedObject)) res.json(releasedObject) return @@ -858,11 +942,7 @@ const query = async function (req, res, next) { } try { let matches = await db.find(props).limit(limit).skip(skip).toArray() - matches = - matches.map(o => { - delete o._id - return o - }) + matches = matches.map(o => idNegotiation(o)) res.set(utils.configureLDHeadersFor(matches)) res.json(matches) } catch (error) { @@ -887,43 +967,60 @@ const id = async function (req, res, next) { //Support requests with 'If-Modified_Since' headers res.set(utils.configureLastModifiedHeader(match)) res.location(match["@id"]) - delete match._id + match = idNegotiation(match) res.json(match) return } - let err = new Error(`No RERUM object with id '${id}'`) - err.status = 404 - throw err + let err = { + "message": `No RERUM object with id '${id}'`, + "status": 404 + } + next(createExpressError(err)) } catch (error) { next(createExpressError(error)) } } +/** + * Create many objects at once with the power of MongoDB bulkWrite() operations. + * + * @see https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/ + */ const bulkCreate = async function (req, res, next) { res.set("Content-Type", "application/json; charset=utf-8") const documents = req.body - // TODO: validate documents gatekeeper function? + let err = {} if (!Array.isArray(documents)) { - let err = new Error("The request body must be an array of objects.") - //err.status = 406 + err.message = "The request body must be an array of objects." err.status = 400 - next(err) + next(createExpressError(err)) return } if (documents.length === 0) { - let err = new Error("No action on an empty array.") - //err.status = 406 + err.message = "No action on an empty array." err.status = 400 - next(err) + next(createExpressError(err)) return } - if (documents.filter(d=>d["@id"] ?? d.id).length > 0) { - let err = new Error("`/bulkCreate` will only accept objects without @id or id properties.") - //err.status = 422 + const gatekeep = documents.filter(d=> { + // Each item must be valid JSON, but can't be an array. + if(Array.isArray(d) || typeof d !== "object") return d + try { + JSON.parse(JSON.stringify(d)) + } catch (err) { + return d + } + // Items must not have an @id, and in some cases same for id. + const idcheck = _contextid(d["@context"]) ? (d.id ?? d["@id"]) : d["@id"] + if(idcheck) return d + }) + if (gatekeep.length > 0) { + err.message = "All objects in the body of a `/bulkCreate` must be JSON and must not contain a declared identifier property." err.status = 400 - next(err) + next(createExpressError(err)) return } + // TODO: bulkWrite SLUGS? Maybe assign an id to each document and then use that to create the slug? // let slug = req.get("Slug") // if(slug){ @@ -936,24 +1033,134 @@ const bulkCreate = async function (req, res, next) { // slug = slug_json.slug_id // } // } + + // unordered bulkWrite() operations have better performance metrics. let bulkOps = [] - documents.forEach(d => { - const id = ObjectID() - let generatorAgent = getAgentClaim(req, next) + const generatorAgent = getAgentClaim(req, next) + for(let d of documents) { + // Do not create empty {}s + if(Object.keys(d).length === 0) continue + const providedID = d?._id + const id = isValidID(providedID) ? providedID : ObjectID() d = utils.configureRerumOptions(generatorAgent, d) - // TODO: check profiles/parameters for 'id' vs '@id' and use that + // id is also protected in this case, so it can't be set. + if(_contextid(d["@context"])) delete d.id d._id = id d['@id'] = `${process.env.RERUM_ID_PREFIX}${id}` bulkOps.push({ insertOne : { "document" : d }}) - }) + } try { - let dbResponse = await db.bulkWrite(bulkOps) + let dbResponse = await db.bulkWrite(bulkOps, {'ordered':false}) res.set("Content-Type", "application/json; charset=utf-8") res.set("Link",dbResponse.result.insertedIds.map(r => `${process.env.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 res.status(201) const estimatedResults = bulkOps.map(f=>{ let doc = f.insertOne.document - delete doc._id + doc = idNegotiation(doc) + return doc + }) + res.json(estimatedResults) // https://www.rfc-editor.org/rfc/rfc7231#section-6.3.2 + } + catch (error) { + //MongoServerError from the client has the following properties: index, code, keyPattern, keyValue + next(createExpressError(error)) + } +} + +/** + * Update many objects at once with the power of MongoDB bulkWrite() operations. + * Make sure to alter object __rerum.history as appropriate. + * The same object may be updated more than once, which will create history branches (not straight sticks) + * + * @see https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/ + */ +const bulkUpdate = async function (req, res, next) { + res.set("Content-Type", "application/json; charset=utf-8") + const documents = req.body + let err = {} + let encountered = [] + if (!Array.isArray(documents)) { + err.message = "The request body must be an array of objects." + err.status = 400 + next(createExpressError(err)) + return + } + if (documents.length === 0) { + err.message = "No action on an empty array." + err.status = 400 + next(createExpressError(err)) + return + } + const gatekeep = documents.filter(d => { + // Each item must be valid JSON, but can't be an array. + if(Array.isArray(d) || typeof d !== "object") return d + try { + JSON.parse(JSON.stringify(d)) + } catch (err) { + return d + } + // Items must have an @id, or in some cases an id will do + const idcheck = _contextid(d["@context"]) ? (d.id ?? d["@id"]) : d["@id"] + if(!idcheck) return d + }) + // The empty {}s will cause this error + if (gatekeep.length > 0) { + err.message = "All objects in the body of a `/bulkUpdate` must be JSON and must contain a declared identifier property." + err.status = 400 + next(createExpressError(err)) + return + } + // unordered bulkWrite() operations have better performance metrics. + let bulkOps = [] + const generatorAgent = getAgentClaim(req, next) + for(const objectReceived of documents){ + // We know it has an id + const idReceived = objectReceived["@id"] ?? objectReceived.id + // Update the same thing twice? can vs should. + // if(encountered.includes(idReceived)) continue + encountered.push(idReceived) + if(!idReceived.includes(process.env.RERUM_ID_PREFIX)) continue + let id = parseDocumentID(idReceived) + let originalObject + try { + originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) + } catch (error) { + next(createExpressError(error)) + return + } + if (null === originalObject) continue + if (utils.isDeleted(originalObject)) continue + id = ObjectID() + let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {} + let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, originalObject, true, false)["__rerum"] } + delete objectReceived["__rerum"] + delete objectReceived["_id"] + delete objectReceived["@id"] + // id is also protected in this case, so it can't be set. + if(_contextid(objectReceived["@context"])) delete objectReceived.id + delete objectReceived["@context"] + let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) + bulkOps.push({ insertOne : { "document" : newObject }}) + if(originalObject.__rerum.history.next.indexOf(newObject["@id"]) === -1){ + originalObject.__rerum.history.next.push(newObject["@id"]) + const replaceOp = { replaceOne : + { + "filter" : { "_id": originalObject["_id"] }, + "replacement" : originalObject, + "upsert" : false + } + } + bulkOps.push(replaceOp) + } + } + try { + let dbResponse = await db.bulkWrite(bulkOps, {'ordered':false}) + res.set("Content-Type", "application/json; charset=utf-8") + res.set("Link", dbResponse.result.insertedIds.map(r => `${process.env.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 + res.status(200) + const estimatedResults = bulkOps.filter(f=>f.insertOne).map(f=>{ + let doc = f.insertOne.document + doc = idNegotiation(doc) return doc }) res.json(estimatedResults) // https://www.rfc-editor.org/rfc/rfc7231#section-6.3.2 @@ -980,9 +1187,11 @@ const idHeadRequest = async function (req, res, next) { res.sendStatus(200) return } - let err = new Error(`No RERUM object with id '${id}'`) - err.status = 404 - throw err + let err = { + "message": `No RERUM object with id '${id}'`, + "status": 404 + } + next(createExpressError(err)) } catch (error) { next(createExpressError(error)) } @@ -1003,9 +1212,11 @@ const queryHeadRequest = async function (req, res, next) { res.sendStatus(200) return } - let err = new Error(`There is no object in the database with id '${id}'. Check the URL.`) - err.status = 404 - throw err + let err = { + "message": `There is no object in the database with id '${id}'. Check the URL.`, + "status": 404 + } + next(createExpressError(err)) } catch (error) { next(createExpressError(error)) } @@ -1042,10 +1253,7 @@ const since = async function (req, res, next) { }) let descendants = getAllDescendants(all, obj, []) descendants = - descendants.map(o => { - delete o._id - return o - }) + descendants.map(o => idNegotiation(o)) res.set(utils.configureLDHeadersFor(descendants)) res.json(descendants) } @@ -1083,10 +1291,7 @@ const history = async function (req, res, next) { }) let ancestors = getAllAncestors(all, obj, []) ancestors = - ancestors.map(o => { - delete o._id - return o - }) + ancestors.map(o => idNegotiation(o)) res.set(utils.configureLDHeadersFor(ancestors)) res.json(ancestors) } @@ -1485,42 +1690,30 @@ async function newTreePrime(obj) { } /** - * - * @param {Object} update `message` and `status` for creating a custom Error - * @param {Error} originalError `source` for tracing this Error - * @returns Error for use in Express.next(err) + * Recieve an error from a route. It should already have a statusCode and statusMessage. + * Note that this may be a Mongo error that occurred during a database action during a route. + * Reformat known mongo errors into regular errors with an apprpriate statusCode and statusMessage. + * + * @param {Object} err An object with `statusMessage` and `statusCode`, or a Mongo error with 'code', for error reporting + * @returns A JSON object with a statusCode and statusMessage to send into rest.js for RESTful erroring. */ -function createExpressError(update, originalError = {}) { - let err = Error("detected error", { cause: originalError }) - if (update.code) { - /** - * Detection that createExpressError(error) passed in a mongo client error. - * IMPORTANT! If you try to write to 'update' when it comes in as a mongo error... - * - POST /v1/api/create 500 - Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client - * - * If you do update.statusMessage or update.statusCode YOU WILL CAUSE THIS ERROR. - * Make sure you write to err instead. Object.assign() will have the same result. - */ - switch (update.code) { +function createExpressError(err) { + let error = {} + if (err.code) { + switch (err.code) { case 11000: //Duplicate _id key error, specific to SLUG support. This is a Conflict. - err.statusMessage = `The id provided in the Slug header already exists. Please use a different Slug.` - err.statusCode = 409 + error.statusMessage = `The id provided already exists. Please use a different _id or Slug.` + error.statusCode = 409 break default: - err.statusMessage = "There was a mongo error that prevented this request from completing successfully." - err.statusCode = 500 + error.statusMessage = "There was a mongo error that prevented this request from completing successfully." + error.statusCode = 500 } } - else { - //Warning! If 'update' is considered sent, this will cause a 500. See notes above. - update.statusMessage = update.message - update.statusCode = update.status - } - Object.assign(err, update) - return err + error.statusCode = err.statusCode ?? err.status ?? 500 + error.statusMessage = err.statusMessage ?? err.message ?? "Detected Error" + return error } /** @@ -1557,8 +1750,10 @@ function getAgentClaim(req, next) { return agent } } - let err = new Error("Could not get agent from req.user. Have you registered with RERUM?") - err.status = 403 + let err = { + "message": "Could not get agent from req.user. Have you registered with RERUM?", + "status": 403 + } next(createExpressError(err)) } @@ -2151,6 +2346,7 @@ export default { query, id, bulkCreate, + bulkUpdate, idHeadRequest, queryHeadRequest, since, @@ -2159,5 +2355,6 @@ export default { historyHeadRequest, remove, _gog_glosses_from_manuscript, - _gog_fragments_from_manuscript + _gog_fragments_from_manuscript, + idNegotiation } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 62490ea2..6cd31a2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,9 @@ "@jest-mock/express": "^2.0.2", "jest": "^29.7.0", "supertest": "^6.2.2" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@ampproject/remapping": { @@ -40,81 +43,19 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", @@ -272,17 +213,19 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -296,99 +239,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.27.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -560,13 +430,14 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -593,13 +464,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -3416,7 +3287,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -4607,14 +4479,6 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4892,63 +4756,13 @@ } }, "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { @@ -5065,14 +4879,14 @@ } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" }, "@babel/helper-validator-option": { "version": "7.23.5", @@ -5080,81 +4894,22 @@ "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==" }, "@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" } }, - "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } + "@babel/types": "^7.27.0" } }, - "@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==" - }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -5268,13 +5023,13 @@ } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" } }, "@babel/traverse": { @@ -5295,13 +5050,12 @@ } }, "@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" } }, "@bcoe/v8-coverage": { @@ -8288,11 +8042,6 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/public/API.html b/public/API.html index 6099c77b..5937de99 100644 --- a/public/API.html +++ b/public/API.html @@ -420,10 +420,11 @@

Bulk Create

Add multiple completely new objects to RERUM and receive an array of the complete records as the response body. - Accepts only a single array of JSON objects in the request body. - The array of JSON objects passed in will be created in the order submitted and the response will have the URI - of the new resource or an error message as an array in the same order. When errors are encountered, - the batch process will attempt to continue for all submitted items. + Accepts only a single array of JSON objects in the request body. The '@id' property must not be present on the objects. + In cases where the Linked Data @context property maps '@id' to 'id', the 'id' property also must not be present. + The array of JSON objects passed may not be created in the order submitted. The response will have the URI + of the new resource or an error message as an array in the order the order the objects were processed. + When errors are encountered, the batch process will attempt to continue for all submitted items.

@@ -730,6 +731,80 @@

Update

+

Bulk Update

+ + + + + + + + + + + + + + + + +
PatternsPayloadsResponses
/bulkUpdate[{JSON}]201 + [{JSON}]
+ + + +

+ Update multiple existing RERUM objects at once and recieve an array of the complete records as the response body. + Accepts only a single array of JSON objects in the request body. The '@id' property must be present for each object. + In cases where the Linked Data @context property maps '@id' to 'id' either of these properties will be sufficient. + The array of JSON objects passed in may not be updated in the order submitted. The response will have the URI + of the new resource or an error message as an array in the order the objects were processed. When errors are encountered, the batch process will attempt to continue for all submitted items. +

+ +

+

Javascript Example
+
 
+                const saved_obj = await fetch("https://devstore.rerum.io/v1/api/bulkUpdate", {
+                    method: "POST",
+                    headers:{
+                        "Authorization": "Bearer eyJz93a...k4laUWw" 
+                        "Content-Type": "application/json; charset=utf-8"
+                    },
+                    body: JSON.stringify([
+                        "@id": "https://devstore.rerum.io/v1/id/abcdef1234567890",
+                        "hello": "new world",
+                        "@id": "https://devstore.rerum.io/v1/id/1234567890abcdef",
+                        "goodbye": "old planet"
+                    ])
+                })
+                .then(resp => resp.json())
+                .catch(err => {throw err})
+            
+

+ +

+

Here is what the response resp looks like:
+

+                [
+                    {
+                        "@id": "https://devstore.rerum.io/v1/id/abcabc1231231230",
+                        "hello": "new world",
+                        "__rerum":{...}
+                    },
+                    {
+                        "@id": "https://devstore.rerum.io/v1/id/defdef4564564567",
+                        "goodbye": "old planet",
+                         "__rerum":{...}
+                    }
+                ]
+            
+

+

Overwrite

diff --git a/rest.js b/rest.js index 20371755..55093541 100644 --- a/rest.js +++ b/rest.js @@ -29,46 +29,44 @@ const checkPatchOverrideSupport = function (req, res) { * REST is all about communication. The response code and the textual body are particular. * RERUM is all about being clear. It will build custom responses sometimes for certain scenarios, will remaining RESTful. * - * Note that the res upstream from this has been converted into err. res will not have what you are looking for, check err instead. + * You have likely reached this with a next(createExpressError(err)) call. End here and send the error. */ const messenger = function (err, req, res, next) { if (res.headersSent) { - next(err) return } - err.message = err.message ?? res.message ?? `` - if (err.statusCode === 401) { + let error = {} + error.message = err.statusMessage ?? err.message ?? `` + error.status = err.statusCode ?? err.status ?? 500 + if (error.status === 401) { //Special handler for token errors from the oauth module //Token errors come through with a message that we want. That message is in the error's WWW-Authenticate header //Other 401s from our app come through with a status message. They may not have headers. if (err.headers?.["WWW-Authenticate"]) { - err.message += err.headers["WWW-Authenticate"] + error.message += err.headers["WWW-Authenticate"] } } - let genericMessage = "" let token = req.header("Authorization") if(token && !token.startsWith("Bearer ")){ - err.message +=` + error.message +=` Your token is not in the correct format. It should be a Bearer token formatted like: "Bearer "` - next(err) - return } - switch (err.statusCode) { + switch (error.status) { case 400: //"Bad Request", most likely because the body and Content-Type are not aligned. Could be bad JSON. - err.message += ` + error.message += ` The body of your request was invalid. Please make sure it is a valid content-type and that the body matches that type. If the body is JSON, make sure it is valid JSON.` break case 401: //The requesting agent is known from the request. That agent does not match __rerum.generatedBy. Unauthorized. if (token) { - err.message += ` + error.message += ` The token provided is Unauthorized. Please check that it is your token and that it is not expired. Token: ${token} ` } else { - err.message += ` + error.message += ` The request does not contain an "Authorization" header and so is Unauthorized. Please include a token with your requests like "Authorization: Bearer ". Make sure you have registered at ${process.env.RERUM_PREFIX}.` } @@ -76,7 +74,7 @@ like "Authorization: Bearer ". Make sure you have registered at ${process case 403: //Forbidden to use this. The provided Bearer does not have the required privileges. if (token) { - err.message += ` + error.message += ` You are Forbidden from performing this action. Check your privileges. Token: ${token}` } @@ -87,24 +85,31 @@ You are Forbidden from performing this action. The request does not contain an " Make sure you have registered at ${process.env.RERUM_PREFIX}. ` } case 404: - err.message += ` + error.message += ` The requested web page or resource could not be found.` break case 405: // These are all handled in api-routes.js already. break + case 409: + // These are all handled in db-controller.js already. + break + case 501: + // Not implemented. Handled upstream. + break case 503: //RERUM is down or readonly. Handled upstream. break case 500: default: //Really bad, probably not specifically caught. - err.message += ` + error.message += ` RERUM experienced a server issue while performing this action. It may not have completed at all, and most likely did not complete successfully.` } - // res.status(statusCode).send(err.statusMessage) - next(err) + console.error(error) + res.set("Content-Type", "text/plain; charset=utf-8") + res.status(error.status).send(error.message) } export default { checkPatchOverrideSupport, messenger } diff --git a/routes/__tests__/bulkUpdate.test.js b/routes/__tests__/bulkUpdate.test.js new file mode 100644 index 00000000..e857e5a0 --- /dev/null +++ b/routes/__tests__/bulkUpdate.test.js @@ -0,0 +1,23 @@ +import { jest } from "@jest/globals" + +// Only real way to test an express route is to mount it and call it so that we can use the req, res, next. +import express from "express" +import request from "supertest" +import controller from '../../db-controller.js' + +// Here is the auth mock so we get a req.user and the controller can function without a NPE. +const addAuth = (req, res, next) => { + req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} + next() +} + +const routeTester = new express() +routeTester.use(express.json()) +routeTester.use(express.urlencoded({ extended: false })) + +// Mount our own /bulkCreate route without auth that will use controller.bulkCreate +routeTester.use("/bulkUpdate", [addAuth, controller.bulkUpdate]) + +it.skip("'/bulkUpdate' route functions", async () => { + // TODO without hitting the v1/id/11111 object because it is already abused. +}) diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index d208c4b2..788247f9 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -38,3 +38,7 @@ it("'/create' route functions", async () => { expect(response.headers["link"]).toBeTruthy() }) + +it.skip("Support setting valid '_id' on '/create' request body.", async () => { + // TODO +}) \ No newline at end of file diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index 5631345f..acfeba44 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -32,3 +32,7 @@ it("'/id/:id' route functions", async () => { expect(response.headers["location"]).toBeTruthy() }) + +it.skip("Proper '@id-id' negotation on GET by URI.", async () => { + // TODO +}) diff --git a/routes/__tests__/idNegotiation.test.js b/routes/__tests__/idNegotiation.test.js new file mode 100644 index 00000000..c9b5c33a --- /dev/null +++ b/routes/__tests__/idNegotiation.test.js @@ -0,0 +1,30 @@ +import { jest } from "@jest/globals" +import dotenv from "dotenv" +import controller from '../../db-controller.js' + +it("Functional '@id-id' negotiation on objects returned.", async () => { + let negotiate = { + "@context": "http://iiif.io/api/presentation/3/context.json", + "_id": "example", + "@id": `${process.env.RERUM_ID_PREFIX}example`, + "test": "item" + } + negotiate = controller.idNegotiation(negotiate) + expect(negotiate._id).toBeUndefined() + expect(negotiate["@id"]).toBeUndefined() + expect(negotiate.id).toBe(`${process.env.RERUM_ID_PREFIX}example`) + expect(negotiate.test).toBe("item") + + let nonegotiate = { + "@context":"http://example.org/context.json", + "_id": "example", + "@id": `${process.env.RERUM_ID_PREFIX}example`, + "id": "test_example", + "test":"item" + } + nonegotiate = controller.idNegotiation(nonegotiate) + expect(nonegotiate._id).toBeUndefined() + expect(nonegotiate["@id"]).toBe(`${process.env.RERUM_ID_PREFIX}example`) + expect(nonegotiate.id).toBe("test_example") + expect(nonegotiate.test).toBe("item") +}) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index 450eaad6..b593b6f9 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -31,3 +31,7 @@ it("'/query' route functions", async () => { expect(response.headers["link"]).toBeTruthy() }) + +it.skip("Proper '@id-id' negotation on objects returned from '/query'.", async () => { + // TODO +}) diff --git a/routes/api-routes.js b/routes/api-routes.js index bd15a4dd..0db3de98 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -22,6 +22,8 @@ import queryRouter from './query.js'; import createRouter from './create.js'; // Support POST requests with JSON Array bodies used for establishing new objects. import bulkCreateRouter from './bulkCreate.js'; +//Support PUT requests with JSON Array bodies used for updating a number of existing objects. +import bulkUpdateRouter from './bulkUpdate.js'; // Support DELETE requests like v1/delete/{object id} to mark an object as __deleted. import deleteRouter from './delete.js'; // Support POST requests with JSON bodies used for replacing some existing object. @@ -47,6 +49,7 @@ router.use('/api', compatabilityRouter) router.use('/api/query', queryRouter) router.use('/api/create', createRouter) router.use('/api/bulkCreate', bulkCreateRouter) +router.use('/api/bulkUpdate', bulkUpdateRouter) router.use('/api/delete', deleteRouter) router.use('/api/overwrite', overwriteRouter) router.use('/api/update', updateRouter) @@ -73,22 +76,8 @@ router.get('/api', (req, res) => { }) router.use('/since', sinceRouter) router.use('/history', historyRouter) -/** - * Use this to catch 404s because of invalid /api/ paths and pass them to the error handler in app.js - * - * Note while we have 501s, they will fall here. Don't let them trick you. - * Detect them and send them out, don't hand up to the 404 catcher in app.js - */ -router.use((req, res, next) => { - if (res.statusCode === 501) { - //We can remove this once we implement the functions, for now we have to catch it here. - let msg = res.statusMessage ?? "This is not yet implemented" - res.status(501).send(msg).end() - } - else { - //A 404 to pass along to our 404 handler in app.js - next() - } -}) + +// Note that error responses are handled by rest.js through app.js. No need to do anything with them here. + // Export API routes export default router diff --git a/routes/bulkUpdate.js b/routes/bulkUpdate.js new file mode 100644 index 00000000..f7fad3fa --- /dev/null +++ b/routes/bulkUpdate.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import express from 'express' +const router = express.Router() + +//This controller will handle all MongoDB interactions. +import controller from '../db-controller.js' +import auth from '../auth/index.js' + +router.route('/') + .put(auth.checkJwt, controller.bulkUpdate) + .all((req, res, next) => { + res.statusMessage = 'Improper request method for creating, please use PUT.' + res.status(405) + next(res) + }) + +export default router diff --git a/routes/patchSet.js b/routes/patchSet.js index 9dc036b8..ff67ec1a 100644 --- a/routes/patchSet.js +++ b/routes/patchSet.js @@ -14,7 +14,7 @@ router.route('/') else { res.statusMessage = 'Improper request method for updating, please use PATCH to add new keys to this object.' res.status(405) - next() + next(res) } }) .all((req, res, next) => { diff --git a/routes/patchUnset.js b/routes/patchUnset.js index 93951e22..6bdf0b65 100644 --- a/routes/patchUnset.js +++ b/routes/patchUnset.js @@ -14,7 +14,7 @@ router.route('/') else { res.statusMessage = 'Improper request method for updating, please use PATCH to remove keys from this object.' res.status(405) - next() + next(res) } }) .all((req, res, next) => { diff --git a/routes/patchUpdate.js b/routes/patchUpdate.js index de5ab79f..5df088bf 100644 --- a/routes/patchUpdate.js +++ b/routes/patchUpdate.js @@ -15,7 +15,7 @@ router.route('/') else { res.statusMessage = 'Improper request method for updating, please use PATCH to alter the existing keys this object.' res.status(405) - next() + next(res) } }) .all((req, res, next) => {