Showing preview only (341K chars total). Download the full file or copy to clipboard to get everything.
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.
[](https://github.com/florianholzapfel/express-restify-mongoose/actions/workflows/node.js.yml)
[](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<unknown>;
query?: QueryOptions;
result?: Record<string, unknown> | Record<string, unknown>[];
statusCode?: number;
totalCount?: number;
};
}
}
}
================================================
FILE: index.d.ts
================================================
declare module "lodash.isplainobject" {
export default function isPlainObject(
o: unknown
): o is Record<string, unknown>;
}
================================================
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<Options, "lean" | "limit" | "readPreference">
) {
return function buildQuery<T>(
query: mongoose.Query<unknown, unknown>,
queryOptions: QueryOptions | undefined
): Promise<T> {
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<T>;
};
}
================================================
FILE: src/detective.ts
================================================
import mongoose from "mongoose";
export function detective(model: mongoose.Model<unknown>, 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<Options, "idProperty" | "onError">
) {
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<Options, "contextFilter" | "outputFn" | "onError"> =
{
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<unknown>,
options: Partial<Options> = {}
) {
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<z.infer<typeof PopulateOptionsSchema>>((field) => {
const pop: z.infer<typeof PopulateOptionsSchema> = {
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<ReturnType<typeof getQueryOptionsSchema>>;
================================================
FILE: src/middleware/access.ts
================================================
import { RequestHandler } from "express";
import { getErrorHandler } from "../errorHandler.js";
import { Access, Options } from "../types";
export function getAccessHandler(
options: Required<Pick<Options, "access" | "idProperty" | "onError">>
) {
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<Options, "idProperty" | "onError">
) {
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<T>(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<Options, "allowRegex" | "idProperty" | "onError">
) {
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<string, unknown>,
dst: Record<string, unknown> = {},
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<unknown>,
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<unknown>, 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<Record<string, unknown>[]>(
// @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<number>(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<Record<string, unknown> | 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<Record<string, unknown> | 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<string, unknown>;
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<string, unknown>) {
const dst: Record<string, unknown> = {};
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<string, unknown>;
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<string, unknown>;
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<unknown>,
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<string, unknown> | Record<string, unknown>[]
>(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<string, unknown>, excluded[i]);
}
}
return item;
}
/**
* Removes excluded keys from a document with populated subdocuments.
*/
private filterPopulatedItem<
T extends Record<string, unknown> | Record<string, unknown>[]
>(
item: T,
options: {
access: Access;
modelName: string;
populate: Exclude<QueryOptions["populate"], undefined | string>;
}
): 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<string, unknown> | Record<string, unknown>[],
options: {
access: Access;
modelName: string;
populate?: Exclude<QueryOptions["populate"], string>;
}
) {
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<unknown>;
}
>;
export type OutputFn = (req: Request, res: Response) => void | Promise<void>;
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<Access>;
contextFilter: (
model: mongoose.Model<unknown>,
req: Request,
done: (
query: mongoose.Model<unknown> | mongoose.Query<unknown, unknown>
) => 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<unknown>;
};
};
================================================
FILE: src/weedout.ts
================================================
export function weedout(obj: Record<string, unknown>, 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) => {
custo
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
SYMBOL INDEX (43 symbols across 21 files)
FILE: express.d.ts
type Request (line 7) | interface Request {
FILE: src/buildQuery.ts
function getBuildQuery (line 5) | function getBuildQuery(
FILE: src/detective.ts
function detective (line 3) | function detective(model: mongoose.Model<unknown>, path: string) {
FILE: src/errorHandler.ts
function getErrorHandler (line 5) | function getErrorHandler(
FILE: src/express-restify-mongoose.ts
function serve (line 41) | function serve(
FILE: src/getQuerySchema.ts
function getQueryOptionsSchema (line 72) | function getQueryOptionsSchema({ allowRegex }: { allowRegex: boolean }) {
type QueryOptions (line 178) | type QueryOptions = z.infer<ReturnType<typeof getQueryOptionsSchema>>;
FILE: src/middleware/access.ts
function getAccessHandler (line 5) | function getAccessHandler(
FILE: src/middleware/ensureContentType.ts
function getEnsureContentTypeHandler (line 5) | function getEnsureContentTypeHandler(
FILE: src/middleware/filterAndFindById.ts
function getFilterAndFindByIdHandler (line 7) | function getFilterAndFindByIdHandler(
FILE: src/middleware/onError.ts
function getOnErrorHandler (line 4) | function getOnErrorHandler(isExpress: boolean) {
FILE: src/middleware/outputFn.ts
function getOutputFnHandler (line 3) | function getOutputFnHandler(isExpress: boolean) {
FILE: src/middleware/prepareOutput.ts
function isDefined (line 6) | function isDefined<T>(arg: T | undefined): arg is T {
function getPrepareOutputHandler (line 10) | function getPrepareOutputHandler(
FILE: src/middleware/prepareQuery.ts
function getPrepareQueryHandler (line 6) | function getPrepareQueryHandler(
FILE: src/moredots.ts
function moredots (line 3) | function moredots(
FILE: src/operations.ts
function operations (line 11) | function operations(
FILE: src/resource_filter.ts
class Filter (line 9) | class Filter {
method add (line 12) | add(
method getExcluded (line 44) | getExcluded(options: { access: Access; modelName: string }) {
method filterItem (line 63) | private filterItem<
method filterPopulatedItem (line 90) | private filterPopulatedItem<
method filterObject (line 155) | filterObject(
FILE: src/types.ts
type Access (line 9) | type Access = "private" | "protected" | "public";
type FilteredKeys (line 11) | type FilteredKeys = {
type ExcludedMap (line 16) | type ExcludedMap = Map<
type OutputFn (line 24) | type OutputFn = (req: Request, res: Response) => void | Promise<void>;
type ReadPreference (line 26) | type ReadPreference =
type Options (line 38) | type Options = {
FILE: src/weedout.ts
function weedout (line 1) | function weedout(obj: Record<string, unknown>, path: string) {
FILE: test/express.mjs
function Express (line 20) | function Express() {
function setup (line 28) | function setup(callback) {
function dismantle (line 38) | function dismantle(app, server, callback) {
function runTests (line 52) | function runTests(createFn) {
FILE: test/integration/setup.mjs
class BaseCustomerSchema (line 13) | class BaseCustomerSchema extends Schema {
method constructor (line 14) | constructor(definition, options) {
function initialize (line 101) | function initialize(opts, callback) {
function reset (line 149) | function reset(callback) {
function close (line 161) | function close(callback) {
FILE: test/restify.mjs
function Restify (line 18) | function Restify() {
function setup (line 26) | function setup(callback) {
function dismantle (line 36) | function dismantle(app, server, callback) {
function runTests (line 50) | function runTests(createFn) {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (351K chars).
[
{
"path": ".eslintrc",
"chars": 553,
"preview": "{\n \"env\": {\n \"es2021\": true\n },\n \"extends\": [\"eslint:recommended\", \"plugin:@typescript-eslint/recommended\"],\n \"ov"
},
{
"path": ".github/workflows/node.js.yml",
"chars": 606,
"preview": "name: Test\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n build:\n runs-on: ubuntu-la"
},
{
"path": ".github/workflows/npm-publish.yml",
"chars": 978,
"preview": "name: Publish\n\non:\n release:\n types: [created]\n\npermissions:\n id-token: write\n contents: read\n\njobs:\n build:\n "
},
{
"path": ".gitignore",
"chars": 18,
"preview": "dist\nnode_modules\n"
},
{
"path": ".swcrc",
"chars": 145,
"preview": "{\n \"$schema\": \"https://json.schemastore.org/swcrc\",\n \"jsc\": {\n \"parser\": {\n \"syntax\": \"typescript\"\n },\n "
},
{
"path": ".vscode/settings.json",
"chars": 240,
"preview": "{\n \"editor.codeActionsOnSave\": {\n \"source.fixAll\": true,\n \"source.organizeImports\": true\n },\n \"editor.defaultFo"
},
{
"path": "CHANGELOG.md",
"chars": 4160,
"preview": "# Changelog\n\n### 7.0.0\n\n> **This release requires mongoose 6.x**\n\n- updated mongoose to version 6.x\n\n### 6.0.0\n\n> **This"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "Copyright (C) 2013 by Florian Holzapfel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "README.md",
"chars": 2666,
"preview": "# express-restify-mongoose\n\nEasily create a flexible REST interface for mongoose models.\n\n[: o is Record<string,"
},
{
"path": "package.json",
"chars": 2127,
"preview": "{\n \"name\": \"express-restify-mongoose\",\n \"version\": \"9.0.10\",\n \"description\": \"Easily create a flexible REST interface"
},
{
"path": "src/buildQuery.ts",
"chars": 1979,
"preview": "import mongoose from \"mongoose\";\nimport { QueryOptions } from \"./getQuerySchema.js\";\nimport { Options } from \"./types\";\n"
},
{
"path": "src/detective.ts",
"chars": 1030,
"preview": "import mongoose from \"mongoose\";\n\nexport function detective(model: mongoose.Model<unknown>, path: string) {\n const keys"
},
{
"path": "src/errorHandler.ts",
"chars": 695,
"preview": "import { ErrorRequestHandler } from \"express\";\nimport { STATUS_CODES } from \"http\";\nimport { Options } from \"./types\";\n\n"
},
{
"path": "src/express-restify-mongoose.ts",
"chars": 6113,
"preview": "import { Application } from \"express\";\nimport mongoose from \"mongoose\";\nimport { deprecate } from \"util\";\nimport { getAc"
},
{
"path": "src/getQuerySchema.ts",
"chars": 4755,
"preview": "import { z } from \"zod\";\n\nconst PopulateOptionsSchema = z.object({\n path: z.string(),\n match: z.record(z.unknown()).op"
},
{
"path": "src/middleware/access.ts",
"chars": 868,
"preview": "import { RequestHandler } from \"express\";\nimport { getErrorHandler } from \"../errorHandler.js\";\nimport { Access, Options"
},
{
"path": "src/middleware/ensureContentType.ts",
"chars": 691,
"preview": "import { RequestHandler } from \"express\";\nimport { getErrorHandler } from \"../errorHandler.js\";\nimport { Options } from "
},
{
"path": "src/middleware/filterAndFindById.ts",
"chars": 1312,
"preview": "import { RequestHandler } from \"express\";\nimport { STATUS_CODES } from \"http\";\nimport mongoose from \"mongoose\";\nimport {"
},
{
"path": "src/middleware/onError.ts",
"chars": 762,
"preview": "import { ErrorRequestHandler } from \"express\";\nimport { serializeError } from \"serialize-error\";\n\nexport function getOnE"
},
{
"path": "src/middleware/outputFn.ts",
"chars": 534,
"preview": "import { OutputFn } from \"../types\";\n\nexport function getOutputFnHandler(isExpress: boolean) {\n const fn: OutputFn = fu"
},
{
"path": "src/middleware/prepareOutput.ts",
"chars": 3125,
"preview": "import { RequestHandler } from \"express\";\nimport { getErrorHandler } from \"../errorHandler.js\";\nimport { Filter } from \""
},
{
"path": "src/middleware/prepareQuery.ts",
"chars": 719,
"preview": "import { RequestHandler } from \"express\";\nimport { getErrorHandler } from \"../errorHandler.js\";\nimport { getQueryOptions"
},
{
"path": "src/moredots.ts",
"chars": 374,
"preview": "import isPlainObject from \"lodash.isplainobject\";\n\nexport function moredots(\n src: Record<string, unknown>,\n dst: Reco"
},
{
"path": "src/operations.ts",
"chars": 11659,
"preview": "import { Request, RequestHandler } from \"express\";\nimport { STATUS_CODES } from \"http\";\nimport isPlainObject from \"lodas"
},
{
"path": "src/resource_filter.ts",
"chars": 4556,
"preview": "import dotProp from \"dot-prop\";\nimport mongoose from \"mongoose\";\nimport { detective } from \"./detective.js\";\nimport { Qu"
},
{
"path": "src/types.ts",
"chars": 1930,
"preview": "import {\n ErrorRequestHandler,\n Request,\n RequestHandler,\n Response,\n} from \"express\";\nimport mongoose from \"mongoos"
},
{
"path": "src/weedout.ts",
"chars": 544,
"preview": "export function weedout(obj: Record<string, unknown>, path: string) {\n const keys = path.split(\".\");\n\n for (let i = 0,"
},
{
"path": "test/express.mjs",
"chars": 1807,
"preview": "import bodyParser from \"body-parser\";\nimport express from \"express\";\nimport methodOverride from \"method-override\";\n\nimpo"
},
{
"path": "test/integration/access.mjs",
"chars": 55807,
"preview": "import assert from \"assert\";\nimport request from \"request\";\nimport { serve } from \"../../dist/express-restify-mongoose.j"
},
{
"path": "test/integration/contextFilter.mjs",
"chars": 5701,
"preview": "import assert from \"assert\";\nimport request from \"request\";\nimport { serve } from \"../../dist/express-restify-mongoose.j"
},
{
"path": "test/integration/create.mjs",
"chars": 12373,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport request from \"request\";\nimport { serve } from \"../."
},
{
"path": "test/integration/delete.mjs",
"chars": 6663,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport request from \"request\";\nimport { serve } from \"../."
},
{
"path": "test/integration/hooks.mjs",
"chars": 2182,
"preview": "import assert from \"assert\";\nimport request from \"request\";\nimport { serve } from \"../../dist/express-restify-mongoose.j"
},
{
"path": "test/integration/middleware.mjs",
"chars": 37437,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport request from \"request\";\nimport sinon from \"sinon\";\n"
},
{
"path": "test/integration/options.mjs",
"chars": 20905,
"preview": "import assert from \"assert\";\nimport request from \"request\";\nimport sinon from \"sinon\";\nimport { serve } from \"../../dist"
},
{
"path": "test/integration/read.mjs",
"chars": 31872,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport request from \"request\";\nimport { serve } from \"../."
},
{
"path": "test/integration/resource_filter.mjs",
"chars": 20590,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport { Filter } from \"../../dist/resource_filter.js\";\n\ni"
},
{
"path": "test/integration/setup.mjs",
"chars": 4346,
"preview": "import mongoose, { Schema } from \"mongoose\";\n\nexport default function () {\n const ProductSchema = new Schema({\n name"
},
{
"path": "test/integration/update.mjs",
"chars": 32186,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport request from \"request\";\nimport { serve } from \"../."
},
{
"path": "test/integration/virtuals.mjs",
"chars": 3807,
"preview": "import assert from \"assert\";\nimport request from \"request\";\nimport { serve } from \"../../dist/express-restify-mongoose.j"
},
{
"path": "test/restify.mjs",
"chars": 1730,
"preview": "import restify from \"restify\";\n\nimport accessTests from \"./integration/access.mjs\";\nimport contextFilterTests from \"./in"
},
{
"path": "test/unit/buildQuery.mjs",
"chars": 7880,
"preview": "import assert from \"assert\";\nimport sinon from \"sinon\";\nimport { getBuildQuery } from \"../../dist/buildQuery.js\";\n\ndescr"
},
{
"path": "test/unit/detective.mjs",
"chars": 1379,
"preview": "import assert from \"assert\";\nimport mongoose, { Schema } from \"mongoose\";\nimport { detective } from \"../../dist/detectiv"
},
{
"path": "test/unit/errorHandler.mjs",
"chars": 1566,
"preview": "import assert from \"assert\";\nimport mongoose from \"mongoose\";\nimport sinon from \"sinon\";\nimport { getErrorHandler } from"
},
{
"path": "test/unit/middleware/access.mjs",
"chars": 2419,
"preview": "import assert from \"assert\";\nimport sinon from \"sinon\";\nimport { getAccessHandler } from \"../../../dist/middleware/acces"
},
{
"path": "test/unit/middleware/ensureContentType.mjs",
"chars": 1633,
"preview": "import assert from \"assert\";\nimport sinon from \"sinon\";\nimport { getEnsureContentTypeHandler } from \"../../../dist/middl"
},
{
"path": "test/unit/middleware/onError.mjs",
"chars": 1616,
"preview": "import sinon from \"sinon\";\nimport { getOnErrorHandler } from \"../../../dist/middleware/onError.js\";\n\ndescribe(\"onError\","
},
{
"path": "test/unit/middleware/outputFn.mjs",
"chars": 2446,
"preview": "import sinon from \"sinon\";\nimport { getOutputFnHandler } from \"../../../dist/middleware/outputFn.js\";\n\ndescribe(\"outputF"
},
{
"path": "test/unit/middleware/prepareOutput.mjs",
"chars": 2589,
"preview": "import sinon from \"sinon\";\nimport { getPrepareOutputHandler } from \"../../../dist/middleware/prepareOutput.js\";\n\ndescrib"
},
{
"path": "test/unit/middleware/prepareQuery.mjs",
"chars": 9782,
"preview": "import assert from \"assert\";\nimport sinon from \"sinon\";\nimport { getPrepareQueryHandler } from \"../../../dist/middleware"
},
{
"path": "test/unit/moredots.mjs",
"chars": 338,
"preview": "import assert from \"assert\";\nimport { moredots } from \"../../dist/moredots.js\";\n\ndescribe(\"moredots\", () => {\n it(\"recu"
},
{
"path": "test/unit/resourceFilter.mjs",
"chars": 3788,
"preview": "import assert from \"assert\";\nimport { Filter } from \"../../dist/resource_filter.js\";\n\nimport setupDb from \"../integratio"
},
{
"path": "test/unit/weedout.mjs",
"chars": 1893,
"preview": "import assert from \"assert\";\nimport { weedout } from \"../../dist/weedout.js\";\n\ndescribe(\"weedout\", () => {\n it(\"removes"
},
{
"path": "test/unit.mjs",
"chars": 453,
"preview": "import \"./unit/buildQuery.mjs\";\nimport \"./unit/detective.mjs\";\nimport \"./unit/errorHandler.mjs\";\nimport \"./unit/middlewa"
},
{
"path": "tsconfig.json",
"chars": 223,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"exactOptionalPropertyTypes\": true,\n \"forceConsistentCasing"
}
]
About this extraction
This page contains the full source code of the florianholzapfel/express-restify-mongoose GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (323.5 KB), approximately 75.1k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.