diff --git a/bin/rerum_v1.js b/bin/rerum_v1.js index a1b953d6..3c549d49 100644 --- a/bin/rerum_v1.js +++ b/bin/rerum_v1.js @@ -73,3 +73,4 @@ function onListening() { : 'port ' + addr.port debug('Listening on ' + bind) } + diff --git a/controllers/bulk.js b/controllers/bulk.js index 4b7eaa40..840ff10b 100644 --- a/controllers/bulk.js +++ b/controllers/bulk.js @@ -13,43 +13,58 @@ import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** - * Create many objects at once with the power of MongoDB bulkWrite() operations. - * - * @see https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/ + * Sets standard JSON response headers on the Express response object. + * @param {import('express').Response} res - Express response object */ -const bulkCreate = async function (req, res, next) { +function setJsonHeaders(res) { res.set("Content-Type", "application/json; charset=utf-8") +} + +//function to validate request body is a non-empty array +function requireNonEmptyArrayBody(req) { const documents = req.body - let err = {} if (!Array.isArray(documents)) { - err.message = "The request body must be an array of objects." - err.status = 400 - next(createExpressError(err)) - return + throw { message: "The request body must be an array of objects.", status: 400 } } if (documents.length === 0) { - err.message = "No action on an empty array." - err.status = 400 - next(createExpressError(err)) - return + throw { message: "No action on an empty array.", status: 400 } + } + return documents +} + +//check if an item is valid JSON object (not array) +function isValidJsonObject(d) { + return d !== null && typeof d === "object" && !Array.isArray(d) +} + +/** + * 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) { + setJsonHeaders(res) + + let documents + try { + documents = requireNonEmptyArrayBody(req) + } catch (err) { + return next(createExpressError(err)) } + 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 - } + // Each item must be valid JSON, but can't be an array. + if (!isValidJsonObject(d)) return true // 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(idcheck) return true // Reject items WITH an id (creates must not have one) + return false + }) 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(createExpressError(err)) - return + return next(createExpressError({ + message: "All objects in the body of a `/bulkCreate` must be JSON and must not contain a declared identifier property.", + status: 400 + })) } // TODO: bulkWrite SLUGS? Maybe assign an id to each document and then use that to create the slug? @@ -82,7 +97,6 @@ const bulkCreate = async function (req, res, next) { } 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 => `${config.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 res.status(201) const estimatedResults = bulkOps.map(f=>{ @@ -106,40 +120,28 @@ const bulkCreate = async function (req, res, next) { * @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 + setJsonHeaders(res) + + let documents + try { + documents = requireNonEmptyArrayBody(req) + } catch (err) { + return next(createExpressError(err)) } + + let encountered = [] 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 + if (!isValidJsonObject(d)) return true const idcheck = _contextid(d["@context"]) ? (d.id ?? d["@id"]) : d["@id"] - if(!idcheck) return d + if (!idcheck) return true // Reject items WITHOUT an id (updates need an id) + return false }) // 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 + return next(createExpressError({ + message: "All objects in the body of a `/bulkUpdate` must be JSON and must contain a declared identifier property.", + status: 400 + })) } // unordered bulkWrite() operations have better performance metrics. let bulkOps = [] @@ -186,7 +188,6 @@ const bulkUpdate = async function (req, res, next) { } 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 => `${config.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 res.status(200) const estimatedResults = bulkOps.filter(f=>f.insertOne).map(f=>{ diff --git a/package-lock.json b/package-lock.json index c5f495e3..d688c74a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "debug": "~4.4.3", "dotenv": "~17.2.3", "express": "^5.2.1", - "express-oauth2-jwt-bearer": "~1.7.1", + "express-oauth2-jwt-bearer": "^1.7.4", "express-urlrewrite": "~2.0.3", "mongodb": "^7.0.0", "morgan": "~1.10.1" @@ -2568,9 +2568,9 @@ } }, "node_modules/express-oauth2-jwt-bearer": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.7.1.tgz", - "integrity": "sha512-IYwLHW9bysGnASwFfXqEZ2aJzY2g4B9P36sWgeIkSqWh1SjeIc8mfoeNsBpzil/lhXNWuttPH0rWQ6norZUBIg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.7.4.tgz", + "integrity": "sha512-teO/eyvU8OkJXiP4cRuoJMrp31nNvjnL47MIkso0D/21AqUGv1O+VEiLisrDA8xjkaCBTufYnV1zepCOCLK4vg==", "license": "MIT", "dependencies": { "jose": "^4.15.5" diff --git a/package.json b/package.json index 057182f6..ef47cd1a 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "debug": "~4.4.3", "dotenv": "~17.2.3", "express": "^5.2.1", - "express-oauth2-jwt-bearer": "~1.7.1", + "express-oauth2-jwt-bearer": "^1.7.4", "express-urlrewrite": "~2.0.3", "mongodb": "^7.0.0", "morgan": "~1.10.1"