UNPKG

mongo-rest-router

Version:

Exposes a Mongo collection via a REST API

452 lines (445 loc) 15.7 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/mongo-rest-router.ts import { Router, json } from "express"; import q2m from "query-to-mongo"; import { ObjectId as ObjectId2 } from "mongodb"; // src/handle-validation-error.ts import { ValidationError } from "ajv"; var handleValidateError = (e, res) => { if (e instanceof SyntaxError) { return res.status(400).send({ error: "Invalid JSON payload", jsonParseError: e.message }); } if (e instanceof ValidationError) { return res.status(400).send({ error: "Invalid payload", validationErrors: e.errors }); } throw e; }; // src/with-db.ts import { MongoClient, MongoError, MongoServerError } from "mongodb"; var clients = {}; var resolveDb = (db) => { if (typeof db == "function") return db(); if (db === void 0) db = process.env.MONGO_URL; if (typeof db == "string") { const client = clients[db] || new MongoClient(db); clients[db] = client; return client.db(); } if (db === void 0) { throw new Error("The withDb() function was called w/o a db. Try setting MONGO_URL."); } return db; }; var withDb = (db) => (req, res, next) => { var _a; const mongoURL = process.env.MONGO_URL; if (!mongoURL) throw new Error("MONGO_URL not set"); try { req.locals.db = resolveDb(db); } catch (e) { if (e instanceof MongoError) { console.warn("Unexpected error connecting to db. Resetting the connection.", e); (_a = clients[mongoURL]) == null ? void 0 : _a.close(); clients[mongoURL] = void 0; } if (e instanceof MongoServerError) { res.status(503).send({ error: "Mongo server is overloaded." }); return; } if (e instanceof MongoError) { res.status(500).send({ error: "Unexpected Mongo error." }); return; } console.error("Unexpected error connecting to db.", e); res.status(500).send({ error: "Unexpected error." }); } if (next) next(); }; // src/json-patch-schema.ts var json_patch_schema_default = { "$async": true, // important so that the validate method throws errors "type": "array", "items": { "oneOf": [{ "type": "object", "properties": { "op": { "enum": ["add", "replace", "test"] }, "path": { "type": "string", "pattern": "^(/([^~/]|~[01])*)*$" }, "value": {} }, "required": ["op", "path", "value"] }, { "type": "object", "properties": { "op": { "enum": ["remove"] }, "path": { "type": "string", "pattern": "^(/([^~/]|~[01])*)*$" } }, "required": ["op", "path"] }, { "type": "object", "properties": { "op": { "enum": ["move", "copy"] }, "from": { "type": "string", "pattern": "^(/([^~/]|~[01])*)*$" }, "path": { "type": "string", "pattern": "^(/([^~/]|~[01])*)*$" } }, "required": ["op", "from", "path"] }] } }; // src/apply-patch-request.ts import Ajv from "ajv"; import addFormats from "ajv-formats"; import { ObjectId } from "mongodb"; import { applyPatch } from "fast-json-patch"; var ajv = addFormats(new Ajv()); var validatePatchSchema = ajv.compile(json_patch_schema_default); var getPatchTarget = (o, keys) => { if (o === void 0) return void 0; if (!Array.isArray(keys)) { keys = keys.split("/").filter((x) => !!x).map((x) => x.replace("~0", "/").replace("~1", "~")); } if (keys.length > 1) { if (Array.isArray(o)) { const index = parseInt(keys[0]); return getPatchTarget(o[index], keys.slice(1)); } return getPatchTarget(o[keys[0]], keys.slice(1)); } if (["number", "bigint", "string", "boolean"].includes(typeof o)) return void 0; return o; }; var applyPatchRequest = (origObject, req) => { let newObject = { _id: new ObjectId() }; const isBodyEmpty = !Object.keys(req.body).length; if (isBodyEmpty) { newObject = __spreadValues({}, origObject); Object.keys(req.query).filter((x) => !!x).forEach((key) => { var _a; const target = getPatchTarget(newObject, key); const value = (_a = req.query[key]) == null ? void 0 : _a.toString(); if (target === void 0 || value === void 0) return; const literals = [ ["undefined", void 0], ["null", null], ["true", true], ["false", false] ]; const l = literals.find(([x]) => value == x); if (l != void 0) { target[key] = l[1]; return; } if (value[0] == '"' && value[0] == value[value.length - 1]) { target[key] = value.slice(1, value.length - 2); return; } const n = parseFloat(value); if (n < Infinity && n > -Infinity) { target[key] = n; return; } const d = new Date(value); if (!isNaN(d.getTime())) { target[key] = d; } target[key] = value; }); } else { const patch = JSON.parse(req.body); validatePatchSchema(req.body); newObject = applyPatch(origObject, patch).newDocument; } if (origObject._id.toHexString() != newObject._id.toHexString()) { const e = { keyword: "", instancePath: "/_id", schemaPath: "", params: [], message: "The _id field is read only." }; throw new ValidationError([e]); } return newObject; }; // src/get-validate.ts import Ajv2 from "ajv"; import addFormats2 from "ajv-formats"; var requiredIdSchema = { type: "string" }; var optionalDateSchema = { type: "string", format: "date-time", nullable: true }; var withId = (schema) => { const newSchema = __spreadValues({}, schema); newSchema.properties = __spreadProps(__spreadValues({}, newSchema.properties), { _id: requiredIdSchema, $async: true }); newSchema.required = [...newSchema.required.filter((x) => x != "_id")]; return newSchema; }; var withManagedDates = (schema, dateFields = {}) => { const newSchema = __spreadValues({}, schema); newSchema.properties = __spreadValues({}, newSchema.properties); newSchema.properties[dateFields.added || "added"] = optionalDateSchema; newSchema.properties[dateFields.lastModified || "lastModified"] = optionalDateSchema; newSchema.properties[dateFields.deleted || "deleted"] = optionalDateSchema; return newSchema; }; var getValidate = (schema, options = {}) => { const ajv2 = addFormats2(new Ajv2()); const { dateFields: dateFieldOverrides } = options; const dateFields = __spreadValues({ added: "added", lastModified: "lastModified", deleted: "deleted" }, dateFieldOverrides); const idSchema = withId(schema); const dateSchema = withManagedDates(withId(schema), dateFields); const validate = (payload, options2) => { const { allowManagedDates: isUpdate = false } = options2 || {}; if (isUpdate) { const p = payload; delete p._id; delete p[dateFields.added]; delete p[dateFields.lastModified]; delete p[dateFields.deleted]; } ajv2.validate(schema, payload); return payload; }; const validateBulk = (payload) => { if (Array.isArray(payload)) { payload.forEach((x) => !validate(x)); return payload; } validate(payload); return [payload]; }; return { validate, validateBulk }; }; // src/mongo-rest-router.ts var NotFoundMessage = "An entry with that id could not be found."; var idPath = "/:id([0-9a-fA-F]{24})"; var MongoRestRouter = (collection, schema, options = {}) => { const { db, methods, sort, noGetSearch: noSearch, noPostBulk, resultsField, noArchive, noManagedDates, dateFields: dateFieldOverrides } = options; const dateFields = __spreadValues({ added: "added", lastModified: "lastModified", deleted: "deleted" }, dateFieldOverrides); const router = Router(options); router.use(json()); router.use(withDb(db)); const { validate, validateBulk } = getValidate(schema, { dateFields }); if (!methods || methods.includes("GET")) { if (!noSearch) { router.get("/", (req, res) => __async(void 0, null, function* () { const c = req.locals.db.collection(collection); const { criteria, options: options2 } = q2m(req.query); options2.sort = options2.sort || sort; if (!noManagedDates) { criteria[dateFields.deleted] = { "$exists": false }; } const result = {}; result.count = yield c.countDocuments(criteria); result[resultsField || collection] = yield c.find(criteria, options2).toArray(); return res.send(result); })); } router.get(idPath, (req, res) => __async(void 0, null, function* () { const c = req.locals.db.collection(collection); const criteria = { "_id": new ObjectId2(req.params.id) }; if (!noManagedDates) { criteria[dateFields.deleted] = { "$exists": false }; } const found = yield c.findOne(criteria); if (!found) { res.status(404).send({ error: NotFoundMessage }); return; } res.send(found); })); if (!noArchive || noManagedDates) { router.get(`/archive`, (req, res) => __async(void 0, null, function* () { const c = req.locals.db.collection(collection); const { criteria, options: options2 } = q2m(req.query); criteria[dateFields.deleted] = { "$exists": true }; const result = {}; result.count = yield c.countDocuments(criteria); result[resultsField || collection] = yield c.find(criteria, options2).toArray(); return res.send(result); })); router.get(`/archive/${idPath}`, (req, res) => __async(void 0, null, function* () { const c = req.locals.db.collection(collection); const criteria = { "_id": new ObjectId2(req.params.id) }; criteria[dateFields.deleted] = { "$exists": true }; const found = yield c.findOne(criteria); if (!found) { res.status(404).send({ error: NotFoundMessage }); return; } res.send(found); })); } } if (!methods || methods.includes("POST")) { router.post("/", (req, res) => __async(void 0, null, function* () { try { const payload = validateBulk(req.body); const c = req.locals.db.collection(collection); if (Array.isArray(payload)) { if (noPostBulk) { const e = { keyword: "", instancePath: "", schemaPath: "", params: [], message: "Expecting an object" }; throw new ValidationError([e]); } if (!noManagedDates) { const now = /* @__PURE__ */ new Date(); payload.forEach((x) => { x[dateFields.added] = now; }); } const result2 = yield c.insertMany(payload); res.send({ insertedIds: result2.insertedIds }); return; } if (!noManagedDates) { payload[dateFields.added] = /* @__PURE__ */ new Date(); } const result = yield c.insertOne(payload); res.send({ insertedId: result.insertedId }); } catch (e) { handleValidateError(e, res); } })); } if (!methods || methods.includes("PUT")) { router.put(idPath, (req, res) => __async(void 0, null, function* () { try { const payload = validate(req.body, { allowManagedDates: true }); payload._id = new ObjectId2(req.params.id); if (!noManagedDates) { const p = payload; p[dateFields.added] = void 0; p[dateFields.lastModified] = /* @__PURE__ */ new Date(); } const c = req.locals.db.collection(collection); const criteria = { "_id": payload._id }; if (!noManagedDates) { criteria[dateFields.deleted] = { "$exists": false }; } const result = yield c.updateOne(criteria, payload); if (result.modifiedCount == 0) { res.status(404).send({ error: NotFoundMessage }); return; } res.send({ modifiedCount: result.modifiedCount }); } catch (e) { handleValidateError(e, res); } })); } if (!methods || methods.includes("PATCH")) { router.patch(idPath, (req, res) => __async(void 0, null, function* () { const c = req.locals.db.collection(collection); const criteria = { "_id": new ObjectId2(req.params.id) }; if (!noManagedDates) { criteria[dateFields.deleted] = { "$exists": false }; } const origObject = yield c.findOne(criteria); if (!origObject) { res.status(404).send({ error: NotFoundMessage }); return; } try { const newObject = applyPatchRequest(origObject, req); console.log("PATCH newObject", newObject); validate(newObject, { allowManagedDates: true }); if (!noManagedDates) { newObject[dateFields.lastModified] = /* @__PURE__ */ new Date(); } const result = yield c.updateOne({ "_id": origObject._id }, { $set: newObject }); res.send({ modifiedCount: result.matchedCount }); } catch (e) { handleValidateError(e, res); return; } })); } if (!methods || methods.includes("DELETE")) { router.delete(idPath, (req, res) => __async(void 0, null, function* () { const c = req.locals.db.collection(collection); if (noArchive || noManagedDates) { const result2 = yield c.deleteOne({ "_id": new ObjectId2(req.params.id) }); res.status(200).send({ deletedCount: result2.deletedCount }); return; } const criteria = { "_id": new ObjectId2(req.params.id) }; if (!noManagedDates) { criteria[dateFields.deleted] = { "$exists": false }; } const updates = {}; updates[dateFields.deleted] = /* @__PURE__ */ new Date(); const result = yield c.updateOne(criteria, { "$set": updates }); res.status(200).send({ deletedCount: result.modifiedCount }); })); if (!noArchive || noManagedDates) { router.delete(`/archive/${idPath}`, (_req, res) => __async(void 0, null, function* () { res.status(501).send({ error: "Archive not yet implemented" }); })); } } return router; }; export { MongoRestRouter, ValidationError, applyPatchRequest, getValidate, handleValidateError, withDb }; //# sourceMappingURL=index.mjs.map