mongo-rest-router
Version:
Exposes a Mongo collection via a REST API
491 lines (483 loc) • 17.9 kB
JavaScript
;
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