Repository: florianholzapfel/express-restify-mongoose Branch: main Commit: a7a87ebb4e4c Files: 58 Total size: 323.5 KB Directory structure: gitextract_s8c0vam1/ ├── .eslintrc ├── .github/ │ └── workflows/ │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── .swcrc ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples/ │ └── invoicing.js ├── express.d.ts ├── index.d.ts ├── package.json ├── src/ │ ├── buildQuery.ts │ ├── detective.ts │ ├── errorHandler.ts │ ├── express-restify-mongoose.ts │ ├── getQuerySchema.ts │ ├── middleware/ │ │ ├── access.ts │ │ ├── ensureContentType.ts │ │ ├── filterAndFindById.ts │ │ ├── onError.ts │ │ ├── outputFn.ts │ │ ├── prepareOutput.ts │ │ └── prepareQuery.ts │ ├── moredots.ts │ ├── operations.ts │ ├── resource_filter.ts │ ├── types.ts │ └── weedout.ts ├── test/ │ ├── express.mjs │ ├── integration/ │ │ ├── access.mjs │ │ ├── contextFilter.mjs │ │ ├── create.mjs │ │ ├── delete.mjs │ │ ├── hooks.mjs │ │ ├── middleware.mjs │ │ ├── options.mjs │ │ ├── read.mjs │ │ ├── resource_filter.mjs │ │ ├── setup.mjs │ │ ├── update.mjs │ │ └── virtuals.mjs │ ├── restify.mjs │ ├── unit/ │ │ ├── buildQuery.mjs │ │ ├── detective.mjs │ │ ├── errorHandler.mjs │ │ ├── middleware/ │ │ │ ├── access.mjs │ │ │ ├── ensureContentType.mjs │ │ │ ├── onError.mjs │ │ │ ├── outputFn.mjs │ │ │ ├── prepareOutput.mjs │ │ │ └── prepareQuery.mjs │ │ ├── moredots.mjs │ │ ├── resourceFilter.mjs │ │ └── weedout.mjs │ └── unit.mjs └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "env": { "es2021": true }, "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "overrides": [ { "env": { "mocha": true, "node": true }, "files": ["test/**/*.js"], "plugins": ["mocha"] } ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2021, "sourceType": "module" }, "plugins": ["@typescript-eslint"], "rules": { "@typescript-eslint/no-unused-vars": [ "error", { "ignoreRestSiblings": true } ] } } ================================================ FILE: .github/workflows/node.js.yml ================================================ name: Test on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest strategy: matrix: mongodb-version: ["4.4", "5.0", "6.0", "7.0"] node-version: [16.x, 18.x, 20.x, 21.x] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: cache: "yarn" node-version: ${{ matrix.node-version }} - uses: supercharge/mongodb-github-action@1.6.0 - run: yarn --frozen-lockfile - run: yarn lint - run: yarn tsc - run: yarn build - run: yarn test ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: Publish on: release: types: [created] permissions: id-token: write contents: read jobs: build: runs-on: ubuntu-latest strategy: matrix: mongodb-version: ["4.4", "5.0", "6.0", "7.0"] node-version: [16.x, 18.x, 20.x, 21.x] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: cache: "yarn" node-version: ${{ matrix.node-version }} - uses: supercharge/mongodb-github-action@1.6.0 - run: yarn --frozen-lockfile - run: yarn lint - run: yarn tsc - run: yarn build - run: yarn test publish: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ - run: npm install -g npm@latest - run: yarn --frozen-lockfile - run: yarn build - run: npm publish ================================================ FILE: .gitignore ================================================ dist node_modules ================================================ FILE: .swcrc ================================================ { "$schema": "https://json.schemastore.org/swcrc", "jsc": { "parser": { "syntax": "typescript" }, "target": "es2020" } } ================================================ FILE: .vscode/settings.json ================================================ { "editor.codeActionsOnSave": { "source.fixAll": true, "source.organizeImports": true }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: CHANGELOG.md ================================================ # Changelog ### 7.0.0 > **This release requires mongoose 6.x** - updated mongoose to version 6.x ### 6.0.0 > **This release requires mongoose ~5.8** - updated mongoose to version 5.8 ### 5.0.0 - dropped support for Node 4 and added support for Node 10 - removed query operator parsing [#285](https://github.com/florianholzapfel/express-restify-mongoose/issues/285) - moved request query in req.erm.query [#299](https://github.com/florianholzapfel/express-restify-mongoose/issues/299) [#353](https://github.com/florianholzapfel/express-restify-mongoose/issues/353) - removed `next` from postProcess [#334](https://github.com/florianholzapfel/express-restify-mongoose/issues/334) - added error when skip and/or limit is not a valid integer - removed `_id` tinkering [#326](https://github.com/florianholzapfel/express-restify-mongoose/issues/326) - removed dependency on `async` ### 4.3.0 - added support for async `outputFn` by returning a Promise ### 4.2.2 - removed dependency on `lodash`, use specific modules and native methods when possible [#352](https://github.com/florianholzapfel/express-restify-mongoose/pull/352) ### 4.2.1 - fixed issue [#294](https://github.com/florianholzapfel/express-restify-mongoose/issues/294) ### 4.2.0 - removed `compile` step, code now runs natively on Node 4+ and `babel` is only used for coverage ### 4.1.1 - fixed `distinct` queries when `options.totalCountHeader` is enabled ### 4.1.0 - improved sync error handling in `buildQuery` by wrapping in a promise - fixed crash when `distinct` and `sort` operators were used in the same query ### 4.0.0 - improved default error middleware by serializing error objects and removing stack traces - fixed Mongoose async middleware error propagation - fixed requests to always set `req.erm.statusCode` - removed `statusCode` from error object and response body - removed undocumented `outputFn` parameter, use `req.erm.result` and `req.erm.statusCode` ### 3.2.0 - added an option to disable regex operations ([#195](https://github.com/florianholzapfel/express-restify-mongoose/issues/195)) - fixed queries with an `idProperty` resulting in a `CastError` to return `404` instead of `400` ([#184](https://github.com/florianholzapfel/express-restify-mongoose/issues/184)) - fixed query parser to handle geospatial operators ([#187](https://github.com/florianholzapfel/express-restify-mongoose/issues/187)) ### 3.1.0 - critical security fix with the `distinct` operator, see [issue #252](https://github.com/florianholzapfel/express-restify-mongoose/issues/252) for details ### 3.0.0 - ported source to ES2015, compiled and published as ES5 with Babel - document filtering is now done right before output allowing access to the full document in post middleware - removed `options.lowercase` and `options.plural`, use `options.name = require('inflection').pluralize('modelName').toLowerCase()` ### 2.0.0 - changed `serve` to no longer returns an Express 4 router, now returns the resource's base path (ie.: `/api/v1/Customer`) - changed `options.private` and `options.protected` to no longer accept comma separated fields, pass an array instead - removed `options.excluded`, use `options.private` - removed support for querying directly with query parameters, use `url?query={"name":"hello"}` - removed $and and $or query parameters, use `url?query={"$or":[...]}` - removed `prereq`, use `preMiddleware` instead - changed `postCreate`, `postUpdate`, and `postDelete` signatures to `(req, res, next)` - deprecated `outputFn`'s `data` parameter, data now available on `req.erm.result` and `req.erm.statusCode` ### 1.0.0 > **This release requires mongoose ~4** - updated mongoose to version 4 - removed `fullErrors`, implement a custom `onError` handler instead - removed `strict` option, allows DELETE without id and POST with id, disallows PUT without id - async `prereq` and `access` now use the standard `(err, data)` callback signature - `access` will throw an exception when an unsupported value is passed - changed `outputFn`'s signature to: `(req, res, { result: result, statusCode: statusCode })` ### 0.7.0 > **This release requires mongoose ~3** ================================================ FILE: LICENSE ================================================ Copyright (C) 2013 by Florian Holzapfel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # express-restify-mongoose Easily create a flexible REST interface for mongoose models. [![Build Status](https://github.com/florianholzapfel/express-restify-mongoose/actions/workflows/node.js.yml/badge.svg)](https://github.com/florianholzapfel/express-restify-mongoose/actions/workflows/node.js.yml) [![npm version](https://badge.fury.io/js/express-restify-mongoose.svg)](https://badge.fury.io/js/express-restify-mongoose) ## Getting started ```sh npm install express-restify-mongoose --save ``` ## Documentation [https://florianholzapfel.github.io/express-restify-mongoose/](https://florianholzapfel.github.io/express-restify-mongoose/) ## Compatibility | This library | Mongoose | MongoDB | NodeJS | ------------ | ----------- | ----------- | -------- | >= 9.0.0 | 6.x - 8.x | 3.6.x - 7.x | >=16 | >= 8.0.0 | 6.x | 3.6.x - 6.x | >=18 | >= 7.0.0 | 6.x | 3.6.x - 6.x | >=14 | >= 6.0.0 | 5.8.0 - 6.x | 3.6.x - 6.x | >=6 | >= 1.0.0 | 4.x | 2.4 - 3.4 | >=0.10 | 0.7.5 | 3.x | 2.4 - 3.0 | * ## Contributing Found a bug or have a suggestion to make? Have a took at [issues or open a new one](https://github.com/florianholzapfel/express-restify-mongoose/issues). Everyone is welcome to contribute code by [creating a pull request](https://github.com/florianholzapfel/express-restify-mongoose/pulls), just make sure to follow [standard style](https://github.com/feross/standard). Many thanks to all [contributors](https://github.com/florianholzapfel/express-restify-mongoose/graphs/contributors)! ## License (MIT) ``` Copyright (C) 2013 by Florian Holzapfel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ================================================ FILE: examples/invoicing.js ================================================ import bodyParser from "body-parser"; import express from "express"; import http from "http"; import methodOverride from "method-override"; import mongoose, { Schema } from "mongoose"; import { serve } from "../dist/express-restify-mongoose.js"; mongoose.connect("mongodb://localhost/database", { useMongoClient: true, }); const Customer = new Schema({ name: { type: String, required: true }, comment: { type: String }, }); const CustomerModel = mongoose.model("Customer", Customer); const Invoice = new Schema({ customer: { type: Schema.Types.ObjectId, ref: "Customer" }, amount: { type: Number, required: true }, }); const InvoiceModel = mongoose.model("Invoice", Invoice); const app = express(); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(methodOverride("X-HTTP-Method-Override")); serve(app, CustomerModel); serve(app, InvoiceModel); http.createServer(app).listen(3000, function () { // eslint-disable-next-line no-undef console.log("Express server listening on port 3000"); }); ================================================ FILE: express.d.ts ================================================ import type { Document, Model } from "mongoose"; import type { QueryOptions } from "./src/getQuerySchema"; import type { Access } from "./src/types"; declare global { namespace Express { export interface Request { access: Access; erm: { document?: Document; model?: Model; query?: QueryOptions; result?: Record | Record[]; statusCode?: number; totalCount?: number; }; } } } ================================================ FILE: index.d.ts ================================================ declare module "lodash.isplainobject" { export default function isPlainObject( o: unknown ): o is Record; } ================================================ FILE: package.json ================================================ { "name": "express-restify-mongoose", "version": "9.0.10", "description": "Easily create a flexible REST interface for mongoose models", "keywords": [ "ReST", "express", "restify", "mongodb", "mongoose", "model" ], "homepage": "http://florianholzapfel.github.io/express-restify-mongoose/", "bugs": { "url": "https://github.com/florianholzapfel/express-restify-mongoose/issues" }, "repository": { "type": "git", "url": "git://github.com/florianholzapfel/express-restify-mongoose.git" }, "license": "MIT", "author": { "name": "Florian Holzapfel", "email": "flo.holzapfel@gmail.com" }, "main": "./dist/express-restify-mongoose.js", "files": [ "dist/" ], "scripts": { "build": "run-p build:*", "build:cjs": "swc src --config module.type=commonjs --out-dir dist", "build:dts": "tsup src/express-restify-mongoose.ts --dts-only", "lint": "eslint . --ext .js,.ts", "test": "run-s test:*", "test:express": "mocha -R spec ./test/express.mjs --timeout 10s", "test:filter": "mocha -R spec ./test/integration/resource_filter.mjs --timeout 10s", "test:restify": "mocha -R spec ./test/restify.mjs --timeout 10s", "test:unit": "mocha -R spec ./test/unit.mjs", "tsc": "tsc --noEmit" }, "dependencies": { "dot-prop": "^6.0.0", "lodash.isplainobject": "~4.0.6", "mongoose": "^8.2.1", "serialize-error": "^8.1.0", "zod": "^3.19.1" }, "devDependencies": { "@swc/cli": "^0.1.57", "@swc/core": "^1.3.17", "@swc/register": "^0.1.10", "@types/express": "^4.17.14", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", "body-parser": "^1.19.0", "esbuild": "^0.15.12", "eslint": "^8.25.0", "eslint-plugin-mocha": "10.3.0", "express": "^4.17.1", "method-override": "^3.0.0", "mocha": "^10.1.0", "npm-run-all": "^4.1.5", "prettier": "^2.7.1", "request": "^2.88.2", "restify": "^4.3.4", "sinon": "^14.0.1", "tsup": "^6.3.0", "typescript": "^4.8.4" }, "engines": { "node": ">=16" } } ================================================ FILE: src/buildQuery.ts ================================================ import mongoose from "mongoose"; import { QueryOptions } from "./getQuerySchema.js"; import { Options } from "./types"; export function getBuildQuery( options: Pick ) { return function buildQuery( query: mongoose.Query, queryOptions: QueryOptions | undefined ): Promise { const promise = new Promise((resolve) => { if (!queryOptions) { return resolve(query); } if (queryOptions.query) { query.where(queryOptions.query); } if (queryOptions.skip) { query.skip(queryOptions.skip); } if ( options.limit && (!queryOptions.limit || queryOptions.limit > options.limit) ) { queryOptions.limit = options.limit; } if ( queryOptions.limit && // @ts-expect-error this is fine 🐶🔥 query.op !== "countDocuments" && !queryOptions.distinct ) { query.limit(queryOptions.limit); } if (queryOptions.sort) { // Necessary to support Mongoose 8 if (typeof queryOptions.sort === 'object') { Object.keys(queryOptions.sort).forEach(key => { // @ts-expect-error this is fine 🐶🔥 queryOptions.sort[key] = Number(queryOptions.sort[key]); }); } // @ts-expect-error this is fine 🐶🔥 query.sort(queryOptions.sort); } if (queryOptions.populate) { // @ts-expect-error this is fine 🐶🔥 query.populate(queryOptions.populate); } if (queryOptions.select) { query.select(queryOptions.select); } if (queryOptions.distinct) { query.distinct(queryOptions.distinct); } if (options.readPreference) { query.read(options.readPreference); } if (options.lean) { query.lean(options.lean); } resolve(query); }); return promise as Promise; }; } ================================================ FILE: src/detective.ts ================================================ import mongoose from "mongoose"; export function detective(model: mongoose.Model, path: string) { const keys = path.split("."); let schema = model.schema; let schemaPath = ""; for (let i = 0, length = keys.length; i < length; i++) { if (schemaPath.length > 0) { schemaPath += "."; } schemaPath += keys[i]; if (schema.path(schemaPath) && schema.path(schemaPath).schema) { schema = schema.path(schemaPath).schema; } } if (!schema) { return; } // @ts-expect-error this is fine 🐶🔥 schemaPath = schema.path(keys[keys.length - 1]) || schema.path(schemaPath); if (!schemaPath && (!model || !model.discriminators)) { return; } // @ts-expect-error this is fine 🐶🔥 if (schemaPath.caster && schemaPath.caster.options) { // @ts-expect-error this is fine 🐶🔥 return schemaPath.caster.options.ref; // @ts-expect-error this is fine 🐶🔥 } else if (schemaPath.options) { // @ts-expect-error this is fine 🐶🔥 return schemaPath.options.ref; } } ================================================ FILE: src/errorHandler.ts ================================================ import { ErrorRequestHandler } from "express"; import { STATUS_CODES } from "http"; import { Options } from "./types"; export function getErrorHandler( options: Pick ) { const fn: ErrorRequestHandler = function errorHandler(err, req, res, next) { if ( err.message === STATUS_CODES[404] || (req.params?.id && err.path === options.idProperty && err.name === "CastError") ) { req.erm.statusCode = 404; } else { req.erm.statusCode = req.erm.statusCode && req.erm.statusCode >= 400 ? req.erm.statusCode : 400; } options.onError(err, req, res, next); }; return fn; } ================================================ FILE: src/express-restify-mongoose.ts ================================================ import { Application } from "express"; import mongoose from "mongoose"; import { deprecate } from "util"; import { getAccessHandler } from "./middleware/access.js"; import { getEnsureContentTypeHandler } from "./middleware/ensureContentType.js"; import { getFilterAndFindByIdHandler } from "./middleware/filterAndFindById.js"; import { getOnErrorHandler } from "./middleware/onError.js"; import { getOutputFnHandler } from "./middleware/outputFn.js"; import { getPrepareOutputHandler } from "./middleware/prepareOutput.js"; import { getPrepareQueryHandler } from "./middleware/prepareQuery.js"; import { operations } from "./operations.js"; import { Filter } from "./resource_filter.js"; import { Options } from "./types"; const defaultOptions: Omit = { prefix: "/api", version: "/v1", idProperty: "_id", restify: false, allowRegex: true, runValidators: false, readPreference: "primary", totalCountHeader: false, private: [], protected: [], lean: true, findOneAndUpdate: true, findOneAndRemove: true, upsert: false, preMiddleware: [], preCreate: [], preRead: [], preUpdate: [], preDelete: [], updateDeep: true, }; const filter = new Filter(); export function serve( app: Application, model: mongoose.Model, options: Partial = {} ) { const serveOptions: Options = { ...defaultOptions, name: typeof options.name === "string" ? options.name : model.modelName, contextFilter: (model, req, done) => done(model), outputFn: getOutputFnHandler( typeof options.restify === "boolean" ? !options.restify : !defaultOptions.restify ), onError: getOnErrorHandler( typeof options.restify === "boolean" ? !options.restify : !defaultOptions.restify ), ...options, }; model.schema.eachPath((name, path) => { if (path.options.access) { switch (path.options.access.toLowerCase()) { case "private": serveOptions.private.push(name); break; case "protected": serveOptions.protected.push(name); break; } } }); filter.add(model, { filteredKeys: { private: serveOptions.private, protected: serveOptions.protected, }, }); const ops = operations(model, serveOptions, filter); let uriItem = `${serveOptions.prefix}${serveOptions.version}/${serveOptions.name}`; if (uriItem.indexOf("/:id") === -1) { uriItem += "/:id"; } const uriItems = uriItem.replace("/:id", ""); const uriCount = uriItems + "/count"; const uriShallow = uriItem + "/shallow"; if (typeof app.delete === "undefined") { // @ts-expect-error restify app.delete = app.del; } // @ts-expect-error restify const modelMiddleware = async (req, res, next) => { const getModel = serveOptions?.modelFactory?.getModel; req.erm = { model: typeof getModel === 'function' ? await getModel(req) : model, }; next(); }; const accessMiddleware = serveOptions.access ? getAccessHandler({ access: serveOptions.access, idProperty: serveOptions.idProperty, onError: serveOptions.onError, }) : []; const ensureContentType = getEnsureContentTypeHandler(serveOptions); const filterAndFindById = getFilterAndFindByIdHandler(serveOptions); const prepareQuery = getPrepareQueryHandler(serveOptions); const prepareOutput = getPrepareOutputHandler( serveOptions, model.modelName, filter ); app.get( uriItems, modelMiddleware, prepareQuery, serveOptions.preMiddleware, serveOptions.preRead, accessMiddleware, ops.getItems, prepareOutput ); app.get( uriCount, modelMiddleware, prepareQuery, serveOptions.preMiddleware, serveOptions.preRead, accessMiddleware, ops.getCount, prepareOutput ); app.get( uriItem, modelMiddleware, prepareQuery, serveOptions.preMiddleware, serveOptions.preRead, accessMiddleware, ops.getItem, prepareOutput ); app.get( uriShallow, modelMiddleware, prepareQuery, serveOptions.preMiddleware, serveOptions.preRead, accessMiddleware, ops.getShallow, prepareOutput ); app.post( uriItems, modelMiddleware, prepareQuery, ensureContentType, serveOptions.preMiddleware, serveOptions.preCreate, accessMiddleware, ops.createObject, prepareOutput ); app.post( uriItem, modelMiddleware, deprecate( prepareQuery, "express-restify-mongoose: in a future major version, the POST method to update resources will be removed. Use PATCH instead." ), ensureContentType, serveOptions.preMiddleware, serveOptions.findOneAndUpdate ? [] : filterAndFindById, serveOptions.preUpdate, accessMiddleware, ops.modifyObject, prepareOutput ); app.put( uriItem, modelMiddleware, deprecate( prepareQuery, "express-restify-mongoose: in a future major version, the PUT method will replace rather than update a resource. Use PATCH instead." ), ensureContentType, serveOptions.preMiddleware, serveOptions.findOneAndUpdate ? [] : filterAndFindById, serveOptions.preUpdate, accessMiddleware, ops.modifyObject, prepareOutput ); app.patch( uriItem, modelMiddleware, prepareQuery, ensureContentType, serveOptions.preMiddleware, serveOptions.findOneAndUpdate ? [] : filterAndFindById, serveOptions.preUpdate, accessMiddleware, ops.modifyObject, prepareOutput ); app.delete( uriItems, modelMiddleware, prepareQuery, serveOptions.preMiddleware, serveOptions.preDelete, ops.deleteItems, prepareOutput ); app.delete( uriItem, modelMiddleware, prepareQuery, serveOptions.preMiddleware, serveOptions.findOneAndRemove ? [] : filterAndFindById, serveOptions.preDelete, ops.deleteItem, prepareOutput ); return uriItems; } ================================================ FILE: src/getQuerySchema.ts ================================================ import { z } from "zod"; const PopulateOptionsSchema = z.object({ path: z.string(), match: z.record(z.unknown()).optional(), options: z.record(z.unknown()).optional(), select: z.string().optional(), populate: z.record(z.unknown()).optional(), // Configure populate query to not use strict populate to maintain // behavior from Mongoose previous to v6 (unless already configured) strictPopulate: z.boolean().optional().default(false), }); const PopulateSchema = z.preprocess((value) => { if (typeof value === "string") { if (value.startsWith("{")) { return JSON.parse(`[${value}]`); } if (value.startsWith("[")) { return JSON.parse(value); } return value; } return Array.isArray(value) ? value : [value]; }, z.union([z.string(), z.array(PopulateOptionsSchema)])); const SelectSchema = z.preprocess((value) => { const fieldToRecord = (field: string) => { if (field.startsWith("-")) { return [field.substring(1), 0]; } return [field, 1]; }; if (typeof value === "string") { if (value.startsWith("{")) { return JSON.parse(value); } return Object.fromEntries( value.split(",").filter(Boolean).map(fieldToRecord) ); } if (Array.isArray(value)) { return Object.fromEntries(value.map(fieldToRecord)); } return value; }, z.record(z.number().min(0).max(1))); const SortSchema = z.preprocess((value) => { if (typeof value === "string" && value.startsWith("{")) { return JSON.parse(value); } return value; }, z.union([z.string(), z.record(z.enum(["asc", "desc", "ascending", "descending", "-1", "1"])), z.record(z.number().min(-1).max(1))])); const LimitSkipSchema = z.preprocess((value) => { if (typeof value !== "string") { return value; } return Number(value); }, z.number()); export function getQueryOptionsSchema({ allowRegex }: { allowRegex: boolean }) { const QuerySchema = z .preprocess((value) => { if (!allowRegex && `${value}`.toLowerCase().includes("$regex")) { throw new Error("regex_not_allowed"); } if (typeof value !== "string") { return value; } return JSON.parse(value); }, z.record(z.unknown())) .transform((value) => { return Object.fromEntries( Object.entries(value).map(([key, value]) => { if (Array.isArray(value) && !key.startsWith("$")) { return [key, { $in: value }]; } return [key, value]; }) ); }); return z .object({ query: QuerySchema.optional(), populate: PopulateSchema.optional(), select: SelectSchema.optional(), sort: SortSchema.optional(), limit: LimitSkipSchema.optional(), skip: LimitSkipSchema.optional(), distinct: z.string().optional(), }) .transform((value) => { if (typeof value.populate === "undefined") { return value; } const populate = typeof value.populate === "string" ? value.populate .split(",") .filter(Boolean) .map>((field) => { const pop: z.infer = { path: field, strictPopulate: false, }; if (!value.select) { return pop; } for (const [k, v] of Object.entries(value.select)) { if (k.startsWith(`${field}.`)) { if (pop.select) { pop.select += " "; } else { pop.select = ""; } if (v === 0) { pop.select += "-"; } pop.select += k.substring(field.length + 1); delete value.select[k]; } } // If other specific fields are selected, add the populated field if ( Object.keys(value.select).length > 0 && !value.select[field] ) { value.select[field] = 1; } return pop; }) : value.populate; return { ...value, populate, }; }) .transform((value) => { if ( !value.populate || (Array.isArray(value.populate) && value.populate.length === 0) ) { delete value.populate; } if (!value.select || Object.keys(value.select).length === 0) { delete value.select; } return value; }); } export type QueryOptions = z.infer>; ================================================ FILE: src/middleware/access.ts ================================================ import { RequestHandler } from "express"; import { getErrorHandler } from "../errorHandler.js"; import { Access, Options } from "../types"; export function getAccessHandler( options: Required> ) { const errorHandler = getErrorHandler(options); const fn: RequestHandler = function access(req, res, next) { const handler = function (access: Access) { if (!["public", "private", "protected"].includes(access)) { throw new Error( 'Unsupported access, must be "public", "private" or "protected"' ); } req.access = access; next(); }; const result = options.access(req); if (typeof result === "string") { handler(result); } else { result.then(handler).catch((err) => errorHandler(err, req, res, next)); } }; return fn; } ================================================ FILE: src/middleware/ensureContentType.ts ================================================ import { RequestHandler } from "express"; import { getErrorHandler } from "../errorHandler.js"; import { Options } from "../types"; export function getEnsureContentTypeHandler( options: Pick ) { const errorHandler = getErrorHandler(options); const fn: RequestHandler = function ensureContentType(req, res, next) { const contentType = req.headers["content-type"]; if (!contentType) { return errorHandler(new Error("missing_content_type"), req, res, next); } if (!contentType.includes("application/json")) { return errorHandler(new Error("invalid_content_type"), req, res, next); } next(); }; return fn; } ================================================ FILE: src/middleware/filterAndFindById.ts ================================================ import { RequestHandler } from "express"; import { STATUS_CODES } from "http"; import mongoose from "mongoose"; import { getErrorHandler } from "../errorHandler.js"; import { Options } from "../types"; export function getFilterAndFindByIdHandler( options: Pick< Options, "contextFilter" | "idProperty" | "onError" | "readPreference" > ) { const errorHandler = getErrorHandler(options); const fn: RequestHandler = function filterAndFindById(req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } if (!req.params.id) { return next(); } options.contextFilter(contextModel, req, (filteredContext) => { filteredContext // @ts-expect-error this is fine 🐶🔥 .findOne() .and({ [options.idProperty]: req.params.id, }) .lean(false) .read(options.readPreference || "p") .exec() .then((doc: mongoose.Document | null) => { if (!doc) { return errorHandler(new Error(STATUS_CODES[404]), req, res, next); } req.erm.document = doc; next(); }) .catch((err: Error) => errorHandler(err, req, res, next)); }); }; return fn; } ================================================ FILE: src/middleware/onError.ts ================================================ import { ErrorRequestHandler } from "express"; import { serializeError } from "serialize-error"; export function getOnErrorHandler(isExpress: boolean) { const fn: ErrorRequestHandler = function onError(err, req, res) { const serializedErr = serializeError(err); delete serializedErr.stack; if (serializedErr.errors) { for (const key in serializedErr.errors) { delete serializedErr.errors[key].reason; delete serializedErr.errors[key].stack; } } res.setHeader("Content-Type", "application/json"); if (isExpress) { res.status(req.erm.statusCode || 500).send(serializedErr); } else { // @ts-expect-error restify res.send(req.erm.statusCode, serializedErr); } }; return fn; } ================================================ FILE: src/middleware/outputFn.ts ================================================ import { OutputFn } from "../types"; export function getOutputFnHandler(isExpress: boolean) { const fn: OutputFn = function outputFn(req, res) { if (!req.erm.statusCode) { throw new Error("statusCode not set"); } if (isExpress) { if (req.erm.result) { res.status(req.erm.statusCode).json(req.erm.result); } else { res.sendStatus(req.erm.statusCode); } } else { // @ts-expect-error restify res.send(req.erm.statusCode, req.erm.result); } }; return fn; } ================================================ FILE: src/middleware/prepareOutput.ts ================================================ import { RequestHandler } from "express"; import { getErrorHandler } from "../errorHandler.js"; import { Filter } from "../resource_filter.js"; import { Options } from "../types"; function isDefined(arg: T | undefined): arg is T { return typeof arg !== "undefined"; } export function getPrepareOutputHandler( options: Pick< Options, | "idProperty" | "onError" | "postCreate" | "postRead" | "postUpdate" | "postDelete" | "outputFn" | "postProcess" | "totalCountHeader" >, modelName: string, filter: Filter ) { const errorHandler = getErrorHandler(options); const fn: RequestHandler = function prepareOutput(req, res, next) { const postMiddleware = (() => { switch (req.method.toLowerCase()) { case "get": { return Array.isArray(options.postRead) ? options.postRead : [options.postRead]; } case "post": { if (req.erm.statusCode === 201) { return Array.isArray(options.postCreate) ? options.postCreate : [options.postCreate]; } return Array.isArray(options.postUpdate) ? options.postUpdate : [options.postUpdate]; } case "put": case "patch": { return Array.isArray(options.postUpdate) ? options.postUpdate : [options.postUpdate]; } case "delete": { return Array.isArray(options.postDelete) ? options.postDelete : [options.postDelete]; } default: { return []; } } })().filter(isDefined); const callback = () => { // TODO: this will, but should not, filter /count queries if (req.erm.result) { req.erm.result = filter.filterObject(req.erm.result, { access: req.access, modelName, // @ts-expect-error this is fine 🐶🔥 populate: req.erm.query?.populate, }); } if (options.totalCountHeader && typeof req.erm.totalCount === "number") { res.header( typeof options.totalCountHeader === "string" ? options.totalCountHeader : "X-Total-Count", `${req.erm.totalCount}` ); } const promise = options.outputFn(req, res); if (options.postProcess) { if (promise && typeof promise.then === "function") { promise .then(() => { options.postProcess?.(req, res); }) .catch((err) => errorHandler(err, req, res, next)); } else { options.postProcess(req, res); } } }; if (!postMiddleware || postMiddleware.length === 0) { return callback(); } postMiddleware .reduce(async (acc, middleware) => { await acc; return new Promise((resolve, reject) => { middleware(req, res, (err) => (err ? reject(err) : resolve(err))); }); }, Promise.resolve()) .then(callback) .catch((err) => errorHandler(err, req, res, next)); }; return fn; } ================================================ FILE: src/middleware/prepareQuery.ts ================================================ import { RequestHandler } from "express"; import { getErrorHandler } from "../errorHandler.js"; import { getQueryOptionsSchema } from "../getQuerySchema.js"; import { Options } from "../types"; export function getPrepareQueryHandler( options: Pick ) { const errorHandler = getErrorHandler(options); const fn: RequestHandler = function prepareQuery(req, res, next) { req.erm = req.erm || {}; try { req.erm.query = getQueryOptionsSchema({ allowRegex: options.allowRegex, }).parse(req.query || {}); next(); } catch (e) { return errorHandler(new Error("invalid_json_query"), req, res, next); } }; return fn; } ================================================ FILE: src/moredots.ts ================================================ import isPlainObject from "lodash.isplainobject"; export function moredots( src: Record, dst: Record = {}, prefix = "" ) { for (const [key, value] of Object.entries(src)) { if (isPlainObject(value)) { moredots(value, dst, `${prefix}${key}.`); } else { dst[`${prefix}${key}`] = value; } } return dst; } ================================================ FILE: src/operations.ts ================================================ import { Request, RequestHandler } from "express"; import { STATUS_CODES } from "http"; import isPlainObject from "lodash.isplainobject"; import mongoose from "mongoose"; import { getBuildQuery } from "./buildQuery.js"; import { getErrorHandler } from "./errorHandler.js"; import { moredots } from "./moredots.js"; import { Filter } from "./resource_filter.js"; import { Options } from "./types"; export function operations( model: mongoose.Model, options: Pick< Options, | "contextFilter" | "findOneAndRemove" | "findOneAndUpdate" | "idProperty" | "lean" | "limit" | "onError" | "readPreference" | "runValidators" | "totalCountHeader" | "upsert" | "updateDeep" >, filter: Filter ) { const buildQuery = getBuildQuery(options); const errorHandler = getErrorHandler(options); function findById(filteredContext: mongoose.Model, id: unknown) { return filteredContext.findOne().and([ { [options.idProperty]: id, }, ]); } function isDistinctExcluded(req: Request) { if (!req.erm.query?.distinct) { return false; } return filter .getExcluded({ access: req.access, modelName: model.modelName, }) .includes(req.erm.query.distinct); } const getItems: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } if (isDistinctExcluded(req)) { req.erm.result = []; req.erm.statusCode = 200; return next(); } options.contextFilter(contextModel, req, (filteredContext) => { buildQuery[]>( // @ts-expect-error this is fine 🐶🔥 filteredContext.find(), req.erm.query ) .then((items) => { req.erm.result = items; req.erm.statusCode = 200; if (options.totalCountHeader && !req.erm.query?.distinct) { options.contextFilter(contextModel, req, (countFilteredContext) => { buildQuery(countFilteredContext.countDocuments(), { ...req.erm.query, skip: 0, limit: 0, }) .then((count) => { req.erm.totalCount = count; next(); }) .catch((err) => errorHandler(err, req, res, next)); }); } else { next(); } }) .catch((err) => errorHandler(err, req, res, next)); }); }; const getCount: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } options.contextFilter(contextModel, req, (filteredContext) => { buildQuery(filteredContext.countDocuments(), req.erm.query) .then((count) => { req.erm.result = { count: count }; req.erm.statusCode = 200; next(); }) .catch((err) => errorHandler(err, req, res, next)); }); }; const getShallow: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } options.contextFilter(contextModel, req, (filteredContext) => { buildQuery | null>( // @ts-expect-error this is fine 🐶🔥 findById(filteredContext, req.params.id), req.erm.query ) .then((item) => { if (!item) { return errorHandler(new Error(STATUS_CODES[404]), req, res, next); } for (const prop in item) { item[prop] = typeof item[prop] === "object" && prop !== "_id" ? true : item[prop]; } req.erm.result = item; req.erm.statusCode = 200; next(); }) .catch((err) => errorHandler(err, req, res, next)); }); }; const deleteItems: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } options.contextFilter(contextModel, req, (filteredContext) => { buildQuery(filteredContext.deleteMany(), req.erm.query) .then(() => { req.erm.statusCode = 204; next(); }) .catch((err) => errorHandler(err, req, res, next)); }); }; const getItem: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } if (isDistinctExcluded(req)) { req.erm.result = []; req.erm.statusCode = 200; return next(); } options.contextFilter(contextModel, req, (filteredContext) => { buildQuery | null>( // @ts-expect-error this is fine 🐶🔥 findById(filteredContext, req.params.id), req.erm.query ) .then((item) => { if (!item) { return errorHandler(new Error(STATUS_CODES[404]), req, res, next); } req.erm.result = item; req.erm.statusCode = 200; next(); }) .catch((err) => errorHandler(err, req, res, next)); }); }; const deleteItem: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } if (options.findOneAndRemove) { options.contextFilter(contextModel, req, (filteredContext) => { // @ts-expect-error this is fine 🐶🔥 findById(filteredContext, req.params.id) .findOneAndDelete() // switched to findOneAndDelete to add support for Mongoose 7 and 8 .then((item) => { if (!item) { return errorHandler(new Error(STATUS_CODES[404]), req, res, next); } req.erm.statusCode = 204; next(); }) .catch((err: Error) => errorHandler(err, req, res, next)); }); } else { req.erm.document ?.deleteOne() // switched to deleteOne to add support for Mongoose 7 and 8 .then(() => { req.erm.statusCode = 204; next(); }) .catch((err: Error) => errorHandler(err, req, res, next)); } }; const createObject: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } req.body = filter.filterObject(req.body || {}, { access: req.access, modelName: model.modelName, // @ts-expect-error this is fine 🐶🔥 populate: req.erm.query?.populate, }); if (req.body._id === null) { delete req.body._id; } // @ts-expect-error this is fine 🐶🔥 if (contextModel.schema.options.versionKey) { // @ts-expect-error this is fine 🐶🔥 delete req.body[contextModel.schema.options.versionKey]; } contextModel .create(req.body) .then((item) => { // @ts-expect-error this is fine 🐶🔥 return contextModel.populate(item, req.erm.query?.populate || []); }) .then((item) => { req.erm.result = item as unknown as Record; req.erm.statusCode = 201; next(); }) .catch((err) => errorHandler(err, req, res, next)); }; const modifyObject: RequestHandler = function (req, res, next) { const contextModel = req.erm.model; if (!contextModel) { return errorHandler(new Error('Model is undefined.'), req, res, next); } req.body = filter.filterObject(req.body || {}, { access: req.access, modelName: model.modelName, // @ts-expect-error this is fine 🐶🔥 populate: req.erm.query?.populate, }); delete req.body._id; // @ts-expect-error this is fine 🐶🔥 if (contextModel.schema.options.versionKey) { // @ts-expect-error this is fine 🐶🔥 delete req.body[contextModel.schema.options.versionKey]; } function depopulate(src: Record) { const dst: Record = {}; for (const [key, value] of Object.entries(src)) { // @ts-expect-error this is fine 🐶🔥 const path = contextModel.schema.path(key); // @ts-expect-error this is fine 🐶🔥 // Add support for Mongoose 7 and 8 while keeping backwards-compatibility to 6 by allowing ObjectID and ObejctId if (path && path.caster && (path.caster.instance === "ObjectID" || path.caster.instance === "ObjectId")) { if (Array.isArray(value)) { for (let j = 0; j < value.length; ++j) { if (typeof value[j] === "object") { dst[key] = dst[key] || []; // @ts-expect-error this is fine 🐶🔥 dst[key].push(value[j]._id); } } } else if (isPlainObject(value)) { dst[key] = value._id; } } else if (isPlainObject(value)) { // Add support for Mongoose 7 and 8 while keeping backwards-compatibility to 6 by allowing ObjectID and ObejctId if (path && (path.instance === "ObjectID" || path.instance === "ObjectId")) { dst[key] = value._id; } else { dst[key] = depopulate(value); } } if (typeof dst[key] === "undefined") { dst[key] = value; } } return dst; } let cleanBody = depopulate(req.body); if (options.updateDeep) { cleanBody = moredots(cleanBody); } if (options.findOneAndUpdate) { options.contextFilter(contextModel, req, (filteredContext) => { // @ts-expect-error this is fine 🐶🔥 findById(filteredContext, req.params.id) .findOneAndUpdate( {}, { $set: cleanBody, }, { new: true, upsert: options.upsert, runValidators: options.runValidators, } ) .exec() .then((item) => { // @ts-expect-error this is fine 🐶🔥 return contextModel.populate(item, req.erm.query?.populate || []); }) .then((item) => { if (!item) { return errorHandler(new Error(STATUS_CODES[404]), req, res, next); } req.erm.result = item as unknown as Record; req.erm.statusCode = 200; next(); }) .catch((err) => errorHandler(err, req, res, next)); }); } else { for (const [key, value] of Object.entries(cleanBody)) { req.erm.document?.set(key, value); } req.erm.document ?.save() .then((item) => { // @ts-expect-error this is fine 🐶🔥 return contextModel.populate(item, req.erm.query?.populate || []); }) .then((item) => { req.erm.result = item as unknown as Record; req.erm.statusCode = 200; next(); }) .catch((err: Error) => errorHandler(err, req, res, next)); } }; return { getItems, getCount, getItem, getShallow, createObject, modifyObject, deleteItems, deleteItem, }; } ================================================ FILE: src/resource_filter.ts ================================================ import dotProp from "dot-prop"; import mongoose from "mongoose"; import { detective } from "./detective.js"; import { QueryOptions } from "./getQuerySchema.js"; import { Access, ExcludedMap, FilteredKeys } from "./types"; import { weedout } from "./weedout.js"; const { get: getProperty, has: hasProperty } = dotProp; // Because we're using an older version of dotProp that supports CommonJS export class Filter { excludedMap: ExcludedMap = new Map(); add( model: mongoose.Model, options: { filteredKeys: FilteredKeys; } ) { if (model.discriminators) { for (const modelName in model.discriminators) { const excluded = this.excludedMap.get(modelName); if (excluded) { options.filteredKeys.private = options.filteredKeys.private.concat( excluded.filteredKeys.private ); options.filteredKeys.protected = options.filteredKeys.protected.concat( excluded.filteredKeys.protected ); } } } this.excludedMap.set(model.modelName, { filteredKeys: options.filteredKeys, model, }); } /** * Gets excluded keys for a given model and access. */ getExcluded(options: { access: Access; modelName: string }) { if (options.access === "private") { return []; } const filteredKeys = this.excludedMap.get(options.modelName)?.filteredKeys; if (!filteredKeys) { return []; } return options.access === "protected" ? filteredKeys.private : filteredKeys.private.concat(filteredKeys.protected); } /** * Removes excluded keys from a document. */ private filterItem< T extends undefined | Record | Record[] >(item: T, excluded: string[]): T { if (!item) { return item; } if (Array.isArray(item)) { return item.map((i) => this.filterItem(i, excluded)) as T; } if (excluded) { if (typeof item.toObject === "function") { item = item.toObject(); } for (let i = 0; i < excluded.length; i++) { weedout(item as Record, excluded[i]); } } return item; } /** * Removes excluded keys from a document with populated subdocuments. */ private filterPopulatedItem< T extends Record | Record[] >( item: T, options: { access: Access; modelName: string; populate: Exclude; } ): T { if (Array.isArray(item)) { return item.map((i) => this.filterPopulatedItem(i, options)) as T; } for (let i = 0; i < options.populate.length; i++) { if (!options.populate[i].path) { continue; } const model = this.excludedMap.get(options.modelName)?.model; if (!model) { continue; } const excluded = this.getExcluded({ access: options.access, modelName: detective(model, options.populate[i].path), }); if (hasProperty(item, options.populate[i].path)) { this.filterItem( getProperty(item, options.populate[i].path) as T, excluded ); } else { const pathToArray = options.populate[i].path .split(".") .slice(0, -1) .join("."); if (hasProperty(item, pathToArray)) { const array = getProperty(item, pathToArray); const pathToObject = options.populate[i].path .split(".") .slice(-1) .join("."); if (Array.isArray(array)) { this.filterItem( // @ts-expect-error this is fine 🐶🔥 array.map((element) => getProperty(element, pathToObject)), excluded ); } } } } return item; } /** * Removes excluded keys from a document. */ filterObject( resource: Record | Record[], options: { access: Access; modelName: string; populate?: Exclude; } ) { const excluded = this.getExcluded({ access: options.access, modelName: options.modelName, }); const filtered = this.filterItem(resource, excluded); if (options?.populate) { this.filterPopulatedItem(filtered, { access: options.access, modelName: options.modelName, populate: options.populate, }); } return filtered; } } ================================================ FILE: src/types.ts ================================================ import { ErrorRequestHandler, Request, RequestHandler, Response, } from "express"; import mongoose from "mongoose"; export type Access = "private" | "protected" | "public"; export type FilteredKeys = { private: string[]; protected: string[]; }; export type ExcludedMap = Map< string, { filteredKeys: FilteredKeys; model: mongoose.Model; } >; export type OutputFn = (req: Request, res: Response) => void | Promise; export type ReadPreference = | "p" | "primary" | "pp" | "primaryPreferred" | "s" | "secondary" | "sp" | "secondaryPreferred" | "n" | "nearest"; export type Options = { prefix: `/${string}`; version: `/v${number}`; idProperty: string; restify: boolean; name?: string; allowRegex: boolean; runValidators: boolean; readPreference: ReadPreference; totalCountHeader: boolean | string; private: string[]; protected: string[]; lean: boolean; limit?: number; findOneAndRemove: boolean; findOneAndUpdate: boolean; upsert: boolean; preMiddleware: RequestHandler | RequestHandler[]; preCreate: RequestHandler | RequestHandler[]; preRead: RequestHandler | RequestHandler[]; preUpdate: RequestHandler | RequestHandler[]; preDelete: RequestHandler | RequestHandler[]; updateDeep: boolean; access?: (req: Request) => Access | Promise; contextFilter: ( model: mongoose.Model, req: Request, done: ( query: mongoose.Model | mongoose.Query ) => void ) => void; postCreate?: RequestHandler | RequestHandler[]; postRead?: RequestHandler | RequestHandler[]; postUpdate?: RequestHandler | RequestHandler[]; postDelete?: RequestHandler | RequestHandler[]; outputFn: OutputFn; postProcess?: (req: Request, res: Response) => void; onError: ErrorRequestHandler; modelFactory?: { getModel: (req: Request) => mongoose.Model; }; }; ================================================ FILE: src/weedout.ts ================================================ export function weedout(obj: Record, path: string) { const keys = path.split("."); for (let i = 0, length = keys.length; i < length; i++) { if (Array.isArray(obj)) { for (let j = 0; j < obj.length; j++) { weedout(obj[j], keys.slice(1).join(".")); } } else if (!obj || typeof obj[keys[i]] === "undefined") { return; } if (i < keys.length - 1) { // @ts-expect-error this is fine 🐶🔥 obj = obj[keys[i]]; } else { delete obj[keys[i]]; } } return obj; } ================================================ FILE: test/express.mjs ================================================ import bodyParser from "body-parser"; import express from "express"; import methodOverride from "method-override"; import accessTests from "./integration/access.mjs"; import contextFilterTests from "./integration/contextFilter.mjs"; import createTests from "./integration/create.mjs"; import deleteTests from "./integration/delete.mjs"; import hookTests from "./integration/hooks.mjs"; import middlewareTests from "./integration/middleware.mjs"; import optionsTests from "./integration/options.mjs"; import readTests from "./integration/read.mjs"; import updateTests from "./integration/update.mjs"; import virtualsTests from "./integration/virtuals.mjs"; import setupDb from "./integration/setup.mjs"; const db = setupDb(); function Express() { let app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(methodOverride()); return app; } function setup(callback) { db.initialize((err) => { if (err) { return callback(err); } db.reset(callback); }); } function dismantle(app, server, callback) { db.close((err) => { if (err) { return callback(err); } if (app.close) { return app.close(callback); } server.close(callback); }); } function runTests(createFn) { describe(createFn.name, () => { createTests(createFn, setup, dismantle); readTests(createFn, setup, dismantle); updateTests(createFn, setup, dismantle); deleteTests(createFn, setup, dismantle); accessTests(createFn, setup, dismantle); contextFilterTests(createFn, setup, dismantle); hookTests(createFn, setup, dismantle); middlewareTests(createFn, setup, dismantle); optionsTests(createFn, setup, dismantle); virtualsTests(createFn, setup, dismantle); }); } runTests(Express); ================================================ FILE: test/integration/access.mjs ================================================ import assert from "assert"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const updateMethods = ["PATCH", "POST", "PUT"]; describe("access", () => { describe("private - include private and protected fields", () => { let app = createFn(); let server; let product; let customer; let invoice; let account; let repeatCustomer; let repeatCustomerInvoice; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.RepeatCustomer, { private: ["job"], protected: ["status"], access: () => { return "private"; }, restify: app.isRestify, }); serve(app, db.models.Customer, { private: [ "age", "favorites.animal", "favorites.purchase.number", "purchases.number", "privateDoes.notExist", ], protected: ["comment", "favorites.color", "protectedDoes.notExist"], access: () => { return Promise.resolve("private"); }, restify: app.isRestify, }); serve(app, db.models.Invoice, { private: ["amount"], protected: ["receipt"], access: () => { return "private"; }, restify: app.isRestify, }); serve(app, db.models.Product, { private: ["department.code"], protected: ["price"], access: () => { return "private"; }, restify: app.isRestify, }); serve(app, db.models.Account, { private: ["accountNumber"], protected: ["points"], access: () => { return "private"; }, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Product.create({ name: "Bobsleigh", price: 42, department: { code: 51, }, }) .then((createdProduct) => { product = createdProduct; return db.models.Customer.create({ name: "Bob", age: 12, comment: "Boo", favorites: { animal: "Boar", color: "Black", purchase: { item: product._id, number: 1, }, }, purchases: [ { item: product._id, number: 2, }, ], returns: [product._id], }); }) .then((createdCustomer) => { customer = createdCustomer; return db.models.Invoice.create({ customer: customer._id, amount: 100, receipt: "A", }); }) .then((createdInvoice) => { invoice = createdInvoice; return db.models.Account.create({ accountNumber: "123XYZ", points: 244, }); }) .then((createdAccount) => { account = createdAccount; return db.models.RepeatCustomer.create({ account: account._id, name: "Mike", visits: 24, status: "Awesome", job: "Hunter", }); }) .then((createdRepeatCustomer) => { repeatCustomer = createdRepeatCustomer; return db.models.Invoice.create({ customer: repeatCustomer._id, amount: 200, receipt: "B", }); }) .then((createdRepeatCustomerInvoice) => { repeatCustomerInvoice = createdRepeatCustomerInvoice; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[0].age, 12); assert.equal(body[0].comment, "Boo"); assert.equal(body[0].purchases.length, 1); assert.deepEqual(body[0].favorites, { animal: "Boar", color: "Black", purchase: { item: product._id.toHexString(), number: 1, }, }); done(); } ); }); it("GET /Customer?distinct=age 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0], 12); done(); } ); }); it("GET /Customer?distinct=comment 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "comment", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0], "Boo"); done(); } ); }); it("GET /Customer/:id 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, 12); assert.equal(body.comment, "Boo"); assert.equal(body.purchases.length, 1); assert.deepEqual(body.favorites, { animal: "Boar", color: "Black", purchase: { item: product._id.toHexString(), number: 1, }, }); done(); } ); }); it("GET /Customer/:id?distinct=age 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { distinct: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0], 12); done(); } ); }); it("GET /Customer/:id?distinct=comment 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { distinct: "comment", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0], "Boo"); done(); } ); }); it("GET /Customer?populate=favorites.purchase.item,purchases.item,returns 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { populate: "favorites.purchase.item,purchases.item,returns", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[0].age, 12); assert.equal(body[0].comment, "Boo"); assert.deepEqual(body[0].favorites, { animal: "Boar", color: "Black", purchase: { item: { __v: 0, _id: product._id.toHexString(), name: "Bobsleigh", price: 42, department: { code: 51, }, }, number: 1, }, }); assert.equal(body[0].purchases.length, 1); assert.ok(body[0].purchases[0].item); assert.equal( body[0].purchases[0].item._id, product._id.toHexString() ); assert.equal(body[0].purchases[0].item.name, "Bobsleigh"); assert.equal(body[0].purchases[0].item.price, 42); assert.deepEqual(body[0].purchases[0].item.department, { code: 51, }); assert.equal(body[0].purchases[0].number, 2); assert.equal(body[0].returns.length, 1); assert.equal(body[0].returns[0].name, "Bobsleigh"); assert.equal(body[0].returns[0].price, 42); assert.deepEqual(body[0].returns[0].department, { code: 51, }); done(); } ); }); it("GET /Customer/:id?populate=favorites.purchase.item,purchases.item,returns 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { populate: "favorites.purchase.item,purchases.item,returns", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, 12); assert.equal(body.comment, "Boo"); assert.deepEqual(body.favorites, { animal: "Boar", color: "Black", purchase: { item: { __v: 0, _id: product._id.toHexString(), name: "Bobsleigh", price: 42, department: { code: 51, }, }, number: 1, }, }); assert.equal(body.purchases.length, 1); assert.ok(body.purchases[0].item); assert.equal(body.purchases[0].item._id, product._id.toHexString()); assert.equal(body.purchases[0].item.name, "Bobsleigh"); assert.equal(body.purchases[0].item.price, 42); assert.deepEqual(body.purchases[0].item.department, { code: 51, }); assert.equal(body.purchases[0].number, 2); assert.equal(body.returns.length, 1); assert.equal(body.returns[0].name, "Bobsleigh"); assert.equal(body.returns[0].price, 42); assert.deepEqual(body.returns[0].department, { code: 51, }); done(); } ); }); it("GET /Invoice?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.ok(body[0].customer); assert.equal(body[0].amount, 100); assert.equal(body[0].receipt, "A"); assert.equal(body[0].customer.name, "Bob"); assert.equal(body[0].customer.age, 12); assert.equal(body[0].customer.comment, "Boo"); assert.equal(body[0].customer.purchases.length, 1); assert.deepEqual(body[0].customer.favorites, { animal: "Boar", color: "Black", purchase: { item: product._id.toHexString(), number: 1, }, }); done(); } ); }); it("GET /Invoice/:id?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice/${invoice._id}`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.amount, 100); assert.equal(body.receipt, "A"); assert.equal(body.customer.name, "Bob"); assert.equal(body.customer.age, 12); assert.equal(body.customer.comment, "Boo"); assert.equal(body.customer.purchases.length, 1); assert.deepEqual(body.customer.favorites, { animal: "Boar", color: "Black", purchase: { item: product._id.toHexString(), number: 1, }, }); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id - saves all fields`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "John", age: 24, comment: "Jumbo", favorites: { animal: "Jaguar", color: "Jade", purchase: { number: 2, }, }, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "John"); assert.equal(body.age, 24); assert.equal(body.comment, "Jumbo"); assert.equal(body.purchases.length, 1); assert.deepEqual(body.favorites, { animal: "Jaguar", color: "Jade", purchase: { item: product._id.toHexString(), number: 2, }, }); done(); } ); }); it(`${method} /Customer/:id - saves all fields (falsy values)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { age: 0, comment: "", favorites: { animal: "", color: "", purchase: { number: 0, }, }, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, 0); assert.equal(body.comment, ""); assert.equal(body.purchases.length, 1); assert.deepEqual(body.favorites, { animal: "", color: "", purchase: { item: product._id.toHexString(), number: 0, }, }); done(); } ); }); }); it("GET /RepeatCustomer 200 - discriminator", (done) => { request.get( { url: `${testUrl}/api/v1/RepeatCustomer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].account, account._id.toHexString()); assert.equal(body[0].name, "Mike"); assert.equal(body[0].visits, 24); assert.equal(body[0].status, "Awesome"); assert.equal(body[0].job, "Hunter"); done(); } ); }); it("GET /RepeatCustomer/:id?populate=account 200 - populate discriminator field from base schema", (done) => { request.get( { url: `${testUrl}/api/v1/RepeatCustomer/${repeatCustomer._id}`, qs: { populate: "account", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.account); assert.equal(body.account._id, account._id.toHexString()); assert.equal(body.account.accountNumber, "123XYZ"); assert.equal(body.account.points, 244); assert.equal(body.name, "Mike"); assert.equal(body.visits, 24); assert.equal(body.status, "Awesome"); assert.equal(body.job, "Hunter"); done(); } ); }); it("GET /Invoice/:id?populate=customer 200 - populated discriminator", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice/${repeatCustomerInvoice._id}`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.amount, 200); assert.equal(body.receipt, "B"); assert.equal(body.customer.name, "Mike"); assert.equal(body.customer.visits, 24); assert.equal(body.customer.status, "Awesome"); assert.equal(body.customer.job, "Hunter"); done(); } ); }); }); describe("protected - exclude private fields and include protected fields", () => { let app = createFn(); let server; let product; let customer; let invoice; let account; let repeatCustomer; let repeatCustomerInvoice; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.RepeatCustomer, { private: ["job"], protected: ["status"], access: () => { return "protected"; }, restify: app.isRestify, }); serve(app, db.models.Customer, { private: [ "age", "favorites.animal", "favorites.purchase.number", "purchases.number", "privateDoes.notExist", ], protected: ["comment", "favorites.color", "protectedDoes.notExist"], access: () => { return Promise.resolve("protected"); }, restify: app.isRestify, }); serve(app, db.models.Invoice, { private: ["amount"], protected: ["receipt"], access: () => { return "protected"; }, restify: app.isRestify, }); serve(app, db.models.Product, { private: ["department.code"], protected: ["price"], access: () => { return "protected"; }, restify: app.isRestify, }); serve(app, db.models.Account, { private: ["accountNumber"], protected: ["points"], access: () => { return "protected"; }, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Product.create({ name: "Bobsleigh", price: 42, department: { code: 51, }, }) .then((createdProduct) => { product = createdProduct; return db.models.Customer.create({ name: "Bob", age: 12, comment: "Boo", favorites: { animal: "Boar", color: "Black", purchase: { item: product._id, number: 1, }, }, purchases: [ { item: product._id, number: 2, }, ], returns: [product._id], }); }) .then((createdCustomer) => { customer = createdCustomer; return db.models.Invoice.create({ customer: customer._id, amount: 100, receipt: "A", }); }) .then((createdInvoice) => { invoice = createdInvoice; return db.models.Account.create({ accountNumber: "123XYZ", points: 244, }); }) .then((createdAccount) => { account = createdAccount; return db.models.RepeatCustomer.create({ account: account._id, name: "Mike", visits: 24, status: "Awesome", job: "Hunter", }); }) .then((createdRepeatCustomer) => { repeatCustomer = createdRepeatCustomer; return db.models.Invoice.create({ customer: repeatCustomer._id, amount: 200, receipt: "B", }); }) .then((createdRepeatCustomerInvoice) => { repeatCustomerInvoice = createdRepeatCustomerInvoice; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[0].age, undefined); assert.equal(body[0].comment, "Boo"); assert.deepEqual(body[0].favorites, { color: "Black", purchase: { item: product._id.toHexString(), }, }); done(); } ); }); it("GET /Customer?distinct=age 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 0); done(); } ); }); it("GET /Customer?distinct=comment 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "comment", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0], "Boo"); done(); } ); }); it("GET /Customer/:id 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, undefined); assert.equal(body.comment, "Boo"); assert.deepEqual(body.favorites, { color: "Black", purchase: { item: product._id.toHexString(), }, }); done(); } ); }); it("GET /Customer/:id?distinct=age 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { distinct: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 0); done(); } ); }); it("GET /Customer/:id?distinct=comment 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { distinct: "comment", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0], "Boo"); done(); } ); }); it("GET /Customer?populate=favorites.purchase.item,purchases.item,returns 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { populate: "favorites.purchase.item,purchases.item,returns", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[0].age, undefined); assert.equal(body[0].comment, "Boo"); assert.deepEqual(body[0].favorites, { color: "Black", purchase: { item: { __v: 0, _id: product._id.toHexString(), name: "Bobsleigh", price: 42, department: {}, }, }, }); assert.equal(body[0].purchases.length, 1); assert.ok(body[0].purchases[0].item); assert.equal( body[0].purchases[0].item._id, product._id.toHexString() ); assert.equal(body[0].purchases[0].item.name, "Bobsleigh"); assert.equal(body[0].purchases[0].item.price, 42); assert.deepEqual(body[0].purchases[0].item.department, {}); assert.equal(body[0].purchases[0].number, undefined); assert.equal(body[0].returns.length, 1); assert.equal(body[0].returns[0].name, "Bobsleigh"); assert.equal(body[0].returns[0].price, 42); assert.deepEqual(body[0].returns[0].department, {}); done(); } ); }); it("GET /Customer/:id?populate=favorites.purchase.item,purchases.item,returns 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { populate: "favorites.purchase.item,purchases.item,returns", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, undefined); assert.equal(body.comment, "Boo"); assert.deepEqual(body.favorites, { color: "Black", purchase: { item: { __v: 0, _id: product._id.toHexString(), name: "Bobsleigh", price: 42, department: {}, }, }, }); assert.equal(body.purchases.length, 1); assert.ok(body.purchases[0].item); assert.equal(body.purchases[0].item._id, product._id.toHexString()); assert.equal(body.purchases[0].item.name, "Bobsleigh"); assert.equal(body.purchases[0].item.price, 42); assert.deepEqual(body.purchases[0].item.department, {}); assert.equal(body.purchases[0].number, undefined); assert.equal(body.returns.length, 1); assert.equal(body.returns[0].name, "Bobsleigh"); assert.equal(body.returns[0].price, 42); assert.deepEqual(body.returns[0].department, {}); done(); } ); }); it("GET /Invoice?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.ok(body[0].customer); assert.equal(body[0].amount, undefined); assert.equal(body[0].receipt, "A"); assert.equal(body[0].customer.name, "Bob"); assert.equal(body[0].customer.age, undefined); assert.equal(body[0].customer.comment, "Boo"); assert.deepEqual(body[0].customer.favorites, { color: "Black", purchase: { item: product._id.toHexString(), }, }); done(); } ); }); it("GET /Invoice/:id?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice/${invoice._id}`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.amount, undefined); assert.equal(body.receipt, "A"); assert.equal(body.customer.name, "Bob"); assert.equal(body.customer.age, undefined); assert.equal(body.customer.comment, "Boo"); assert.deepEqual(body.customer.favorites, { color: "Black", purchase: { item: product._id.toHexString(), }, }); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id - saves protected and public fields`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "John", age: 24, comment: "Jumbo", favorites: { animal: "Jaguar", color: "Jade", purchase: { number: 2, }, }, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "John"); assert.equal(body.age, undefined); assert.equal(body.comment, "Jumbo"); assert.deepEqual(body.favorites, { color: "Jade", purchase: { item: product._id.toHexString(), }, }); db.models.Customer.findById(customer._id) .then((customer) => { assert.equal(customer.age, 12); assert.deepEqual(customer.favorites.toObject(), { animal: "Boar", color: "Jade", purchase: { item: product._id, number: 1, }, }); done(); }) .catch(done); } ); }); it(`${method} /Customer/:id - saves protected and public fields (falsy values)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { age: 0, comment: "", favorites: { animal: "", color: "", purchase: { number: 0, }, }, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, undefined); assert.equal(body.comment, ""); assert.deepEqual(body.favorites, { color: "", purchase: { item: product._id.toHexString(), }, }); db.models.Customer.findById(customer._id) .then((foundCustomer) => { assert.equal(foundCustomer.age, 12); assert.deepEqual(foundCustomer.favorites.toObject(), { animal: "Boar", color: "", purchase: { item: product._id, number: 1, }, }); done(); }) .catch(done); } ); }); }); it("GET /RepeatCustomer 200 - discriminator", (done) => { request.get( { url: `${testUrl}/api/v1/RepeatCustomer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body[0].name, "Mike"); assert.equal(body[0].visits, 24); assert.equal(body[0].status, "Awesome"); assert.equal(body[0].job, undefined); done(); } ); }); it("GET /RepeatCustomer/:id?populate=account 200 - populate discriminator field from base schema", (done) => { request.get( { url: `${testUrl}/api/v1/RepeatCustomer/${repeatCustomer._id}`, qs: { populate: "account", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.account); assert.equal(body.account._id, account._id.toHexString()); assert.equal(body.account.accountNumber, undefined); assert.equal(body.account.points, 244); assert.equal(body.name, "Mike"); assert.equal(body.visits, 24); assert.equal(body.status, "Awesome"); assert.equal(body.job, undefined); done(); } ); }); it("GET /Invoice/:id?populate=customer 200 - populated discriminator", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice/${repeatCustomerInvoice._id}`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.amount, undefined); assert.equal(body.receipt, "B"); assert.equal(body.customer.name, "Mike"); assert.equal(body.customer.visits, 24); assert.equal(body.customer.status, "Awesome"); assert.equal(body.customer.job, undefined); done(); } ); }); }); describe("public - exclude private and protected fields", () => { let app = createFn(); let server; let product; let customer; let invoice; let account; let repeatCustomer; let repeatCustomerInvoice; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.RepeatCustomer, { private: ["job"], protected: ["status"], restify: app.isRestify, }); serve(app, db.models.Customer, { private: [ "age", "favorites.animal", "favorites.purchase.number", "purchases.number", "privateDoes.notExist", ], protected: ["comment", "favorites.color", "protectedDoes.notExist"], restify: app.isRestify, }); serve(app, db.models.Invoice, { private: ["amount"], protected: ["receipt"], restify: app.isRestify, }); serve(app, db.models.Product, { private: ["department.code"], protected: ["price"], restify: app.isRestify, }); serve(app, db.models.Account, { private: ["accountNumber"], protected: ["points"], restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Product.create({ name: "Bobsleigh", price: 42, department: { code: 51, }, }) .then((createdProduct) => { product = createdProduct; return db.models.Customer.create({ name: "Bob", age: 12, comment: "Boo", favorites: { animal: "Boar", color: "Black", purchase: { item: product._id, number: 1, }, }, purchases: [ { item: product._id, number: 2, }, ], returns: [product._id], }); }) .then((createdCustomer) => { customer = createdCustomer; return db.models.Invoice.create({ customer: customer._id, amount: 100, receipt: "A", }); }) .then((createdInvoice) => { invoice = createdInvoice; return db.models.Account.create({ accountNumber: "123XYZ", points: 244, }); }) .then((createdAccount) => { account = createdAccount; return db.models.RepeatCustomer.create({ account: account._id, name: "Mike", visits: 24, status: "Awesome", job: "Hunter", }); }) .then((createdRepeatCustomer) => { repeatCustomer = createdRepeatCustomer; return db.models.Invoice.create({ customer: repeatCustomer._id, amount: 200, receipt: "B", }); }) .then((createdRepeatCustomerInvoice) => { repeatCustomerInvoice = createdRepeatCustomerInvoice; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[0].age, undefined); assert.equal(body[0].comment, undefined); assert.equal(body[0].purchases.length, 1); assert.deepEqual(body[0].favorites, { purchase: { item: product._id.toHexString(), }, }); done(); } ); }); it("GET /Customer?distinct=age 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 0); done(); } ); }); it("GET /Customer?distinct=comment 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "comment", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 0); done(); } ); }); it("GET /Customer/:id 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, undefined); assert.equal(body.comment, undefined); assert.equal(body.purchases.length, 1); assert.deepEqual(body.favorites, { purchase: { item: product._id.toHexString(), }, }); done(); } ); }); it("GET /Customer/:id?distinct=age 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { distinct: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 0); done(); } ); }); it("GET /Customer/:id?distinct=comment 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { distinct: "comment", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 0); done(); } ); }); it("GET /Customer?populate=favorites.purchase.item,purchases.item,returns 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { populate: "favorites.purchase.item,purchases.item,returns", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[0].age, undefined); assert.equal(body[0].comment, undefined); assert.deepEqual(body[0].favorites, { purchase: { item: { __v: 0, _id: product._id.toHexString(), name: "Bobsleigh", department: {}, }, }, }); assert.equal(body[0].purchases.length, 1); assert.ok(body[0].purchases[0].item); assert.equal( body[0].purchases[0].item._id, product._id.toHexString() ); assert.equal(body[0].purchases[0].item.name, "Bobsleigh"); assert.equal(body[0].purchases[0].item.price, undefined); assert.deepEqual(body[0].purchases[0].item.department, {}); assert.equal(body[0].purchases[0].number, undefined); assert.equal(body[0].returns.length, 1); assert.equal(body[0].returns[0].name, "Bobsleigh"); assert.equal(body[0].returns[0].price, undefined); assert.deepEqual(body[0].returns[0].department, {}); done(); } ); }); it("GET /Customer/:id?populate=favorites.purchase.item,purchases.item,returns 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { populate: "favorites.purchase.item,purchases.item,returns", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, undefined); assert.equal(body.comment, undefined); assert.deepEqual(body.favorites, { purchase: { item: { __v: 0, _id: product._id.toHexString(), name: "Bobsleigh", department: {}, }, }, }); assert.equal(body.purchases.length, 1); assert.ok(body.purchases[0].item); assert.equal(body.purchases[0].item._id, product._id.toHexString()); assert.equal(body.purchases[0].item.name, "Bobsleigh"); assert.equal(body.purchases[0].item.price, undefined); assert.deepEqual(body.purchases[0].item.department, {}); assert.equal(body.purchases[0].number, undefined); assert.equal(body.returns.length, 1); assert.equal(body.returns[0].name, "Bobsleigh"); assert.equal(body.returns[0].price, undefined); assert.deepEqual(body.returns[0].department, {}); done(); } ); }); it("GET /Invoice?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.ok(body[0].customer); assert.equal(body[0].amount, undefined); assert.equal(body[0].receipt, undefined); assert.equal(body[0].customer.name, "Bob"); assert.equal(body[0].customer.age, undefined); assert.equal(body[0].customer.comment, undefined); assert.deepEqual(body[0].customer.favorites, { purchase: { item: product._id.toHexString(), }, }); done(); } ); }); it("GET /Invoice/:id?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice/${invoice._id}`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.amount, undefined); assert.equal(body.receipt, undefined); assert.equal(body.customer.name, "Bob"); assert.equal(body.customer.age, undefined); assert.equal(body.customer.comment, undefined); assert.deepEqual(body.customer.favorites, { purchase: { item: product._id.toHexString(), }, }); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id - saves public fields`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "John", age: 24, comment: "Jumbo", favorites: { animal: "Jaguar", color: "Jade", purchase: { number: 2, }, }, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "John"); assert.equal(body.age, undefined); assert.equal(body.comment, undefined); assert.deepEqual(body.favorites, { purchase: { item: product._id.toHexString(), }, }); db.models.Customer.findById(customer._id) .then((foundCustomer) => { assert.equal(foundCustomer.age, 12); assert.equal(foundCustomer.comment, "Boo"); assert.deepEqual(foundCustomer.favorites.toObject(), { animal: "Boar", color: "Black", purchase: { item: product._id, number: 1, }, }); done(); }) .catch(done); } ); }); it(`${method} /Customer/:id - saves public fields (falsy values)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { age: 0, comment: "", favorites: { animal: "", color: "", purchase: { number: 0, }, }, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, undefined); assert.equal(body.comment, undefined); assert.deepEqual(body.favorites, { purchase: { item: product._id.toHexString(), }, }); db.models.Customer.findById(customer._id) .then((foundCustomer) => { assert.equal(foundCustomer.age, 12); assert.equal(foundCustomer.comment, "Boo"); assert.deepEqual(foundCustomer.favorites.toObject(), { animal: "Boar", color: "Black", purchase: { item: product._id, number: 1, }, }); done(); }) .catch(done); } ); }); }); it("GET /RepeatCustomer 200 - discriminator", (done) => { request.get( { url: `${testUrl}/api/v1/RepeatCustomer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body[0].name, "Mike"); assert.equal(body[0].visits, 24); assert.equal(body[0].status, undefined); assert.equal(body[0].job, undefined); done(); } ); }); it("GET /RepeatCustomer/:id?populate=account 200 - populate discriminator field from base schema", (done) => { request.get( { url: `${testUrl}/api/v1/RepeatCustomer/${repeatCustomer._id}`, qs: { populate: "account", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.account); assert.equal(body.account._id, account._id.toHexString()); assert.equal(body.account.accountNumber, undefined); assert.equal(body.account.points, undefined); assert.equal(body.name, "Mike"); assert.equal(body.visits, 24); assert.equal(body.status, undefined); assert.equal(body.job, undefined); done(); } ); }); it("GET /Invoice/:id?populate=customer 200 - populated discriminator", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice/${repeatCustomerInvoice._id}`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.amount, undefined); assert.equal(body.receipt, undefined); assert.equal(body.customer.name, "Mike"); assert.equal(body.customer.visits, 24); assert.equal(body.customer.status, undefined); assert.equal(body.customer.job, undefined); done(); } ); }); }); describe("yields an error", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { access: () => { let err = new Error("Something went wrong"); return Promise.reject(err); }, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 500", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "Error", message: "Something went wrong", }); done(); } ); }); }); }); } ================================================ FILE: test/integration/contextFilter.mjs ================================================ import assert from "assert"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const updateMethods = ["PATCH", "POST", "PUT"]; describe("contextFilter", () => { let app = createFn(); let server; let customers; let contextFilter = function (model, req, done) { done( model.find({ name: { $ne: "Bob" }, age: { $lt: 36 }, }) ); }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { contextFilter: contextFilter, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create([ { name: "Bob", age: 12, }, { name: "John", age: 24, }, { name: "Mike", age: 36, }, ]) .then((createdCustomers) => { customers = createdCustomers; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200 - filtered name and age", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].name, "John"); assert.equal(body[0].age, 24); done(); } ); }); it("GET /Customer/:id 404 - filtered name", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it("GET /Customer/:id/shallow 404 - filtered age", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customers[2]._id}/shallow`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it("GET /Customer/count 200 - filtered name and age", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/count`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.count, 1); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[1]._id}`, json: { name: "Johnny", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Johnny"); done(); } ); }); it(`${method} /Customer/:id 404 - filtered name`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); db.models.Customer.findById(customers[0]._id) .then((foundCustomer) => { assert.notEqual(foundCustomer.name, "Bobby"); done(); }) .catch(done); } ); }); }); it("DELETE /Customer/:id 200", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customers[1]._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); db.models.Customer.findById(customers[1]._id) .then((foundCustomer) => { assert.ok(!foundCustomer); done(); }) .catch(done); } ); }); it("DELETE /Customer/:id 404 - filtered age", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customers[2]._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); db.models.Customer.findById(customers[2]._id) .then((foundCustomer) => { assert.ok(foundCustomer); assert.equal(foundCustomer.name, "Mike"); done(); }) .catch(done); } ); }); it("DELETE /Customer 200 - filtered name and age", (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); db.models.Customer.countDocuments() .then((count) => { assert.equal(count, 2); done(); }) .catch(done); } ); }); }); } ================================================ FILE: test/integration/create.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); let testPort = 30023; let testUrl = `http://localhost:${testPort}`; let invalidId = "invalid-id"; let randomId = new mongoose.Types.ObjectId().toHexString(); describe("Create documents", () => { let app = createFn(); let server; let customer, product; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { restify: app.isRestify, }); serve(app, db.models.Invoice, { restify: app.isRestify, }); serve(app, db.models.Product, { restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } Promise.all([ db.models.Customer.create({ name: "Bob", }), db.models.Product.create({ name: "Bobsleigh", }), ]) .then(([createdCustomer, createdProduct]) => { customer = createdCustomer; product = createdProduct; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.name, "John"); done(); } ); }); it("POST /Customer 201 - generate _id (undefined)", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { _id: undefined, name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.name, "John"); done(); } ); }); it("POST /Customer 201 - generate _id (null)", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { _id: null, name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.name, "John"); done(); } ); }); it("POST /Customer 201 - use provided _id", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { _id: randomId, name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.ok(body._id === randomId); assert.equal(body.name, "John"); done(); } ); }); it("POST /Customer 201 - ignore __v", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { __v: "1", name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.ok(body.__v === 0); assert.equal(body.name, "John"); done(); } ); }); it("POST /Customer 201 - array", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: [ { name: "John", }, { name: "Mike", }, ], }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(Array.isArray(body)); assert.equal(body.length, 2); assert.ok(body[0]._id); assert.equal(body[0].name, "John"); assert.ok(body[1]._id); assert.equal(body[1].name, "Mike"); done(); } ); }); it("POST /Customer 400 - validation error", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.equal(body.name, "ValidationError"); assert.deepEqual(body, { name: "ValidationError", message: "Customer validation failed: name: Path `name` is required.", _message: "Customer validation failed", errors: { name: { kind: "required", message: "Path `name` is required.", name: "ValidatorError", path: "name", properties: { message: "Path `name` is required.", path: "name", type: "required", }, }, }, }); done(); } ); }); it("POST /Customer 400 - missing content type", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); done(); } ); }); it("POST /Customer 400 - invalid content type", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, formData: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); done(); } ); }); it("POST /Invoice 201 - referencing customer and product ids as strings", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: customer._id.toHexString(), products: product._id.toHexString(), amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.customer, customer._id); assert.equal(body.amount, 42); done(); } ); }); it("POST /Invoice 201 - referencing customer and products ids as strings", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: customer._id.toHexString(), products: [product._id.toHexString(), product._id.toHexString()], amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.customer, customer._id); assert.equal(body.amount, 42); done(); } ); }); it("POST /Invoice 201 - referencing customer and product ids", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: customer._id, products: product._id, amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.customer, customer._id); assert.equal(body.amount, 42); done(); } ); }); it("POST /Invoice 201 - referencing customer and products ids", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: customer._id, products: [product._id, product._id], amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.customer, customer._id); assert.equal(body.amount, 42); done(); } ); }); it("POST /Invoice 201 - referencing customer and product", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: customer, products: product, amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.customer, customer._id); assert.equal(body.amount, 42); done(); } ); }); it("POST /Invoice 201 - referencing customer and products", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: customer, products: [product, product], amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.customer, customer._id); assert.equal(body.amount, 42); done(); } ); }); it("POST /Invoice?populate=customer,products 201 - referencing customer and products", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer,products", }, json: { customer: customer, products: [product, product], amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.amount, 42); assert.equal(body.customer._id, customer._id); assert.equal(body.customer.name, customer.name); assert.equal(body.products[0]._id, product._id.toHexString()); assert.equal(body.products[0].name, product.name); assert.equal(body.products[1]._id, product._id.toHexString()); assert.equal(body.products[1].name, product.name); done(); } ); }); it("POST /Invoice 400 - referencing invalid customer and products ids", (done) => { request.post( { url: `${testUrl}/api/v1/Invoice`, json: { customer: invalidId, products: [invalidId, invalidId], amount: 42, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); delete body.message; delete body.errors.customer.message; assert.deepEqual(body, { name: "ValidationError", _message: "Invoice validation failed", errors: { customer: { kind: "ObjectId", name: "CastError", path: "customer", stringValue: '"invalid-id"', value: "invalid-id", valueType: "string", }, "products.0": { kind: "[ObjectId]", message: 'Cast to [ObjectId] failed for value "[ \'invalid-id\', \'invalid-id\' ]" (type string) at path "products.0" because of "CastError"', name: "CastError", path: "products.0", stringValue: "\"[ 'invalid-id', 'invalid-id' ]\"", value: "[ 'invalid-id', 'invalid-id' ]", valueType: "string", }, }, }); done(); } ); }); }); } ================================================ FILE: test/integration/delete.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const invalidId = "invalid-id"; const randomId = new mongoose.Types.ObjectId().toHexString(); describe("Delete documents", () => { describe("findOneAndRemove: true", () => { let app = createFn(); let server; let customer; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { findOneAndRemove: true, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create([ { name: "Bob", }, { name: "John", }, { name: "Mike", }, ]) .then((createdCustomers) => { customer = createdCustomers[0]; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("DELETE /Customer 204 - no id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); it("DELETE /Customer/:id 204 - created id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); it("DELETE /Customer/:id 404 - invalid id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${invalidId}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it("DELETE /Customer/:id 404 - random id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${randomId}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it('DELETE /Customer?query={"name":"John"} 200 - exact match', (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ name: "John", }), }, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); db.models.Customer.find({}) .then((customers) => { assert.equal(customers.length, 2); customers.forEach((customer) => { assert.ok(customer.name !== "John"); }); done(); }) .catch(done); } ); }); }); describe("findOneAndRemove: false", () => { let app = createFn(); let server; let customer; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { findOneAndRemove: false, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create([ { name: "Bob", }, { name: "John", }, { name: "Mike", }, ]) .then((createdCustomers) => { customer = createdCustomers[0]; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("DELETE /Customer 204 - no id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); it("DELETE /Customer/:id 204 - created id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); it("DELETE /Customer/:id 404 - invalid id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${invalidId}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it("DELETE /Customer/:id 404 - random id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${randomId}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it('DELETE /Customer?query={"name":"John"} 200 - exact match', (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ name: "John", }), }, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); db.models.Customer.find({}) .then((customers) => { assert.equal(customers.length, 2); customers.forEach((customer) => { assert.ok(customer.name !== "John"); }); done(); }) .catch(done); } ); }); }); }); } ================================================ FILE: test/integration/hooks.mjs ================================================ import assert from "assert"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); let testPort = 30023; let testUrl = `http://localhost:${testPort}`; describe("Mongoose hooks", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Hook, { restify: app.isRestify, }); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("POST /Hook 201", (done) => { request.post( { url: `${testUrl}/api/v1/Hook`, json: { preSaveError: false, postSaveError: false, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 201); assert.ok(body._id); assert.equal(body.preSaveError, false); assert.equal(body.postSaveError, false); done(); } ); }); it("POST /Hook 400", (done) => { request.post( { url: `${testUrl}/api/v1/Hook`, json: { preSaveError: true, postSaveError: false, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "Error", message: "AsyncPreSaveError", }); done(); } ); }); it("POST /Hook 400", (done) => { request.post( { url: `${testUrl}/api/v1/Hook`, json: { preSaveError: false, postSaveError: true, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "Error", message: "AsyncPostSaveError", }); done(); } ); }); }); } ================================================ FILE: test/integration/middleware.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import request from "request"; import sinon from "sinon"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const invalidId = "invalid-id"; const randomId = new mongoose.Types.ObjectId().toHexString(); const updateMethods = ["PATCH", "POST", "PUT"]; describe("preMiddleware/Create/Read/Update/Delete - undefined", () => { let app = createFn(); let server; let customer; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "John", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 201); done(); } ); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); it("DELETE /Customer/:id 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); }); describe("preMiddleware", () => { let app = createFn(); let server; let customer; let options = { preMiddleware: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.preMiddleware.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preMiddleware); let args = options.preMiddleware.args[0]; assert.equal(args.length, 3); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/:id 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preMiddleware); let args = options.preMiddleware.args[0]; assert.equal(args.length, 3); assert.equal(typeof args[2], "function"); done(); } ); }); it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "Pre", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 201); sinon.assert.calledOnce(options.preMiddleware); let args = options.preMiddleware.args[0]; assert.equal(args.length, 3); assert.equal(typeof args[2], "function"); done(); } ); }); it("POST /Customer 400 - not called (missing content type)", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); sinon.assert.notCalled(options.preMiddleware); done(); } ); }); it("POST /Customer 400 - not called (invalid content type)", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, formData: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); sinon.assert.notCalled(options.preMiddleware); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preMiddleware); let args = options.preMiddleware.args[0]; assert.equal(args.length, 3); assert.equal(typeof args[2], "function"); done(); } ); }); it(`${method} /Customer/:id 400 - not called (missing content type)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); sinon.assert.notCalled(options.preMiddleware); done(); } ); }); it(`${method} /Customer/:id 400 - not called (invalid content type)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, formData: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); sinon.assert.notCalled(options.preMiddleware); done(); } ); }); }); it("DELETE /Customer 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); sinon.assert.calledOnce(options.preMiddleware); let args = options.preMiddleware.args[0]; assert.equal(args.length, 3); assert.equal(typeof args[2], "function"); done(); } ); }); it("DELETE /Customer/:id 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); sinon.assert.calledOnce(options.preMiddleware); let args = options.preMiddleware.args[0]; assert.equal(args.length, 3); assert.equal(typeof args[2], "function"); done(); } ); }); }); describe("preCreate", () => { let app = createFn(); let server; let options = { preCreate: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); afterEach(() => { options.preCreate.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "Bob", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 201); sinon.assert.calledOnce(options.preCreate); let args = options.preCreate.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 201); assert.equal(typeof args[2], "function"); done(); } ); }); }); describe("preRead", () => { let app = createFn(); let server; let customer; let options = { preRead: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.preRead.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preRead); let args = options.preRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result[0].name, "Bob"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/count 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/count`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preRead); let args = options.preRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.count, 1); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/:id 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preRead); let args = options.preRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/:id/shallow 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}/shallow`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preRead); let args = options.preRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); }); describe("preUpdate", () => { let app = createFn(); let server; let customer; let options = { preUpdate: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.preUpdate.resetHistory(); }); after((done) => { dismantle(app, server, done); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.preUpdate); let args = options.preUpdate.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bobby"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it(`${method} /Customer/:id 400 - not called (missing content type)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); sinon.assert.notCalled(options.preUpdate); done(); } ); }); it(`${method} /Customer/:id 400 - not called (invalid content type)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, formData: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); sinon.assert.notCalled(options.preUpdate); done(); } ); }); }); }); describe("preDelete", () => { let app = createFn(); let server; let customer; let options = { preDelete: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.preDelete.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("DELETE /Customer 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); sinon.assert.calledOnce(options.preDelete); let args = options.preDelete.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result, undefined); assert.equal(args[0].erm.statusCode, 204); assert.equal(typeof args[2], "function"); done(); } ); }); it("DELETE /Customer/:id 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); sinon.assert.calledOnce(options.preDelete); let args = options.preDelete.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result, undefined); assert.equal(args[0].erm.statusCode, 204); assert.equal(typeof args[2], "function"); done(); } ); }); }); describe("postCreate/Read/Update/Delete - undefined", () => { let app = createFn(); let server; let customer; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "John", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 201); done(); } ); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request.post( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); it("DELETE /Customer/:id 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); }); describe("postCreate", () => { let app = createFn(); let server; let options = { postCreate: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); afterEach(() => { options.postCreate.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "Bob", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 201); sinon.assert.calledOnce(options.postCreate); let args = options.postCreate.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 201); assert.equal(typeof args[2], "function"); done(); } ); }); it("POST /Customer 400 - missing required field", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { comment: "Bar", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "ValidationError", _message: "Customer validation failed", message: "Customer validation failed: name: Path `name` is required.", errors: { name: { kind: "required", message: "Path `name` is required.", name: "ValidatorError", path: "name", properties: { fullPath: "name", message: "Path `name` is required.", path: "name", type: "required", }, }, }, }); sinon.assert.notCalled(options.postCreate); done(); } ); }); }); describe("postRead", () => { let app = createFn(); let server; let customer; let options = { postRead: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.postRead.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postRead); let args = options.postRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result[0].name, "Bob"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/count 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/count`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postRead); let args = options.postRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.count, 1); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/:id 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postRead); let args = options.postRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it("GET /Customer/:id 404", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${randomId}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); sinon.assert.notCalled(options.postRead); done(); } ); }); it("GET /Customer/:id 404 - invalid id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${invalidId}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); sinon.assert.notCalled(options.postRead); done(); } ); }); it("GET /Customer/:id/shallow 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer._id}/shallow`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postRead); let args = options.postRead.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); }); describe("postUpdate", () => { let app = createFn(); let server; let customer; let options = { postUpdate: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.postUpdate.resetHistory(); }); after((done) => { dismantle(app, server, done); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postUpdate); let args = options.postUpdate.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bobby"); assert.equal(args[0].erm.statusCode, 200); assert.equal(typeof args[2], "function"); done(); } ); }); it(`${method} /Customer/:id 404 - random id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${randomId}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); sinon.assert.notCalled(options.postUpdate); done(); } ); }); it(`${method} /Customer/:id 404 - invalid id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${invalidId}`, json: { name: "Bobby", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); sinon.assert.notCalled(options.postUpdate); done(); } ); }); it(`${method} /Customer/:id 400 - not called (missing content type)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); sinon.assert.notCalled(options.postUpdate); done(); } ); }); it(`${method} /Customer/:id 400 - not called (invalid content type)`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, formData: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); sinon.assert.notCalled(options.postUpdate); done(); } ); }); }); }); describe("postDelete", () => { let app = createFn(); let server; let customer; let options = { postDelete: sinon.spy((req, res, next) => { next(); }), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; }) .then(done) .catch(done); }); }); afterEach(() => { options.postDelete.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("DELETE /Customer 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); sinon.assert.calledOnce(options.postDelete); let args = options.postDelete.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result, undefined); assert.equal(args[0].erm.statusCode, 204); assert.equal(typeof args[2], "function"); done(); } ); }); it("DELETE /Customer/:id 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); sinon.assert.calledOnce(options.postDelete); let args = options.postDelete.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result, undefined); assert.equal(args[0].erm.statusCode, 204); assert.equal(typeof args[2], "function"); done(); } ); }); it("DELETE /Customer/:id 404", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${randomId}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); sinon.assert.notCalled(options.postDelete); done(); } ); }); it("DELETE /Customer/:id 404 - invalid id", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${invalidId}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); sinon.assert.notCalled(options.postDelete); done(); } ); }); }); describe("postCreate yields an error", () => { let app = createFn(); let server; let options = { postCreate: sinon.spy((req, res, next) => { next(new Error("Something went wrong")); }), postProcess: sinon.spy(), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); afterEach(() => { options.postCreate.resetHistory(); }); after((done) => { dismantle(app, server, done); }); // TODO: This test is weird it("POST /Customer 201", (done) => { request.post( { url: `${testUrl}/api/v1/Customer`, json: { name: "Bob", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 400); sinon.assert.calledOnce(options.postCreate); let args = options.postCreate.args[0]; assert.equal(args.length, 3); assert.equal(args[0].erm.result.name, "Bob"); assert.equal(args[0].erm.statusCode, 400); assert.equal(typeof args[2], "function"); sinon.assert.notCalled(options.postProcess); done(); } ); }); }); describe("postProcess", () => { let app = createFn(); let server; let options = { postProcess: sinon.spy(), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); afterEach(() => { options.postProcess.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postProcess); let args = options.postProcess.args[0]; assert.equal(args.length, 2); assert.deepEqual(args[0].erm.result, []); assert.equal(args[0].erm.statusCode, 200); done(); } ); }); }); describe("postProcess (async outputFn)", () => { let app = createFn(); let server; let options = { outputFn: (req, res) => { if (app.isRestify) { res.send(200); } else { res.sendStatus(200); } return Promise.resolve(); }, postProcess: sinon.spy(), restify: app.isRestify, }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, options); server = app.listen(testPort, done); }); }); afterEach(() => { options.postProcess.resetHistory(); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); sinon.assert.calledOnce(options.postProcess); let args = options.postProcess.args[0]; assert.equal(args.length, 2); assert.deepEqual(args[0].erm.result, []); assert.equal(args[0].erm.statusCode, 200); done(); } ); }); }); } ================================================ FILE: test/integration/options.mjs ================================================ import assert from "assert"; import request from "request"; import sinon from "sinon"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const updateMethods = ["PATCH", "POST", "PUT"]; describe("no options", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve( app, db.models.Customer, app.isRestify ? { restify: app.isRestify, } : undefined ); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); describe("defaults - version set in defaults", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } const defaults = { version: "/custom", }; serve(app, db.models.Customer, { ...defaults, restify: app.isRestify, }); serve(app, db.models.Invoice, { ...defaults, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/custom/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); it("GET /Invoice 200", (done) => { request.get( { url: `${testUrl}/api/custom/Invoice`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); describe("totalCountHeader - boolean (default header)", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { totalCountHeader: true, restify: app.isRestify, }); db.models.Customer.create([ { name: "Bob", }, { name: "John", }, { name: "Mike", }, ]) .then(() => { server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer?limit=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(res.headers["x-total-count"], 3); assert.equal(body.length, 1); done(); } ); }); it("GET /Customer?skip=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { skip: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(res.headers["x-total-count"], 3); assert.equal(body.length, 2); done(); } ); }); it("GET /Customer?limit=1&skip=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 1, skip: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(res.headers["x-total-count"], 3); assert.equal(body.length, 1); done(); } ); }); it("GET /Customer?distinct=name 200 - ignore total count header", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); assert.equal(res.headers["x-total-count"], undefined); assert.equal(body[0], "Bob"); assert.equal(body[1], "John"); assert.equal(body[2], "Mike"); done(); } ); }); }); describe("totalCountHeader - boolean (default header) + contextFilter", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { totalCountHeader: true, contextFilter: (model, req, done) => done(model.find()), restify: app.isRestify, }); db.models.Customer.create([ { name: "Bob", }, { name: "John", }, { name: "Mike", }, ]) .then(() => { server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer?limit=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(res.headers["x-total-count"], 3); assert.equal(body.length, 1); done(); } ); }); }); describe("totalCountHeader - string (custom header)", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { totalCountHeader: "X-Custom-Count", restify: app.isRestify, }); db.models.Customer.create([ { name: "Bob", }, { name: "John", }, { name: "Mike", }, ]) .then(() => { server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer?limit=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(res.headers["x-custom-count"], 3); assert.equal(body.length, 1); done(); } ); }); }); describe("limit", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { limit: 2, restify: app.isRestify, }); db.models.Customer.create([ { name: "Bob", }, { name: "John", }, { name: "Mike", }, ]) .then(() => { server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); done(); } ); }); it("GET /Customer 200 - override limit in options (query.limit === 0)", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 0, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); done(); } ); }); it("GET /Customer 200 - override limit in options (query.limit < options.limit)", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); done(); } ); }); it("GET /Customer 200 - override limit in query (options.limit < query.limit)", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 3, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); done(); } ); }); it("GET /Customer/count 200 - ignore limit", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/count`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.count, 3); done(); } ); }); }); describe("name", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { name: "Client", restify: app.isRestify, }); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Client 200", (done) => { request.get( { url: `${testUrl}/api/v1/Client`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); describe("prefix", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { prefix: "/applepie", restify: app.isRestify, }); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /applepie/v1/Customer 200", (done) => { request.get( { url: `${testUrl}/applepie/v1/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); describe("version", () => { describe("v8", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { version: "/v8", restify: app.isRestify, }); server = app.listen(testPort, done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /v8/Customer 200", (done) => { request.get( { url: `${testUrl}/api/v8/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); describe("custom id location", () => { let app = createFn(); let server; let customer; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { version: "/v8/Entities/:id", restify: app.isRestify, }); db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /v8/Entities/Customer 200", (done) => { request.get( { url: `${testUrl}/api/v8/Entities/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); it("GET /v8/Entities/:id/Customer 200", (done) => { request.get( { url: `${testUrl}/api/v8/Entities/${customer._id}/Customer`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); it("GET /v8/Entities/:id/Customer/shallow 200", (done) => { request.get( { url: `${testUrl}/api/v8/Entities/${customer._id}/Customer/shallow`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); it("GET /v8/Entities/Customer/count 200", (done) => { request.get( { url: `${testUrl}/api/v8/Entities/Customer/count`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); }); describe("defaults - preUpdate with falsy findOneAndUpdate", () => { let app = createFn(); let server; let customer; let options = { findOneAndUpdate: false, preUpdate: [ sinon.spy((req, res, next) => { next(); }), sinon.spy((req, res, next) => { next(); }), ], }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Product, { ...options, restify: app.isRestify, }); // order is important, test the second attached model to potentially reproduce the error. serve(app, db.models.Customer, { ...options, restify: app.isRestify, }); db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, json: { age: 12, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, 12); assert.equal(options.preUpdate.length, 2); sinon.assert.calledOnce(options.preUpdate[0]); sinon.assert.calledOnce(options.preUpdate[1]); options.preUpdate[0].resetHistory(); options.preUpdate[1].resetHistory(); done(); } ); }); }); }); describe("defaults - preDelete with falsy findOneAndRemove", () => { let app = createFn(); let server; let customer; let options = { findOneAndRemove: false, preDelete: [ sinon.spy((req, res, next) => { next(); }), sinon.spy((req, res, next) => { next(); }), ], }; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Product, { ...options, restify: app.isRestify, }); // order is important, test the second attached model to potentially reproduce the error. serve(app, db.models.Customer, { ...options, restify: app.isRestify, }); db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("DELETE /Customer/:id 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer._id}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); assert.equal(options.preDelete.length, 2); sinon.assert.calledOnce(options.preDelete[0]); sinon.assert.calledOnce(options.preDelete[1]); done(); } ); }); }); describe("idProperty", () => { let app = createFn(); let server; let customer; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { idProperty: "name", restify: app.isRestify, }); db.models.Customer.create({ name: "Bob", }) .then((createdCustomer) => { customer = createdCustomer; server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer/:name 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customer.name}`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); done(); } ); }); updateMethods.forEach((method) => { it(`${method} /Customer/:name 200`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customer.name}`, json: { age: 12, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); assert.equal(body.age, 12); done(); } ); }); }); it("DELETE /Customer/:name 204", (done) => { request.del( { url: `${testUrl}/api/v1/Customer/${customer.name}`, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 204); done(); } ); }); }); describe("allowRegex", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { allowRegex: false, restify: app.isRestify, }); db.models.Customer.create({ name: "Bob", }) .then(() => { server = app.listen(testPort, done); }) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ name: { $regex: "^B" }, }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); delete body.reason; assert.deepEqual(body, { message: "invalid_json_query", name: "Error", }); done(); } ); }); }); } ================================================ FILE: test/integration/read.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const invalidId = "invalid-id"; const randomId = new mongoose.Types.ObjectId().toHexString(); describe("Read documents", () => { let app = createFn(); let server; let customers; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { allowRegex: true, restify: app.isRestify, }); serve(app, db.models.Invoice, { restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Product.create({ name: "Bobsleigh", }) .then((createdProduct) => { return db.models.Customer.create([ { name: "Bob", age: 12, favorites: { animal: "Boar", color: "Black", purchase: { item: createdProduct._id, number: 1, }, }, coordinates: [45.2667, 72.15], }, { name: "John", age: 24, favorites: { animal: "Jaguar", color: "Jade", purchase: { item: createdProduct._id, number: 2, }, }, }, { name: "Mike", age: 36, favorites: { animal: "Medusa", color: "Maroon", purchase: { item: createdProduct._id, number: 3, }, }, }, ]); }) .then((createdCustomers) => { customers = createdCustomers; return db.models.Invoice.create([ { customer: customers[0]._id, amount: 100, receipt: "A", }, { customer: customers[1]._id, amount: 200, receipt: "B", }, { customer: customers[2]._id, amount: 300, receipt: "C", }, ]); }) .then(() => done()) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); done(); } ); }); it("GET /Customer/:id 200 - created id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); done(); } ); }); it("GET /Customer/:id 404 - invalid id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${invalidId}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it("GET /Customer/:id 404 - random id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${randomId}`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); describe("ignore unknown parameters", () => { it("GET /Customer?foo=bar 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { foo: "bar", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); done(); } ); }); }); describe("limit", () => { it("GET /Customer?limit=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); done(); } ); }); it("GET /Customer?limit=foo 400 - evaluates to NaN", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { limit: "foo", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "Error", message: "invalid_json_query", }); done(); } ); }); }); describe("skip", () => { it("GET /Customer?skip=1 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { skip: 1, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); done(); } ); }); it("GET /Customer?skip=foo 400 - evaluates to NaN", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { skip: "foo", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "Error", message: "invalid_json_query", }); done(); } ); }); }); describe("sort", () => { it("GET /Customer?sort=name 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { sort: "name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); assert.equal(body[0].name, "Bob"); assert.equal(body[1].name, "John"); assert.equal(body[2].name, "Mike"); done(); } ); }); it("GET /Customer?sort=-name 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { sort: "-name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); assert.equal(body[0].name, "Mike"); assert.equal(body[1].name, "John"); assert.equal(body[2].name, "Bob"); done(); } ); }); it('GET /Customer?sort={"name":1} 200', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { sort: { name: 1, }, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); assert.equal(body[0].name, "Bob"); assert.equal(body[1].name, "John"); assert.equal(body[2].name, "Mike"); done(); } ); }); it('GET /Customer?sort={"name":-1} 200', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { sort: { name: -1, }, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); assert.equal(body[0].name, "Mike"); assert.equal(body[1].name, "John"); assert.equal(body[2].name, "Bob"); done(); } ); }); }); describe("query", () => { it("GET /Customer?query={} 200 - empty object", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({}), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); done(); } ); }); it('GET /Customer?query={"$near": { "$geometry": { "coordinates": [45.2667, 72.1500] } }} 200 - coordinates', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ coordinates: { $near: { $geometry: { type: "Point", coordinates: [45.2667, 72.15], }, $maxDistance: 1000, }, }, }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); done(); } ); }); it("GET /Customer?query=invalidJson 400 - invalid json", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: "invalidJson", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "Error", message: "invalid_json_query", }); done(); } ); }); describe("string", () => { it('GET /Customer?query={"name":"John"} 200 - exact match', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ name: "John", }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].name, "John"); done(); } ); }); it('GET /Customer?query={"favorites.animal":"Jaguar"} 200 - exact match (nested property)', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ "favorites.animal": "Jaguar", }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].favorites.animal, "Jaguar"); done(); } ); }); it('GET /Customer?query={"name":{"$regex":"^J"}} 200 - name starting with', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ name: { $regex: "^J" }, }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.ok(body[0].name[0] === "J"); done(); } ); }); it('GET /Customer?query={"name":["Bob","John"]}&sort=name 200 - in', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ name: ["Bob", "John"], }), sort: "name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].name, "Bob"); assert.equal(body[1].name, "John"); done(); } ); }); }); describe("number", () => { it('GET /Customer?query={"age":"24"} 200 - exact match', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ age: 24, }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].age, 24); done(); } ); }); it('GET /Customer?query={"age":["12","24"]}&sort=age 200 - in', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { query: JSON.stringify({ age: ["12", "24"], }), sort: "age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 2); assert.equal(body[0].age, 12); assert.equal(body[1].age, 24); done(); } ); }); }); }); describe("select", () => { it('GET /Customer?select=["name"] 200 - only include', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { select: ["name"], }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((item) => { assert.equal(Object.keys(item).length, 2); assert.ok(item._id); assert.ok(item.name); }); done(); } ); }); it("GET /Customer?select=name 200 - only include", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { select: "name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((item) => { assert.equal(Object.keys(item).length, 2); assert.ok(item._id); assert.ok(item.name); }); done(); } ); }); it("GET /Customer?select=favorites.animal 200 - only include (nested field)", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { select: "favorites.animal", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((item) => { assert.equal(Object.keys(item).length, 2); assert.ok(item._id); assert.ok(item.favorites); assert.ok(item.favorites.animal); assert.ok(item.favorites.color === undefined); }); done(); } ); }); it("GET /Customer?select=-name 200 - exclude name", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { select: "-name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((item) => { assert.ok(item.name === undefined); }); done(); } ); }); it('GET /Customer?select={"name":1} 200 - only include name', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { select: JSON.stringify({ name: 1, }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((item) => { assert.equal(Object.keys(item).length, 2); assert.ok(item._id); assert.ok(item.name); }); done(); } ); }); it('GET /Customer?select={"name":0} 200 - exclude name', (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { select: JSON.stringify({ name: 0, }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((item) => { assert.ok(item.name === undefined); }); done(); } ); }); }); describe("populate", () => { it("GET /Invoice?populate=customer 200", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.ok(invoice.customer.age); }); done(); } ); }); it('GET /Invoice?populate={path:"customer"} 200', (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: JSON.stringify({ path: "customer", }), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.ok(invoice.customer.age); }); done(); } ); }); it('GET /Invoice?populate=[{path:"customer"}] 200', (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: JSON.stringify([ { path: "customer", }, ]), }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.ok(invoice.customer.age); }); done(); } ); }); it("GET /Customer?populate=favorites.purchase.item 200 - nested field", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { populate: "favorites.purchase.item", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((customer) => { assert.ok(customer.favorites.purchase); assert.ok(customer.favorites.purchase.item); assert.ok(customer.favorites.purchase.item._id); assert.ok(customer.favorites.purchase.item.name); assert.ok(customer.favorites.purchase.number); }); done(); } ); }); it("GET /Invoice?populate=customer.account 200 - ignore deep populate", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer.account", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.customer); assert.equal(typeof invoice.customer, "string"); }); done(); } ); }); it("GET /Invoice?populate=evilCustomer 200 - ignore unknown field", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "evilCustomer", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); done(); } ); }); describe("with select", () => { it("GET Invoices?populate=customer&select=amount 200 - only include amount and customer document", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", select: "amount", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.amount); assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.ok(invoice.customer.age); assert.equal(invoice.receipt, undefined); }); done(); } ); }); it("GET Invoices?populate=customer&select=amount,customer.name 200 - only include amount and customer name", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", select: "amount,customer.name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.amount); assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.equal(invoice.customer.age, undefined); assert.equal(invoice.receipt, undefined); }); done(); } ); }); it("GET Invoices?populate=customer&select=customer.name 200 - include all invoice fields, but only include customer name", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", select: "customer.name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.amount); assert.ok(invoice.receipt); assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.equal(invoice.customer.age, undefined); }); done(); } ); }); it("GET Invoices?populate=customer&select=-customer.name 200 - include all invoice and fields, but exclude customer name", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", select: "-customer.name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.amount); assert.ok(invoice.receipt); assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.age); assert.equal(invoice.customer.name, undefined); }); done(); } ); }); it("GET Invoices?populate=customer&select=amount,-customer.-id,customer.name 200 - only include amount and customer name and exclude customer _id", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", select: "amount,-customer._id,customer.name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.amount); assert.ok(invoice.customer); assert.ok(invoice.customer.name); assert.equal(invoice.receipt, undefined); assert.equal(invoice.customer._id, undefined); assert.equal(invoice.customer.age, undefined); }); done(); } ); }); it("GET Invoices?populate=customer&select=customer.name,customer.age 200 - only include customer name and age", (done) => { request.get( { url: `${testUrl}/api/v1/Invoice`, qs: { populate: "customer", select: "customer.name,customer.age", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); body.forEach((invoice) => { assert.ok(invoice.amount); assert.ok(invoice.receipt); assert.ok(invoice.customer); assert.ok(invoice.customer._id); assert.ok(invoice.customer.name); assert.ok(invoice.customer.age); }); done(); } ); }); }); }); describe("distinct", () => { it("GET /Customer?distinct=name 200 - array of unique names", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, qs: { distinct: "name", }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 3); assert.equal(body[0], "Bob"); assert.equal(body[1], "John"); assert.equal(body[2], "Mike"); done(); } ); }); }); describe("count", () => { it("GET /Customer/count 200", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/count`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.count, 3); done(); } ); }); it("GET /Customer/count 200 - ignores sort", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/count`, qs: { sort: { _id: 1, }, }, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.count, 3); done(); } ); }); }); describe("shallow", () => { it("GET /Customer/:id/shallow 200 - created id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${customers[0]._id}/shallow`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); done(); } ); }); it("GET /Customer/:id/shallow 404 - invalid id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${invalidId}/shallow`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it("GET /Customer/:id/shallow 404 - random id", (done) => { request.get( { url: `${testUrl}/api/v1/Customer/${randomId}/shallow`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); }); }); } ================================================ FILE: test/integration/resource_filter.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import { Filter } from "../../dist/resource_filter.js"; import setupDb from "./setup.mjs"; const db = setupDb(); describe("Resource filter", () => { const filter = new Filter(); before((done) => { db.initialize((err) => { if (err) { return done(err); } filter.add(db.models.Customer, { filteredKeys: { private: [ "comment", "address", "favorites.purchase.number", "purchases.number", "purchases.item.price", ], protected: [], }, }); filter.add(db.models.Invoice, { filteredKeys: { private: ["amount", "customer.address", "products.price"], protected: [], }, }); filter.add(db.models.Product, { filteredKeys: { private: ["price", "department.code"], protected: [], }, }); db.reset(done); }); }); after((done) => { db.close(done); }); describe("lean", () => { describe("with populated docs", () => { it("excludes fields from populated items", () => { let invoice = { customer: { name: "John", address: "123 Drury Lane", }, amount: 42, }; invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, ], }); assert.ok( invoice.amount === undefined, "Invoice amount should be excluded" ); assert.ok( invoice.customer.address === undefined, "Customer address should be excluded" ); }); it("iterates through array of populated objects", () => { let invoice = { customer: "objectid", amount: 240, products: [ { name: "Squirt Gun", price: 42, }, { name: "Water Balloons", price: 1, }, { name: "Garden Hose", price: 10, }, ], }; invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "products", }, ], }); invoice.products.forEach((product) => { assert.ok( product.name !== undefined, "product name should be populated" ); assert.ok( product.price === undefined, "product price should be excluded" ); }); }); it("filters multiple populated models", () => { let invoice = { customer: { name: "John", address: "123 Drury Lane", }, amount: 240, products: [ { name: "Squirt Gun", price: 42, }, { name: "Water Balloons", price: 1, }, { name: "Garden Hose", price: 10, }, ], }; invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, { path: "products", }, ], }); assert.equal( invoice.customer.name, "John", "customer name should be populated" ); assert.ok( invoice.customer.address === undefined, "customer address should be excluded" ); invoice.products.forEach((product) => { assert.ok( product.name !== undefined, "product name should be populated" ); assert.ok( product.price === undefined, "product price should be excluded" ); }); }); it("filters nested populated docs", () => { let customer = { name: "John", favorites: { purchase: { item: { name: "Squirt Gun", price: 42 }, number: 2, }, }, }; customer = filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "favorites.purchase.item", }, ], }); assert.ok( customer.favorites.purchase.item, "Purchased item should be included" ); assert.ok( customer.favorites.purchase.item.name !== undefined, "Purchased item name should be included" ); assert.ok( customer.favorites.purchase.item.price === undefined, "Purchased item price should be excluded" ); assert.ok( customer.favorites.purchase.number === undefined, "Purchased item number should be excluded" ); }); it("filters embedded array of populated docs", () => { let customer = { name: "John", purchases: [ { item: { name: "Squirt Gun", price: 42 }, number: 2, }, { item: { name: "Water Balloons", price: 1 }, number: 200, }, { item: { name: "Garden Hose", price: 10 }, number: 1, }, ], }; customer = filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "purchases.item", }, ], }); customer.purchases.forEach((p) => { assert.ok( p.number === undefined, "Purchase number should be excluded" ); assert.ok(p.item, "Item should be included"); assert.ok(p.item.name !== undefined, "Item name should be populated"); assert.ok( p.item.price === undefined, "Item price should be excluded" ); }); }); }); }); describe("not lean", () => { it("excludes items in the excluded string", () => { let customer = new db.models.Customer({ name: "John", address: "123 Drury Lane", comment: "Has a big nose", }); customer = filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, }); assert.equal(customer.name, "John", "Customer name should be John"); assert.ok( customer.address === undefined, "Customer address should be excluded" ); assert.ok( customer.comment === undefined, "Customer comment should be excluded" ); }); it("excludes fields from embedded documents", () => { let product = new db.models.Product({ name: "Garden Hose", department: { name: "Gardening", code: 435, }, }); product = filter.filterObject(product, { access: "public", modelName: db.models.Product.modelName, }); assert.equal( product.name, "Garden Hose", "Product name should be included" ); assert.equal( product.department.name, "Gardening", "Deparment name should be included" ); assert.ok( product.department.code === undefined, "Deparment code should be excluded" ); }); it("excludes fields from embedded arrays", () => { let customer = new db.models.Customer({ name: "John", purchases: [ { item: new mongoose.Types.ObjectId(), number: 2, }, { item: new mongoose.Types.ObjectId(), number: 100, }, { item: new mongoose.Types.ObjectId(), number: 1, }, ], }); customer = filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, }); customer.purchases.forEach((purchase) => { assert.ok(purchase.item !== undefined, "item should be included"); assert.ok(purchase.number === undefined, "number should be excluded"); }); }); describe("with populated docs", () => { let products; let invoiceId; let customerId; before((done) => { products = [ { name: "Squirt Gun", department: { code: 51, }, price: 42, }, { name: "Water Balloons", department: { code: 819, }, price: 1, }, { name: "Garden Hose", department: { code: 555, }, price: 10, }, ]; let createdProducts; db.models.Product.create(products) .then((products) => { assert.ok(products); createdProducts = products; return new db.models.Customer({ name: "John", address: "123 Drury Lane", purchases: [ { item: createdProducts[0]._id, number: 2, }, { item: createdProducts[1]._id, number: 100, }, { item: createdProducts[2]._id, number: 1, }, ], favorites: { purchase: { item: createdProducts[0]._id, number: 2, }, }, }).save(); }) .then((customer) => { assert.ok(customer); customerId = customer._id; return new db.models.Invoice({ customer: customer._id, amount: 42, products: [ createdProducts[0]._id, createdProducts[1]._id, createdProducts[2]._id, ], }).save(); }) .then((invoice) => { assert.ok(invoice); invoiceId = invoice._id; done(); }) .catch(done); }); after((done) => { db.models.Customer.deleteMany() .then(() => db.models.Invoice.deleteMany()) .then(() => db.models.Product.deleteMany()) .then(()=>{done();}) .catch(done); }); it("excludes fields from populated items", (done) => { db.models.Invoice.findById(invoiceId) .populate("customer") .exec() .then((invoice) => { assert.ok(invoice); invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, ], }); assert.ok( invoice.amount === undefined, "Invoice amount should be excluded" ); assert.ok( invoice.customer.name !== undefined, "Customer name should be included" ); assert.ok( invoice.customer.address === undefined, "Customer address should be excluded" ); done(); }) .catch(done); }); it("iterates through array of populated objects", (done) => { db.models.Invoice.findById(invoiceId) .populate("products") .exec() .then((invoice) => { assert.ok(invoice); invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "products", }, ], }); invoice.products.forEach((product) => { assert.ok( product.name !== undefined, "Product name should be populated" ); assert.ok( product.price === undefined, "Product price should be excluded" ); }); done(); }) .catch(done); }); it("filters multiple populated models", (done) => { db.models.Invoice.findById(invoiceId) .populate("products customer") .exec() .then((invoice) => { assert.ok(invoice); invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, { path: "products", }, ], }); assert.equal( invoice.customer.name, "John", "Customer name should be populated" ); assert.ok( invoice.customer.address === undefined, "Customer address should be excluded" ); invoice.products.forEach((product) => { assert.ok( product.name !== undefined, "Product name should be populated" ); assert.ok( product.price === undefined, "Product price should be excluded" ); }); done(); }) .catch(done); }); it("filters nested populated docs", (done) => { db.models.Customer.findById(customerId) .populate("favorites.purchase.item") .exec() .then((customer) => { assert.ok(customer); customer = filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "favorites.purchase.item", }, ], }); assert.ok( customer.favorites.purchase.item, "Purchased item should be included" ); assert.ok( customer.favorites.purchase.item.number === undefined, "Purchased item number should be excluded" ); assert.ok( customer.favorites.purchase.item.name !== undefined, "Purchased item name should be included" ); assert.ok( customer.favorites.purchase.item.price === undefined, "Purchased item price should be excluded" ); done(); }) .catch(done); }); it("filters embedded array of populated docs", (done) => { db.models.Customer.findById(customerId) .populate("purchases.item") .exec() .then((customer) => { assert.ok(customer); customer = filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "purchases.item", }, ], }); customer.purchases.forEach((p, i) => { assert.ok( p.number === undefined, "Purchase number should be excluded" ); assert.equal( p.item.name, products[i].name, "Item name should be populated" ); assert.ok( p.item.price === undefined, "Item price should be excluded" ); assert.ok(p.item.department); assert.ok(p.item.department.code === undefined); }); done(); }) .catch(done); }); }); }); describe("protected fields", () => { it("defaults to not including any", () => { const filter = new Filter(); filter.add(db.models.Invoice, { filteredKeys: { private: ["amount"], protected: ["products"], }, }); let invoice = { customer: "objectid", amount: 240, products: ["objectid"], }; invoice = filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, }); assert.equal(invoice.customer, "objectid"); assert.ok(invoice.amount === undefined, "Amount should be excluded"); assert.ok(invoice.products === undefined, "Products should be excluded"); }); it("returns protected fields", () => { const filter = new Filter(); filter.add(db.models.Invoice, { filteredKeys: { private: ["amount"], protected: ["products"], }, }); let invoice = { customer: "objectid", amount: 240, products: ["objectid"], }; invoice = filter.filterObject(invoice, { access: "protected", modelName: db.models.Invoice.modelName, }); assert.equal(invoice.customer, "objectid"); assert.ok(invoice.amount === undefined, "Amount should be excluded"); assert.equal( invoice.products[0], "objectid", "Products should be included" ); }); }); describe("descriminated schemas", () => { const filter = new Filter(); before((done) => { filter.add(db.models.Account, { filteredKeys: { private: ["accountNumber"], protected: [], }, }); filter.add(db.models.RepeatCustomer, { filteredKeys: { private: [], protected: [], }, }); db.models.Account.create({ accountNumber: "123XYZ", points: 244, }) .then((account) => { assert.ok(account); return db.models.RepeatCustomer.create({ name: "John Smith", account: account._id, }); }) .then(() => done()) .catch(done); }); after((done) => { db.models.Account.deleteMany() .then(() => db.models.Customer.deleteMany()) .then(()=>{done();}) .catch(done); }); it.skip("should filter populated from subschema", (done) => { db.models.RepeatCustomer.findOne() .populate("account") .exec((err, doc) => { assert.ok(!err); let customer = filter.filterObject(doc, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "account", }, ], }); assert.equal(customer.name, "John Smith"); assert.equal(customer.account.points, 244); assert.ok( customer.account.accountNumber === undefined, "account number should be excluded" ); done(); }); }); it.skip("should filter populated from base schema", (done) => { db.models.Customer.findOne() .populate("account") .exec((err, doc) => { assert.ok(!err); doc.populate("account", (err, doc) => { assert.ok(!err); let customer = filter.filterObject(doc, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "account", }, ], }); assert.equal(customer.name, "John Smith"); assert.equal(customer.account.points, 244); assert.ok( customer.account.accountNumber === undefined, "account number should be excluded" ); done(); }); }); }); }); }); ================================================ FILE: test/integration/setup.mjs ================================================ import mongoose, { Schema } from "mongoose"; export default function () { const ProductSchema = new Schema({ name: { type: String, required: true }, department: { name: { type: String }, code: { type: Number }, }, price: { type: Number }, }); class BaseCustomerSchema extends Schema { constructor(definition, options) { const def = Object.assign(definition, { account: { type: Schema.Types.ObjectId, ref: "Account" }, name: { type: String, required: true, unique: true }, comment: { type: String }, address: { type: String }, age: { type: Number }, favorites: { animal: { type: String }, color: { type: String }, purchase: { item: { type: Schema.Types.ObjectId, ref: "Product" }, number: { type: Number }, }, }, purchases: [ { item: { type: Schema.Types.ObjectId, ref: "Product" }, number: { type: Number }, }, ], returns: [{ type: Schema.Types.ObjectId, ref: "Product" }], creditCard: { type: String, access: "protected" }, ssn: { type: String, access: "private" }, coordinates: { type: [Number], index: "2dsphere" }, }); super(def, options); } } const CustomerSchema = new BaseCustomerSchema( {}, { toObject: { virtuals: true }, toJSON: { virtuals: true }, } ); CustomerSchema.virtual("info").get(function () { return this.name + " is awesome"; }); const InvoiceSchema = new Schema( { customer: { type: Schema.Types.ObjectId, ref: "Customer" }, amount: { type: Number }, receipt: { type: String }, products: [{ type: Schema.Types.ObjectId, ref: "Product" }], }, { toObject: { virtuals: true }, toJSON: { virtuals: true }, versionKey: "__version", } ); const RepeatCustomerSchema = new BaseCustomerSchema({ account: { type: Schema.Types.ObjectId, ref: "Account" }, visits: { type: Number }, status: { type: String }, job: { type: String }, }); const AccountSchema = new Schema({ accountNumber: String, points: Number, }); const HooksSchema = new Schema({ preSaveError: Boolean, postSaveError: Boolean, }); HooksSchema.pre("save", true, function (next, done) { next(); setTimeout(() => { done(this.preSaveError ? new Error("AsyncPreSaveError") : null); }, 42); }); HooksSchema.post("save", function (doc, next) { setTimeout(() => { next(doc.postSaveError ? new Error("AsyncPostSaveError") : null); }, 42); }); function initialize(opts, callback) { if (typeof opts === "function") { callback = opts; opts = {}; } opts = { connect: true, ...opts, }; if (!mongoose.models.Customer) { mongoose.model("Customer", CustomerSchema); } if (!mongoose.models.Invoice) { mongoose.model("Invoice", InvoiceSchema); } if (!mongoose.models.Product) { mongoose.model("Product", ProductSchema); } if (!mongoose.models.RepeatCustomer) { mongoose.models.Customer.discriminator( "RepeatCustomer", RepeatCustomerSchema ); } if (!mongoose.models.Account) { mongoose.model("Account", AccountSchema); } if (!mongoose.models.Hook) { mongoose.model("Hook", HooksSchema); } if (opts.connect) { const uri = process.env.MONGO_URL || "mongodb://localhost/database"; mongoose.connect(uri).then(function () { callback(); }); } else if (typeof callback === "function") { callback(); } } function reset(callback) { Promise.all([ mongoose.models.Customer.deleteMany().exec(), mongoose.models.Invoice.deleteMany().exec(), mongoose.models.Product.deleteMany().exec(), mongoose.models.RepeatCustomer.deleteMany().exec(), mongoose.models.Account.deleteMany().exec(), ]) .then(() => callback()) .catch(callback); } function close(callback) { mongoose.connection.close() .then(()=>{ callback() }) .catch(callback) } return { initialize: initialize, models: mongoose.models, reset: reset, close: close, }; } ================================================ FILE: test/integration/update.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; const invalidId = "invalid-id"; const randomId = new mongoose.Types.ObjectId().toHexString(); const updateMethods = ["PATCH", "POST", "PUT"]; describe("Update documents", () => { describe("findOneAndUpdate: true", () => { let app = createFn(); let server; let customers; let products; let invoice; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { findOneAndUpdate: true, restify: app.isRestify, }); serve(app, db.models.Invoice, { findOneAndUpdate: true, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create([ { name: "Bob", }, { name: "John", }, ]) .then((createdCustomers) => { customers = createdCustomers; return db.models.Product.create([ { name: "Bobsleigh", }, { name: "Jacket", }, ]); }) .then((createdProducts) => { products = createdProducts; return db.models.Invoice.create({ customer: customers[0]._id, products: createdProducts, amount: 100, }); }) .then((createdInvoice) => { invoice = createdInvoice; return db.models.Customer.create({ name: "Jane", purchases: [ { item: products[0]._id, number: 1, }, { item: products[1]._id, number: 3, }, ], returns: [products[0]._id, products[1]._id], }); }) .then((customer) => { customers.push(customer); }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200 - empty body`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); done(); } ); }); it(`${method} /Customer/:id 200 - created id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { name: "Mike", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Mike"); done(); } ); }); it(`${method} /Customer/:id 400 - cast error`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { age: "not a number", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); delete body.reason; assert.deepEqual(body, { kind: "Number", message: 'Cast to Number failed for value "not a number" (type string) at path "age"', name: "CastError", path: "age", stringValue: '"not a number"', value: "not a number", valueType: "string", }); done(); } ); }); it(`${method} /Customer/:id 400 - mongo error`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.equal(body.name, "MongoServerError"); // Remove extra whitespace and allow code 11001 for MongoDB < 3 assert.ok( body.message .replace(/\s+/g, " ") .replace("exception: ", "") .match( /E11000 duplicate key error (?:index|collection): database.customers(?:\.\$| index: )name_1 dup key: { (?:name|): "John" }/ ) !== null ); assert.ok(body.code === 11000 || body.code === 11001); assert.ok(!body.codeName || body.codeName === "DuplicateKey"); // codeName is optional assert.equal(body.ok, 0); done(); } ); }); it(`${method} /Customer/:id 400 - missing content type`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); done(); } ); }); it(`${method} /Customer/:id 400 - invalid content type`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, formData: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); done(); } ); }); it(`${method} /Customer/:id 404 - invalid id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${invalidId}`, json: { name: "Mike", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it(`${method} /Customer/:id 404 - random id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${randomId}`, json: { name: "Mike", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and product ids as strings`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id.toHexString(), products: products[1]._id.toHexString(), }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and products ids as strings`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id.toHexString(), products: [products[1]._id.toHexString()], }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and product ids`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id, products: products[1]._id, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and products ids`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id, products: [products[1]._id], }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); describe("populated subdocument", () => { it(`${method} /Invoice/:id 200 - update with populated customer`, (done) => { db.models.Invoice.findById(invoice._id) .populate("customer") .exec() .then((invoice) => { assert.notEqual(invoice.amount, 200); invoice.amount = 200; request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: invoice, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.amount, 200); assert.equal(body.customer, invoice.customer._id); done(); } ); }) .catch(done); }); it(`${method} /Invoice/:id 200 - update with populated products`, (done) => { db.models.Invoice.findById(invoice._id) .populate("products") .exec() .then((invoice) => { assert.notEqual(invoice.amount, 200); invoice.amount = 200; request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: invoice, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.amount, 200); assert.deepEqual(body.products, [ invoice.products[0]._id.toHexString(), invoice.products[1]._id.toHexString(), ]); done(); } ); }) .catch(done); }); it(`${method} /Invoice/:id?populate=customer,products 200 - update with populated customer`, (done) => { db.models.Invoice.findById(invoice._id) .populate("customer products") .exec() .then((invoice) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, qs: { populate: "customer,products", }, json: invoice, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.customer._id, invoice.customer._id); assert.equal(body.customer.name, invoice.customer.name); assert.ok(body.products); assert.equal( body.products[0]._id, invoice.products[0]._id.toHexString() ); assert.equal( body.products[0].name, invoice.products[0].name ); assert.equal( body.products[1]._id, invoice.products[1]._id.toHexString() ); assert.equal( body.products[1].name, invoice.products[1].name ); done(); } ); }) .catch(done); }); it(`${method} /Customer/:id 200 - update with reduced count of populated returns`, (done) => { db.models.Customer.findOne({ name: "Jane" }) .populate("purchases returns") .exec() .then((customer) => { customer.returns = [customer.returns[1]]; request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { populate: "returns,purchases.item", }, json: customer, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.returns); assert.equal(body.returns.length, 1); assert.equal(body.returns[0]._id, products[1]._id); done(); } ); }) .catch(done); }); }); }); it("PATCH /Customer 404 (Express), 405 (Restify)", (done) => { request.patch( { url: `${testUrl}/api/v1/Customer`, json: {}, }, (err, res) => { assert.ok(!err); if (app.isRestify) { assert.equal(res.statusCode, 405); } else { assert.equal(res.statusCode, 404); } done(); } ); }); it("PUT /Customer 404 (Express), 405 (Restify)", (done) => { request.put( { url: `${testUrl}/api/v1/Customer`, json: {}, }, (err, res) => { assert.ok(!err); if (app.isRestify) { assert.equal(res.statusCode, 405); } else { assert.equal(res.statusCode, 404); } done(); } ); }); }); describe("findOneAndUpdate: false", () => { let app = createFn(); let server; let customers; let products; let invoice; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { findOneAndUpdate: false, restify: app.isRestify, }); serve(app, db.models.Invoice, { findOneAndUpdate: false, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create([ { name: "Bob", }, { name: "John", }, ]) .then((createdCustomers) => { customers = createdCustomers; return db.models.Product.create([ { name: "Bobsleigh", }, { name: "Jacket", }, ]); }) .then((createdProducts) => { products = createdProducts; return db.models.Invoice.create({ customer: customers[0]._id, products: createdProducts, amount: 100, }); }) .then((createdInvoice) => { invoice = createdInvoice; return db.models.Customer.create({ name: "Jane", purchases: [ { item: products[0]._id, number: 1, }, { item: products[1]._id, number: 3, }, ], returns: [products[0]._id, products[1]._id], }); }) .then((customer) => { customers.push(customer); }) .then(done) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); updateMethods.forEach((method) => { it(`${method} /Customer/:id 200 - empty body`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: {}, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Bob"); done(); } ); }); it(`${method} /Customer/:id 200 - created id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { name: "Mike", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.name, "Mike"); done(); } ); }); it(`${method} /Customer/:id 400 - validation error`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { age: "not a number", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(body, { name: "ValidationError", _message: "Customer validation failed", message: 'Customer validation failed: age: Cast to Number failed for value "not a number" (type string) at path "age"', errors: { age: { kind: "Number", message: 'Cast to Number failed for value "not a number" (type string) at path "age"', name: "CastError", path: "age", stringValue: '"not a number"', value: "not a number", valueType: "string", }, }, }); done(); } ); }); it(`${method} /Customer/:id 400 - mongo error`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, json: { name: "John", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); // Remove extra whitespace, allow 6, 8, or 9 keys and code 11001 for MongoDB < 3 assert.equal(body.name, "MongoServerError"); assert.ok( body.message .replace(/\s+/g, " ") .replace("exception: ", "") .match( /E11000 duplicate key error (?:index|collection): database.customers(?:\.\$| index: )name_1 dup key: { (?:name|): "John" }/ ) !== null ); assert.ok(body.code === 11000 || body.code === 11001); assert.ok(!body.writeErrors || body.writeErrors.length === 1); done(); } ); }); it(`${method} /Customer/:id 400 - missing content type`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "missing_content_type", }); done(); } ); }); it(`${method} /Customer/:id 400 - invalid content type`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${customers[0]._id}`, formData: { name: "Mike", }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 400); assert.deepEqual(JSON.parse(body), { name: "Error", message: "invalid_content_type", }); done(); } ); }); it(`${method} /Customer/:id 404 - invalid id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${invalidId}`, json: { name: "Mike", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it(`${method} /Customer/:id 404 - random id`, (done) => { request( { method, url: `${testUrl}/api/v1/Customer/${randomId}`, json: { name: "Mike", }, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 404); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and product ids as strings`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id.toHexString(), products: products[1]._id.toHexString(), }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and products ids as strings`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id.toHexString(), products: [products[1]._id.toHexString()], }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and product ids`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id, products: products[1]._id, }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); it(`${method} /Invoice/:id 200 - referencing customer and products ids`, (done) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: { customer: customers[1]._id, products: [products[1]._id], }, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.customer, customers[1]._id); assert.equal(body.products[0], products[1]._id); done(); } ); }); describe("populated subdocument", () => { it(`${method} /Invoice/:id 200 - update with populated customer`, (done) => { db.models.Invoice.findById(invoice._id) .populate("customer") .exec() .then((invoice) => { assert.notEqual(invoice.amount, 200); invoice.amount = 200; request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: invoice, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.amount, 200); assert.equal(body.customer, invoice.customer._id); done(); } ); }) .catch(done); }); it(`${method} /Invoice/:id 200 - update with populated products`, (done) => { db.models.Invoice.findById(invoice._id) .populate("products") .exec() .then((invoice) => { assert.notEqual(invoice.amount, 200); invoice.amount = 200; request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, json: invoice, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.amount, 200); assert.deepEqual(body.products, [ invoice.products[0]._id.toHexString(), invoice.products[1]._id.toHexString(), ]); done(); } ); }) .catch(done); }); it(`${method} /Invoice/:id?populate=customer,products 200 - update with populated customer`, (done) => { db.models.Invoice.findById(invoice._id) .populate("customer products") .exec() .then((invoice) => { request( { method, url: `${testUrl}/api/v1/Invoice/${invoice._id}`, qs: { populate: "customer,products", }, json: invoice, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.customer); assert.equal(body.customer._id, invoice.customer._id); assert.equal(body.customer.name, invoice.customer.name); assert.ok(body.products); assert.equal( body.products[0]._id, invoice.products[0]._id.toHexString() ); assert.equal( body.products[0].name, invoice.products[0].name ); assert.equal( body.products[1]._id, invoice.products[1]._id.toHexString() ); assert.equal( body.products[1].name, invoice.products[1].name ); done(); } ); }) .catch(done); }); it(`${method} /Customer/:id 200 - update with reduced count of populated returns`, (done) => { db.models.Customer.findOne({ name: "Jane" }) .populate("purchases returns") .exec() .then((customer) => { customer.returns = [customer.returns[1]]; request( { method, url: `${testUrl}/api/v1/Customer/${customer._id}`, qs: { populate: "returns,purchases.item", }, json: customer, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.ok(body.returns); assert.equal(body.returns.length, 1); assert.equal(body.returns[0]._id, products[1]._id); done(); } ); }) .catch(done); }); }); }); it("PATCH /Customer 404 (Express), 405 (Restify)", (done) => { request.patch( { url: `${testUrl}/api/v1/Customer`, json: {}, }, (err, res) => { assert.ok(!err); if (app.isRestify) { assert.equal(res.statusCode, 405); } else { assert.equal(res.statusCode, 404); } done(); } ); }); it("PUT /Customer 404 (Express), 405 (Restify)", (done) => { request.put( { url: `${testUrl}/api/v1/Customer`, json: {}, }, (err, res) => { assert.ok(!err); if (app.isRestify) { assert.equal(res.statusCode, 405); } else { assert.equal(res.statusCode, 404); } done(); } ); }); }); }); } ================================================ FILE: test/integration/virtuals.mjs ================================================ import assert from "assert"; import request from "request"; import { serve } from "../../dist/express-restify-mongoose.js"; import setupDb from "./setup.mjs"; export default function (createFn, setup, dismantle) { const db = setupDb(); const testPort = 30023; const testUrl = `http://localhost:${testPort}`; describe("virtuals", () => { describe("lean: true", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { lean: true, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then(() => done()) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200 - unavailable", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].info, undefined); done(); } ); }); }); describe("lean: false", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { lean: false, restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then(() => done()) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200 - available", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res, body) => { assert.ok(!err); assert.equal(res.statusCode, 200); assert.equal(body.length, 1); assert.equal(body[0].info, "Bob is awesome"); done(); } ); }); }); describe("readPreference: secondary", () => { let app = createFn(); let server; before((done) => { setup((err) => { if (err) { return done(err); } serve(app, db.models.Customer, { readPreference: "secondary", restify: app.isRestify, }); server = app.listen(testPort, done); }); }); beforeEach((done) => { db.reset((err) => { if (err) { return done(err); } db.models.Customer.create({ name: "Bob", }) .then(() => done()) .catch(done); }); }); after((done) => { dismantle(app, server, done); }); it("GET /Customer 200 - available", (done) => { request.get( { url: `${testUrl}/api/v1/Customer`, json: true, }, (err, res) => { assert.ok(!err); assert.equal(res.statusCode, 200); done(); } ); }); }); }); } ================================================ FILE: test/restify.mjs ================================================ import restify from "restify"; import accessTests from "./integration/access.mjs"; import contextFilterTests from "./integration/contextFilter.mjs"; import createTests from "./integration/create.mjs"; import deleteTests from "./integration/delete.mjs"; import hookTests from "./integration/hooks.mjs"; import middlewareTests from "./integration/middleware.mjs"; import optionsTests from "./integration/options.mjs"; import readTests from "./integration/read.mjs"; import updateTests from "./integration/update.mjs"; import virtualsTests from "./integration/virtuals.mjs"; import setupDb from "./integration/setup.mjs"; const db = setupDb(); function Restify() { let app = restify.createServer(); app.use(restify.plugins.queryParser()); app.use(restify.plugins.bodyParser()); app.isRestify = true; return app; } function setup(callback) { db.initialize((err) => { if (err) { return callback(err); } db.reset(callback); }); } function dismantle(app, server, callback) { db.close((err) => { if (err) { return callback(err); } if (app.close) { return app.close(callback); } server.close(callback); }); } function runTests(createFn) { describe(createFn.name, () => { createTests(createFn, setup, dismantle); readTests(createFn, setup, dismantle); updateTests(createFn, setup, dismantle); deleteTests(createFn, setup, dismantle); accessTests(createFn, setup, dismantle); contextFilterTests(createFn, setup, dismantle); hookTests(createFn, setup, dismantle); middlewareTests(createFn, setup, dismantle); optionsTests(createFn, setup, dismantle); virtualsTests(createFn, setup, dismantle); }); } runTests(Restify); ================================================ FILE: test/unit/buildQuery.mjs ================================================ import assert from "assert"; import sinon from "sinon"; import { getBuildQuery } from "../../dist/buildQuery.js"; describe("buildQuery", () => { let query = { where: sinon.spy(), skip: sinon.spy(), limit: sinon.spy(), sort: sinon.spy(), select: sinon.spy(), populate: sinon.spy(), distinct: sinon.spy(), }; afterEach(() => { for (let key in query) { query[key].resetHistory(); } }); it("does not call any methods and returns a query object", () => { return getBuildQuery({})(query).then((result) => { for (let key in query) { sinon.assert.notCalled(query[key]); } assert.equal(result, query); }); }); describe("query", () => { it("calls where and returns a query object", () => { let queryOptions = { query: "foo", }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.where); sinon.assert.calledWithExactly(query.where, queryOptions.query); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.limit); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.select); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); }); describe("skip", () => { it("calls skip and returns a query object", () => { let queryOptions = { skip: "1", }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.skip); sinon.assert.calledWithExactly(query.skip, queryOptions.skip); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.limit); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.select); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); }); describe("limit", () => { it("calls limit and returns a query object", () => { let queryOptions = { limit: "1", }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.limit); sinon.assert.calledWithExactly(query.limit, queryOptions.limit); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.select); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); it("calls limit and returns a query object", () => { let options = { limit: 1, }; let queryOptions = { limit: "2", }; return getBuildQuery(options)(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.limit); sinon.assert.calledWithExactly(query.limit, options.limit); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.select); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); it("does not call limit on count endpoint and returns a query object", () => { let queryOptions = { limit: "2", }; query.op = "countDocuments"; return getBuildQuery({})(query, queryOptions).then((result) => { delete query.op; for (let key in query) { sinon.assert.notCalled(query[key]); } assert.equal(result, query); }); }); it("does not call limit on count endpoint and returns a query object", () => { let options = { limit: 1, }; let queryOptions = { limit: "2", }; query.op = "countDocuments"; return getBuildQuery(options)(query, queryOptions).then((result) => { delete query.op; for (let key in query) { sinon.assert.notCalled(query[key]); } assert.equal(result, query); }); }); it("does not call limit on queries that have a distinct option set and returns the query object", () => { let options = { limit: 1, }; let queryOptions = { distinct: "name", }; return getBuildQuery(options)(query, queryOptions).then((result) => { for (let key in query) { if (key === "distinct") continue; sinon.assert.notCalled(query[key]); } sinon.assert.called(query.distinct); assert.equal(result, query); }); }); }); describe("sort", () => { it("calls sort and returns a query object", () => { let queryOptions = { sort: "foo", }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.sort); sinon.assert.calledWithExactly(query.sort, queryOptions.sort); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.limit); sinon.assert.notCalled(query.select); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); }); describe("select", () => { it("accepts an object", () => { let queryOptions = { select: { foo: 1, bar: 0, }, }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.select); sinon.assert.calledWithExactly(query.select, { foo: 1, bar: 0, }); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.limit); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); }); describe("populate", () => { it("accepts an object wrapped in an array to populate a path", () => { let queryOptions = { populate: [ { path: "foo.bar", select: "baz", match: { qux: "quux" }, options: { sort: "baz" }, }, ], }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.populate); sinon.assert.calledWithExactly(query.populate, [ { path: "foo.bar", select: "baz", match: { qux: "quux" }, options: { sort: "baz" }, }, ]); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.limit); sinon.assert.notCalled(query.select); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.distinct); assert.equal(result, query); }); }); }); describe("distinct", () => { it("calls distinct and returns a query object", () => { let queryOptions = { distinct: "foo", }; return getBuildQuery({})(query, queryOptions).then((result) => { sinon.assert.calledOnce(query.distinct); sinon.assert.calledWithExactly(query.distinct, "foo"); sinon.assert.notCalled(query.where); sinon.assert.notCalled(query.skip); sinon.assert.notCalled(query.limit); sinon.assert.notCalled(query.sort); sinon.assert.notCalled(query.populate); sinon.assert.notCalled(query.select); assert.equal(result, query); }); }); }); }); ================================================ FILE: test/unit/detective.mjs ================================================ import assert from "assert"; import mongoose, { Schema } from "mongoose"; import { detective } from "../../dist/detective.js"; describe("detective", () => { const InvoiceSchema = new Schema({ customer: { type: Schema.Types.ObjectId, ref: "Customer" }, very: { deep: { ref: { type: Schema.Types.ObjectId, ref: "Reference" }, }, }, products: [{ type: Schema.Types.ObjectId, ref: "Product" }], }); mongoose.model("Invoice", InvoiceSchema); it("returns undefined when path does not exist", () => { const modelName = detective(mongoose.models.Invoice, "foo.bar"); assert.equal(modelName, undefined); }); it("returns undefined when path is not a ref", () => { const modelName = detective(mongoose.models.Invoice, "_id"); assert.equal(modelName, undefined); }); it("returns the referenced model name", () => { const modelName = detective(mongoose.models.Invoice, "customer"); assert.equal(modelName, "Customer"); }); it("returns the referenced model name when ref is an array", () => { const modelName = detective(mongoose.models.Invoice, "products"); assert.equal(modelName, "Product"); }); it("returns the referenced model name at a deep path", () => { const modelName = detective(mongoose.models.Invoice, "very.deep.ref"); assert.equal(modelName, "Reference"); }); }); ================================================ FILE: test/unit/errorHandler.mjs ================================================ import assert from "assert"; import mongoose from "mongoose"; import sinon from "sinon"; import { getErrorHandler } from "../../dist/errorHandler.js"; describe("errorHandler", () => { it("is a function", () => { assert.equal(typeof getErrorHandler, "function"); }); it("returns a function", () => { assert.equal(typeof getErrorHandler(), "function"); }); it("sets statusCode 400 and calls onError", () => { const options = { onError: sinon.spy(), }; const req = { erm: {}, params: {}, }; const err = new Error("Something went wrong"); getErrorHandler(options)(err, req); sinon.assert.calledOnce(options.onError); assert.equal(req.erm.statusCode, 400); }); it("sets statusCode 400 and calls onError", () => { const options = { onError: sinon.spy(), idProperty: "42", }; const req = { erm: {}, params: { id: "42", }, }; const err = new Error("Something went wrong"); getErrorHandler(options)(err, req); sinon.assert.calledOnce(options.onError); assert.equal(req.erm.statusCode, 400); }); it("sets statusCode 404 and calls onError", () => { const options = { onError: sinon.spy(), idProperty: "_id", }; const req = { erm: {}, params: { id: "42", }, }; const err = new mongoose.CastError("type", "42", "_id"); getErrorHandler(options)(err, req); sinon.assert.calledOnce(options.onError); assert.equal(req.erm.statusCode, 404); }); }); ================================================ FILE: test/unit/middleware/access.mjs ================================================ import assert from "assert"; import sinon from "sinon"; import { getAccessHandler } from "../../../dist/middleware/access.js"; describe("access", () => { let next = sinon.spy(); let onError = sinon.spy(); afterEach(() => { next.resetHistory(); onError.resetHistory(); }); describe("returns (sync)", () => { it("adds access field to req", () => { let req = { erm: {}, }; getAccessHandler({ access: () => { return "private"; }, })(req, {}, next); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); assert.equal(req.access, "private"); }); it("throws an exception with unsupported parameter", () => { let req = { erm: {}, }; assert.throws(() => { getAccessHandler({ access: () => { return "foo"; }, })(req, {}, next); }, Error('Unsupported access, must be "public", "private" or "protected"')); sinon.assert.notCalled(next); assert.equal(req.access, undefined); }); }); describe("yields (async)", (done) => { it("adds access field to req", () => { let req = { erm: {}, }; getAccessHandler({ access: () => { return Promise.resolve("private"); }, })(req, {}, () => { assert.equal(req.access, "private"); done(); }); }); it("calls onError", (done) => { let req = { erm: {}, }; let err = new Error("Something bad happened"); getAccessHandler({ access: () => { return Promise.reject(err); }, onError, })(req, {}, next); setTimeout(() => { sinon.assert.calledOnce(onError); sinon.assert.calledWithExactly(onError, err, req, {}, next); sinon.assert.notCalled(next); assert.equal(req.access, undefined); done(); }); }); it("throws an exception with unsupported parameter", (done) => { let req = { erm: {}, }; getAccessHandler({ access: () => { return Promise.resolve("foo"); }, onError, })(req, {}, next); setTimeout(() => { sinon.assert.calledOnce(onError); sinon.assert.notCalled(next); assert.equal(req.access, undefined); done(); }); }); }); }); ================================================ FILE: test/unit/middleware/ensureContentType.mjs ================================================ import assert from "assert"; import sinon from "sinon"; import { getEnsureContentTypeHandler } from "../../../dist/middleware/ensureContentType.js"; describe("ensureContentType", () => { let onError = sinon.spy(); let next = sinon.spy(); afterEach(() => { onError.resetHistory(); next.resetHistory(); }); it("calls next with an error (missing_content_type)", () => { let req = { erm: {}, headers: {}, params: {}, }; getEnsureContentTypeHandler({ onError })(req, {}, next); sinon.assert.calledOnce(onError); sinon.assert.calledWithExactly( onError, sinon.match.instanceOf(Error) /*new Error('missing_content_type')*/, req, {}, next ); sinon.assert.notCalled(next); assert.equal(req.access, undefined); }); it("calls next with an error (invalid_content_type)", () => { let req = { erm: {}, headers: { "content-type": "invalid/type", }, params: {}, }; getEnsureContentTypeHandler({ onError })(req, {}, next); sinon.assert.calledOnce(onError); sinon.assert.calledWithExactly( onError, sinon.match.instanceOf(Error) /*new Error('invalid_content_type')*/, req, {}, next ); sinon.assert.notCalled(next); assert.equal(req.access, undefined); }); it("calls next", () => { let req = { headers: { "content-type": "application/json", }, params: {}, }; getEnsureContentTypeHandler({ onError })(req, {}, next); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); }); }); ================================================ FILE: test/unit/middleware/onError.mjs ================================================ import sinon from "sinon"; import { getOnErrorHandler } from "../../../dist/middleware/onError.js"; describe("onError", () => { const req = { erm: { statusCode: 500, }, }; let res = { setHeader: () => undefined, status: function () { return this; }, send: () => undefined, }; let setHeader = sinon.spy(res, "setHeader"); let status = sinon.spy(res, "status"); let send = sinon.spy(res, "send"); let next = sinon.spy(); afterEach(() => { setHeader.resetHistory(); status.resetHistory(); send.resetHistory(); next.resetHistory(); }); it("with express", () => { getOnErrorHandler(true)(new Error("An error occurred"), req, res, next); sinon.assert.calledOnce(setHeader); sinon.assert.calledWithExactly( setHeader, "Content-Type", "application/json" ); sinon.assert.calledOnce(status); sinon.assert.calledWithExactly(status, 500); sinon.assert.calledOnce(send); sinon.assert.calledWithExactly(send, { message: "An error occurred", name: "Error", }); sinon.assert.notCalled(next); }); it("with restify", () => { getOnErrorHandler(false)(new Error("An error occurred"), req, res, next); sinon.assert.calledOnce(setHeader); sinon.assert.calledWithExactly( setHeader, "Content-Type", "application/json" ); sinon.assert.notCalled(status); sinon.assert.calledOnce(send); sinon.assert.calledWithExactly(send, 500, { message: "An error occurred", name: "Error", }); sinon.assert.notCalled(next); }); }); ================================================ FILE: test/unit/middleware/outputFn.mjs ================================================ import sinon from "sinon"; import { getOutputFnHandler } from "../../../dist/middleware/outputFn.js"; describe("outputFn", () => { let res = { sendStatus: () => undefined, status: function () { return this; }, json: () => undefined, send: () => undefined, }; let sendStatus = sinon.spy(res, "sendStatus"); let status = sinon.spy(res, "status"); let json = sinon.spy(res, "json"); let send = sinon.spy(res, "send"); afterEach(() => { sendStatus.resetHistory(); status.resetHistory(); json.resetHistory(); send.resetHistory(); }); describe("express", () => { it("sends status code and message", () => { getOutputFnHandler(true)( { erm: { statusCode: 200, }, }, res ); sinon.assert.calledOnce(sendStatus); sinon.assert.calledWithExactly(sendStatus, 200); sinon.assert.notCalled(status); sinon.assert.notCalled(json); sinon.assert.notCalled(send); }); it("sends data and status code", () => { let req = { erm: { statusCode: 201, result: { name: "Bob", }, }, }; getOutputFnHandler(true)(req, res); sinon.assert.calledOnce(status); sinon.assert.calledWithExactly(status, 201); sinon.assert.calledOnce(json); sinon.assert.calledWithExactly(json, { name: "Bob", }); sinon.assert.notCalled(sendStatus); sinon.assert.notCalled(send); }); }); describe("restify", () => { it("sends status code", () => { getOutputFnHandler(false)( { erm: { statusCode: 200, }, }, res ); sinon.assert.calledOnce(send); sinon.assert.calledWithExactly(send, 200, undefined); sinon.assert.notCalled(sendStatus); sinon.assert.notCalled(status); sinon.assert.notCalled(json); }); it("sends data and status code", () => { let req = { erm: { statusCode: 201, result: { name: "Bob", }, }, }; getOutputFnHandler(false)(req, res); sinon.assert.calledOnce(send); sinon.assert.calledWithExactly(send, 201, { name: "Bob", }); sinon.assert.notCalled(sendStatus); sinon.assert.notCalled(status); sinon.assert.notCalled(json); }); }); }); ================================================ FILE: test/unit/middleware/prepareOutput.mjs ================================================ import sinon from "sinon"; import { getPrepareOutputHandler } from "../../../dist/middleware/prepareOutput.js"; describe("prepareOutput", () => { let onError = sinon.spy(); let outputFn = sinon.spy(); let outputFnPromise = sinon.spy(() => { return Promise.resolve(); }); let postProcess = sinon.spy(); let next = sinon.spy(); afterEach(() => { onError.resetHistory(); outputFn.resetHistory(); outputFnPromise.resetHistory(); next.resetHistory(); }); it("calls outputFn with default options and no post* middleware", () => { let req = { method: "GET", erm: {}, }; let options = { onError: onError, outputFn: outputFn, }; getPrepareOutputHandler(options)(req, {}, next); sinon.assert.calledOnce(outputFn); sinon.assert.calledWithExactly(outputFn, req, {}); sinon.assert.notCalled(onError); sinon.assert.notCalled(next); }); it("calls outputFn with default options and no post* middleware (async)", () => { let req = { method: "GET", erm: {}, }; let options = { onError: onError, outputFn: outputFnPromise, }; getPrepareOutputHandler(options)(req, {}, next); sinon.assert.calledOnce(outputFnPromise); sinon.assert.calledWithExactly(outputFnPromise, req, {}); sinon.assert.notCalled(onError); sinon.assert.notCalled(next); }); it("calls postProcess with default options and no post* middleware", () => { let req = { method: "GET", erm: {}, }; let options = { onError: onError, outputFn: outputFn, postProcess: postProcess, }; getPrepareOutputHandler(options)(req, {}, next); sinon.assert.calledOnce(outputFn); sinon.assert.calledWithExactly(outputFn, req, {}); sinon.assert.calledOnce(postProcess); sinon.assert.calledWithExactly(postProcess, req, {}); sinon.assert.notCalled(onError); sinon.assert.notCalled(next); }); it("calls postProcess with default options and no post* middleware (async outputFn)", () => { let req = { method: "GET", erm: {}, }; let options = { onError: onError, outputFn: outputFnPromise, postProcess: postProcess, }; getPrepareOutputHandler(options)(req, {}, next); sinon.assert.calledOnce(outputFnPromise); sinon.assert.calledWithExactly(outputFnPromise, req, {}); sinon.assert.calledOnce(postProcess); sinon.assert.calledWithExactly(postProcess, req, {}); sinon.assert.notCalled(onError); sinon.assert.notCalled(next); }); }); ================================================ FILE: test/unit/middleware/prepareQuery.mjs ================================================ import assert from "assert"; import sinon from "sinon"; import { getPrepareQueryHandler } from "../../../dist/middleware/prepareQuery.js"; describe("prepareQuery", () => { let options = { onError: sinon.spy(), allowRegex: true, }; let next = sinon.spy(); afterEach(() => { options.onError.resetHistory(); next.resetHistory(); }); describe("jsonQueryParser", () => { it("converts $regex to undefined", () => { let req = { query: { query: '{"foo":{"$regex":"bar"}}', }, }; getPrepareQueryHandler({ ...options, allowRegex: false })(req, {}, next); sinon.assert.calledOnce(options.onError); sinon.assert.calledWithExactly( options.onError, sinon.match.instanceOf(Error) /*new Error('invalid_json_query')*/, req, {}, next ); sinon.assert.notCalled(next); }); it("converts [] to $in", () => { let req = { query: { query: '{"foo":["bar"]}', }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { query: { foo: { $in: ["bar"] }, }, }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); }); it("calls next when query is empty", () => { getPrepareQueryHandler(options)({}, {}, next); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("ignores keys that are not whitelisted and calls next", () => { let req = { query: { foo: "bar", }, }; getPrepareQueryHandler(options)(req, {}, next); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls next when query key is valid json", () => { let req = { query: { query: '{"foo":"bar"}', }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { query: JSON.parse(req.query.query), }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls onError when query key is invalid json", () => { let req = { erm: {}, params: {}, query: { query: "not json", }, }; getPrepareQueryHandler(options)(req, {}, next); sinon.assert.calledOnce(options.onError); sinon.assert.calledWithExactly( options.onError, sinon.match.instanceOf(Error) /*new Error('invalid_json_query')*/, req, {}, next ); sinon.assert.notCalled(next); }); it("calls next when sort key is valid json", () => { let req = { query: { sort: '{"foo":"asc"}', }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { sort: JSON.parse(req.query.sort), }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls next when sort key is a string", () => { let req = { query: { sort: "foo", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, req.query); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls next when skip key is a string", () => { let req = { query: { skip: "1", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, req.query); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls next when limit key is a string", () => { let req = { query: { limit: "1", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, req.query); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls next when distinct key is a string", () => { let req = { query: { distinct: "foo", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, req.query); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("calls next when populate key is a string", () => { let req = { query: { populate: "foo", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { populate: [ { path: "foo", strictPopulate: false, }, ], }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); describe("select", () => { it("parses a string to include fields", () => { let req = { query: { select: "foo", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { select: { foo: 1, }, }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("parses a string to exclude fields", () => { let req = { query: { select: "-foo", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { select: { foo: 0, }, }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("parses a comma separated list of fields to include", () => { let req = { query: { select: "foo,bar", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { select: { foo: 1, bar: 1, }, }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("parses a comma separated list of fields to exclude", () => { let req = { query: { select: "-foo,-bar", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { select: { foo: 0, bar: 0, }, }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("parses a comma separated list of nested fields", () => { let req = { query: { select: "foo.bar,baz.qux.quux", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { select: { "foo.bar": 1, "baz.qux.quux": 1, }, }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); }); describe("populate", () => { it("parses a string to populate a path", () => { let req = { query: { populate: "foo", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { populate: [ { path: "foo", strictPopulate: false, }, ], }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("parses a string to populate multiple paths", () => { let req = { query: { populate: "foo,bar", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { populate: [ { path: "foo", strictPopulate: false, }, { path: "bar", strictPopulate: false, }, ], }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("accepts an object to populate a path", () => { let req = { query: { populate: { path: "foo.bar", select: "baz", match: { qux: "quux" }, options: { sort: "baz" }, }, }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { populate: [ { path: "foo.bar", select: "baz", match: { qux: "quux" }, options: { sort: "baz" }, strictPopulate: false, }, ], }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); it("parses a string to populate and select fields", () => { let req = { query: { populate: "foo", select: "foo.bar,foo.baz", }, }; getPrepareQueryHandler(options)(req, {}, next); assert.deepEqual(req.erm.query, { populate: [ { path: "foo", select: "bar baz", strictPopulate: false, }, ], }); sinon.assert.calledOnce(next); sinon.assert.calledWithExactly(next); sinon.assert.notCalled(options.onError); }); }); }); ================================================ FILE: test/unit/moredots.mjs ================================================ import assert from "assert"; import { moredots } from "../../dist/moredots.js"; describe("moredots", () => { it("recursively converts objects to dot notation", () => { const result = moredots({ foo: { bar: { baz: 42, }, }, }); assert.strictEqual(result["foo.bar.baz"], 42); }); }); ================================================ FILE: test/unit/resourceFilter.mjs ================================================ import assert from "assert"; import { Filter } from "../../dist/resource_filter.js"; import setupDb from "../integration/setup.mjs"; describe("resourceFilter", () => { describe("filterObject", () => { const db = setupDb(); db.initialize({ connect: false, }); const filter = new Filter(); filter.add(db.models.Invoice, { filteredKeys: { private: [], protected: [], }, }); filter.add(db.models.Customer, { filteredKeys: { private: ["name"], protected: [], }, }); filter.add(db.models.Product, { filteredKeys: { private: ["name"], protected: [], }, }); it("removes keys in populated document", () => { let invoice = { customer: { name: "John", }, amount: "42", }; filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, ], }); assert.deepEqual(invoice, { customer: {}, amount: "42", }); }); it("removes keys in array with populated document", () => { let invoices = [ { customer: { name: "John", }, amount: "42", }, { customer: { name: "Bob", }, amount: "3.14", }, ]; filter.filterObject(invoices, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, ], }); assert.deepEqual(invoices, [ { customer: {}, amount: "42", }, { customer: {}, amount: "3.14", }, ]); }); it("ignores undefined path", () => { let invoice = { amount: "42", }; filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "customer", }, ], }); assert.deepEqual(invoice, { amount: "42", }); }); it("skip when populate path is undefined", () => { let invoice = { customer: { name: "John", }, amount: "42", }; filter.filterObject(invoice, { populate: [{}], }); assert.deepEqual(invoice, { customer: { name: "John", }, amount: "42", }); }); it("removes keys in populated document array", () => { let invoice = { products: [ { name: "Squirt Gun", }, { name: "Water Balloons", }, ], amount: "42", }; filter.filterObject(invoice, { access: "public", modelName: db.models.Invoice.modelName, populate: [ { path: "products", }, ], }); assert.deepEqual(invoice, { products: [{}, {}], amount: "42", }); }); it("removes keys in populated document in array", () => { let customer = { name: "John", purchases: [ { item: { name: "Squirt Gun", }, }, ], }; filter.filterObject(customer, { access: "public", modelName: db.models.Customer.modelName, populate: [ { path: "purchases.item", }, ], }); assert.deepEqual(customer, { purchases: [ { item: {}, }, ], }); }); }); }); ================================================ FILE: test/unit/weedout.mjs ================================================ import assert from "assert"; import { weedout } from "../../dist/weedout.js"; describe("weedout", () => { it("removes root keys", () => { const src = { foo: "bar", }; weedout(src, "foo"); assert.equal(src.foo, undefined); }); it("ignores undefined root keys", () => { const src = { foo: "bar", }; weedout(src, "bar"); assert.deepEqual(src, { foo: "bar", }); }); it("removes nested keys", () => { const src = { foo: { bar: { baz: "42", }, }, }; weedout(src, "foo.bar.baz"); assert.deepEqual(src.foo.bar, {}); }); it("ignores undefined nested keys", () => { const src = { foo: { bar: { baz: "42", }, }, }; weedout(src, "baz.bar.foo"); assert.deepEqual(src, { foo: { bar: { baz: "42", }, }, }); }); it("removes keys inside object arrays", () => { const src = { foo: [ { bar: { baz: "3.14", }, }, { bar: { baz: "pi", }, }, ], }; weedout(src, "foo.bar.baz"); src.foo.forEach((foo) => { assert.deepEqual(foo.bar, {}); }); }); it("removes keys inside object arrays inside object arrays", () => { const src = { foo: [ { bar: [ { baz: "to", }, { baz: "be", }, ], }, { bar: [ { baz: "or", }, { baz: "not", }, ], }, ], }; weedout(src, "foo.bar.baz"); src.foo.forEach((foo) => { foo.bar.forEach((bar) => { assert.deepEqual(bar, {}); }); }); }); }); ================================================ FILE: test/unit.mjs ================================================ import "./unit/buildQuery.mjs"; import "./unit/detective.mjs"; import "./unit/errorHandler.mjs"; import "./unit/middleware/access.mjs"; import "./unit/middleware/ensureContentType.mjs"; import "./unit/middleware/onError.mjs"; import "./unit/middleware/outputFn.mjs"; import "./unit/middleware/prepareOutput.mjs"; import "./unit/middleware/prepareQuery.mjs"; import "./unit/moredots.mjs"; import "./unit/resourceFilter.mjs"; import "./unit/weedout.mjs"; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "Node", "skipLibCheck": true, "strict": true } }