UNPKG

mongo-rest-router

Version:

Exposes a Mongo collection via a REST API

491 lines (483 loc) 17.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; 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 __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 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/index.ts var src_exports = {}; __export(src_exports, { MongoRestRouter: () => MongoRestRouter, ValidationError: () => import_ajv.ValidationError, applyPatchRequest: () => applyPatchRequest, getValidate: () => getValidate, handleValidateError: () => handleValidateError, withDb: () => withDb }); module.exports = __toCommonJS(src_exports); // src/mongo-rest-router.ts var import_express = require("express"); var import_query_to_mongo = __toESM(require("query-to-mongo")); var import_mongodb3 = require("mongodb"); // src/handle-validation-error.ts var import_ajv = require("ajv"); var handleValidateError = (e, res) => { if (e instanceof SyntaxError) { return res.status(400).send({ error: "Invalid JSON payload", jsonParseError: e.message }); } if (e instanceof import_ajv.ValidationError) { return res.status(400).send({ error: "Invalid payload", validationErrors: e.errors }); } throw e; }; // src/with-db.ts var import_mongodb = require("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 import_mongodb.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 import_mongodb.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 import_mongodb.MongoServerError) { res.status(503).send({ error: "Mongo server is overloaded." }); return; } if (e instanceof import_mongodb.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 var import_ajv2 = __toESM(require("ajv")); var import_ajv_formats = __toESM(require("ajv-formats")); var import_mongodb2 = require("mongodb"); var import_fast_json_patch = require("fast-json-patch"); var ajv = (0, import_ajv_formats.default)(new import_ajv2.default()); 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 import_mongodb2.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 = (0, import_fast_json_patch.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 import_ajv.ValidationError([e]); } return newObject; }; // src/get-validate.ts var import_ajv3 = __toESM(require("ajv")); var import_ajv_formats2 = __toESM(require("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 = (0, import_ajv_formats2.default)(new import_ajv3.default()); 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 = (0, import_express.Router)(options); router.use((0, import_express.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 } = (0, import_query_to_mongo.default)(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 import_mongodb3.ObjectId(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 } = (0, import_query_to_mongo.default)(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 import_mongodb3.ObjectId(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 import_ajv.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 import_mongodb3.ObjectId(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 import_mongodb3.ObjectId(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 import_mongodb3.ObjectId(req.params.id) }); res.status(200).send({ deletedCount: result2.deletedCount }); return; } const criteria = { "_id": new import_mongodb3.ObjectId(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; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MongoRestRouter, ValidationError, applyPatchRequest, getValidate, handleValidateError, withDb }); //# sourceMappingURL=index.js.map