UNPKG

mongoose-bread

Version:

A pagination and softdelete library for mongoose

1,390 lines (1,303 loc) 56.7 kB
"use strict"; const mongoose = require("mongoose"); const { expect } = require("chai"); const { MongooseBreadError } = require("../dist/index"); /** * @type {import('../src/index').mongooseBread} mongooseBread plugin */ const mongooseBread = require("../dist/index"); const MONGO_URI = "mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000"; const ProductCustomKeysSchema = new mongoose.Schema( { name: String, price: Number, currency: String, }, { timestamps: true, } ); ProductCustomKeysSchema.plugin(mongooseBread, { paramsIdKey: "idCustom", bulkIdsKey: "_idsCustom", bulkDocsKey: "_docsCustom", }); const ProductCustomKeys = mongoose.model( "ProductCustomKeys", ProductCustomKeysSchema ); const ProductCustomLabelsSchema = new mongoose.Schema( { name: String, price: Number, currency: String, }, { timestamps: true, } ); ProductCustomLabelsSchema.plugin(mongooseBread, { customLabels: { docs: "docsCustom", limit: "limitCustom", page: "pageCustom", pagingCounter: "pagingCounterCustom", hasNextPage: "hasNextPageCustom", hasPrevPage: "hasPrevPageCustom", nextPage: "nextPageCustom", prevPage: "prevPageCustom", totalDocs: "totalDocsCustom", totalPages: "totalPagesCustom", meta: "paginationCustom", acknowledged: "acknowledgedCustom", modifiedCount: "modifiedCountCustom", deletedCount: "deletedCountCustom", createdCount: "createdCountCustom", readCount: "readCountCustom", }, }); const ProductCustomLabels = mongoose.model( "ProductCustomLabels", ProductCustomLabelsSchema ); const ProductSchema = new mongoose.Schema( { name: String, price: Number, currency: String, }, { timestamps: true, } ); ProductSchema.plugin(mongooseBread, { searchableFields: ["name", "price", "currency", "nonExistingField"], }); const Product = mongoose.model("Product", ProductSchema); const ProductSoftDeleteSchema = new mongoose.Schema( { name: String, price: Number, currency: String, tags: [String], options: [ { name: String, }, ], }, { timestamps: true, } ); ProductSoftDeleteSchema.plugin(mongooseBread, { searchableFields: ["name", "tags", "options.name"], softDelete: true, softDeleteOptions: { validateBeforeDelete: true, indexFields: true, deletedAt: true, deletedBy: true, requestUserIdPath: "auth.user._id", }, }); const ProductSoftDelete = mongoose.model( "ProductSoftDelete", ProductSoftDeleteSchema ); const ProductTransactionSchema = new mongoose.Schema( { name: String, price: Number, currency: String, }, { timestamps: true, } ); ProductTransactionSchema.plugin(mongooseBread, { searchableFields: ["name"], runUpdateTransaction: true, }); const ProductTransaction = mongoose.model( "ProductTransaction", ProductTransactionSchema ); const ProductAtlasSearchSchema = new mongoose.Schema( { name: String, description: String, price: Number, currency: String, }, { timestamps: true, } ); ProductAtlasSearchSchema.plugin(mongooseBread, { searchableFields: ["name", "description", "price"], enableAtlasSearch: true, atlasSearchIndex: "fulltexttest", }); const ProductAtlasSearch = mongoose.model( "ProductAtlasSearch", ProductAtlasSearchSchema ); const ProductAtlasSearchWrongConfigSchema = new mongoose.Schema( { name: String, description: String, price: Number, currency: String, }, { timestamps: true, } ); ProductAtlasSearchWrongConfigSchema.plugin(mongooseBread, { searchableFields: ["name", "description"], enableAtlasSearch: true, }); const ProductAtlasSearchWrongConfig = mongoose.model( "ProductAtlasSearchWrongConfig", ProductAtlasSearchWrongConfigSchema ); describe("mongoose-bread", async function () { // #region before before(function (done) { mongoose.connect( MONGO_URI, { useUnifiedTopology: true, useNewUrlParser: true, }, done ); }); before(function (done) { mongoose.connection.db.dropDatabase(done); }); before(async function () { let products = new Array(10).fill(undefined).map( (_, i) => new ProductCustomKeys({ name: `ProductCustomKeys #${i + 1}`, price: (i + 1) * 10, currency: i < 5 ? "EUR" : "USD", }) ); return ProductCustomKeys.create(products); }); before(async function () { let products = new Array(10).fill(undefined).map( (_, i) => new ProductCustomLabels({ name: `ProductCustomLabels #${i + 1}`, price: (i + 1) * 10, currency: i < 5 ? "EUR" : "USD", }) ); return ProductCustomLabels.create(products); }); before(async function () { let products = new Array(10).fill(undefined).map( (_, i) => new Product({ name: `Product #${i + 1}`, price: (i + 1) * 10, currency: i < 5 ? "EUR" : "USD", }) ); return Product.create(products); }); before(async function () { let products = new Array(10).fill(undefined).map( (_, i) => new ProductSoftDelete({ name: `ProductSoftDelete #${i + 1}`, price: (i + 1) * 10, currency: i < 5 ? "EUR" : "USD", tags: i < 5 ? ["my", "awesome", "product"] : ["not", "listed"], options: i < 5 ? [{ name: `Option ${i + 1}` }] : [], }) ); return ProductSoftDelete.create(products); }); before(async function () { let products = new Array(10).fill(undefined).map( (_, i) => new Product({ name: `Product #${i + 1}`, price: (i + 1) * 10, currency: i < 5 ? "EUR" : "USD", }) ); return ProductTransaction.create(products); }); before(async function () { let products = new Array(10).fill(undefined).map( (_, i) => new Product({ name: `Product #${i + 1}`, description: "This is product number " + (i + 1), price: (i + 1) * 10, currency: i < 5 ? "EUR" : "USD", }) ); return ProductAtlasSearch.create(products); }); // #endregion before // #region custom properties describe("with custom property access Keys", function () { it("reads a document correctly", function () { return ProductCustomKeys.create({ name: "temp" }) .then((doc) => { const mockRequest = { params: { idCustom: doc._id } }; const options = ProductCustomKeys.breadHelper().createReadOptions({ ...mockRequest, }); return ProductCustomKeys.read(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc.name).to.equal("temp"); }); }); it("edits many documents correctly", function () { return ProductCustomKeys.create([ { name: "doc#1", currency: "EUR" }, { name: "doc#2", currency: "EUR" }, ]) .then((docs) => { const mockRequest = { body: { _idsCustom: docs.map((d) => d._id), _docsCustom: [{ currency: "USD" }], }, }; const options = ProductCustomKeys.breadHelper().createEditOptions({ ...mockRequest, }); return ProductCustomKeys.edit(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(2); const doc0 = result.docs[0]; expect(doc0).to.be.an.instanceOf(Object); expect(doc0.name).to.equal("doc#1"); expect(doc0.currency).to.equal("USD"); const doc1 = result.docs[1]; expect(doc1).to.be.an.instanceOf(Object); expect(doc1.name).to.equal("doc#2"); expect(doc1.currency).to.equal("USD"); }); }); it("adds many documents correctly", function () { const mockRequest = { body: { _docsCustom: [ { name: "temp#1", price: 5, currency: "USD" }, { name: "temp#2", price: 50, currency: "EUR" }, ], }, }; const options = ProductCustomKeys.breadHelper().createAddOptions({ ...mockRequest, }); return ProductCustomKeys.add(options).then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result).to.have.property("createdCount"); expect(result).to.have.property("acknowledged"); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(2); const doc0 = result.docs[0]; expect(doc0).to.be.an.instanceOf(Object); expect(doc0.name).to.equal("temp#1"); expect(doc0.price).to.equal(5); expect(doc0.currency).to.equal("USD"); const doc1 = result.docs[1]; expect(doc1).to.be.an.instanceOf(Object); expect(doc1.name).to.equal("temp#2"); expect(doc1.price).to.equal(50); expect(doc1.currency).to.equal("EUR"); }); }); it("destroys a document correctly", function () { return ProductCustomKeys.create({ name: "temp" }) .then((doc) => { const mockRequest = { params: { idCustom: doc._id } }; const options = ProductCustomKeys.breadHelper().createDeleteOptions({ ...mockRequest, }); return Promise.all([ ProductCustomKeys.destroy(options), Promise.resolve(doc), ]); }) .then(([result, doc]) => { expect(result).to.be.an.instanceOf(Object); expect(result.acknowledged).to.equal(true); expect(result.deletedCount).to.equal(1); return ProductCustomKeys.findById(doc._id); }) .then((doc) => { expect(doc).to.be.null; }); }); it("destroys many documents correctly", function () { return ProductCustomKeys.create([ { name: "a" }, { name: "b" }, { name: "c" }, ]) .then((docs) => { const mockRequest = { body: { _idsCustom: docs.map((d) => d._id) } }; const options = ProductCustomKeys.breadHelper().createDeleteOptions({ ...mockRequest, }); return ProductCustomKeys.destroy(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.acknowledged).to.equal(true); expect(result.deletedCount).to.equal(3); }); }); }); // #endregion custom properties // #region custom labels describe("with custom Labels", function () { it("applies customLabels on browse result", function () { const mockRequest = {}; const options = ProductCustomLabels.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductCustomLabels.browse(options).then((result) => { expect(result).to.have.keys([ "docsCustom", "paginationCustom", "acknowledgedCustom", "readCountCustom", ]); expect(result).to.have.property("paginationCustom").with.keys([ "offset", // autogenerated and not customizable "limitCustom", "pageCustom", "pagingCounterCustom", "hasNextPageCustom", "hasPrevPageCustom", "nextPageCustom", "prevPageCustom", "totalDocsCustom", "totalPagesCustom", ]); }); }); it("applies customLabels on read result", function () { return ProductCustomLabels.findOne() .then((doc) => { const mockRequest = { params: { id: doc._id } }; const options = ProductCustomLabels.breadHelper().createReadOptions({ ...mockRequest, }); return ProductCustomLabels.read(options); }) .then((result) => { expect(result).to.have.keys([ "docsCustom", "acknowledgedCustom", "readCountCustom", ]); }); }); it("applies customLabels on edit result", function () { return ProductCustomLabels.findOne() .then((doc) => { const mockRequest = { params: { id: doc._id }, body: {} }; const options = ProductCustomLabels.breadHelper().createEditOptions({ ...mockRequest, }); return ProductCustomLabels.edit(options); }) .then((result) => { expect(result).to.have.keys([ "docsCustom", "acknowledgedCustom", "modifiedCountCustom", ]); }); }); it("applies customLabels on add result", function () { const mockRequest = { body: {} }; const options = ProductCustomLabels.breadHelper().createAddOptions({ ...mockRequest, }); return ProductCustomLabels.add(options).then((result) => { expect(result).to.have.keys([ "docsCustom", "acknowledgedCustom", "createdCountCustom", ]); }); }); it("applies customLabels on destroy result", function () { return ProductCustomLabels.findOne() .then((doc) => { const mockRequest = { params: { id: doc._id } }; const options = ProductCustomLabels.breadHelper().createDeleteOptions( { ...mockRequest } ); return ProductCustomLabels.destroy(options); }) .then((result) => { expect(result).to.have.keys([ "docsCustom", "deletedCountCustom", "acknowledgedCustom", ]); }); }); }); // #endregion custom labels // #region with softDelete describe("with softDelete", function () { it("adds plugin methods to Model", function () { expect(ProductSoftDelete.browse).to.be.an.instanceOf(Function); expect(ProductSoftDelete.read).to.be.an.instanceOf(Function); expect(ProductSoftDelete.edit).to.be.an.instanceOf(Function); expect(ProductSoftDelete.add).to.be.an.instanceOf(Function); expect(ProductSoftDelete.softDelete).to.be.an.instanceOf(Function); expect(ProductSoftDelete.destroy).to.be.an.instanceOf(Function); expect(ProductSoftDelete.rehabilitate).to.be.an.instanceOf(Function); }); it("adds plugin helper getter method to Model", function () { expect(ProductSoftDelete.breadHelper).to.be.an.instanceOf(Function); }); it("adds plugin helper methods to Model via helper getter", function () { expect( ProductSoftDelete.breadHelper().createBrowseOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createBrowseDeletedOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createReadOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createReadDeletedOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createEditOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createAddOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createDeleteOptions ).to.be.an.instanceOf(Function); expect( ProductSoftDelete.breadHelper().createRehabilitateOptions ).to.be.an.instanceOf(Function); }); it("leaves the dependency methods intact", function () { expect(ProductSoftDelete.paginate).to.be.an.instanceOf(Function); expect(ProductSoftDelete.delete).to.be.an.instanceOf(Function); expect(ProductSoftDelete.deleteById).to.be.an.instanceOf(Function); expect(ProductSoftDelete.restore).to.be.an.instanceOf(Function); }); it("throws Error if invalid request is passed to helper methods", function () { let mockRequest, fn; const helper = ProductSoftDelete.breadHelper(); const { createReadDeletedOptions, createRehabilitateOptions } = helper; mockRequest = {}; fn = createReadDeletedOptions.bind(helper, { ...mockRequest }); expect(fn, "readDeletedOptions - empty request").to.throw( 'mongooseBread helper "createReadDeletedOptions" expects request.params.id to be set' ); fn = createRehabilitateOptions.bind(helper, { ...mockRequest }); expect(fn, "rehabilitateOptions - empty request").to.throw( 'mongooseBread helper "createRehabilitateOptions" expects request.body to be set' ); mockRequest = { params: { id: "invalid id" } }; fn = createReadDeletedOptions.bind(helper, { ...mockRequest }); expect(fn, "readDeletedOptions - invalid params.id").to.throw( 'mongooseBread helper "createReadDeletedOptions" expects request.params.id to be a valid ObjectId' ); fn = createRehabilitateOptions.bind(helper, { ...mockRequest }); expect(fn, "rehabilitateOptions - invalid params.id").to.throw( 'mongooseBread helper "createRehabilitateOptions" expects request.params.id to be a valid ObjectId' ); mockRequest = { body: {} }; fn = createReadDeletedOptions.bind(helper, { ...mockRequest }); expect(fn, "readDeletedOptions - empty request.body").to.throw( 'mongooseBread helper "createReadDeletedOptions" expects request.params.id to be set' ); fn = createRehabilitateOptions.bind(helper, { ...mockRequest }); expect(fn, "rehabilitateOptions - empty request.body").to.throw( 'mongooseBread helper "createRehabilitateOptions" expects request.body._ids to be set' ); mockRequest = { body: { _ids: "invalid type - should be array but is string" }, }; fn = createReadDeletedOptions.bind(helper, { ...mockRequest }); expect(fn, "readDeletedOptions - invalid body._ids").to.throw( 'mongooseBread helper "createReadDeletedOptions" expects request.params.id to be set' ); fn = createRehabilitateOptions.bind(helper, { ...mockRequest }); expect(fn, "rehabilitateOptions - invalid body._ids").to.throw( 'mongooseBread helper "createRehabilitateOptions" expects request.body._ids to be an array' ); mockRequest = { params: { id: "64719c5415594e6bc75b6edc" }, body: [] }; fn = createRehabilitateOptions.bind(helper, { ...mockRequest }); expect( fn, "rehabilitateOptions - valid params.id & body is empty Array" ).to.throw( 'mongooseBread helper "createRehabilitateOptions" expects request.body to be an Object' ); }); it("parses requests with helper methods correctly", function () { let mockRequest = {}; const browseDeletedOptions = ProductSoftDelete.breadHelper().createBrowseDeletedOptions({ ...mockRequest, }); mockRequest = { params: { id: "6470bd3ff7cfba0acb59c044" } }; const readDeletedOptions = ProductSoftDelete.breadHelper().createReadDeletedOptions({ ...mockRequest, }); mockRequest = { params: { id: "6470bd3ff7cfba0acb59c044" } }; const rehabilitateOptions = ProductSoftDelete.breadHelper().createRehabilitateOptions({ ...mockRequest, }); mockRequest = { body: { _ids: ["6470bd3ff7cfba0acb59c044"] } }; const bulkRehabilitateOptions = ProductSoftDelete.breadHelper().createRehabilitateOptions({ ...mockRequest, }); expect(browseDeletedOptions).to.be.an.instanceOf(Object); expect(browseDeletedOptions).to.include.keys([ "query", "paginateOptions", ]); expect(browseDeletedOptions) .property("paginateOptions") .to.include.keys(["customFind", "customCount"]); expect(browseDeletedOptions) .property("paginateOptions") .that.has.property("customFind") .to.equal("findDeleted"); expect(browseDeletedOptions) .property("paginateOptions") .that.has.property("customCount") .to.equal("countDocumentsDeleted"); expect(readDeletedOptions).to.be.an.instanceOf(Object); expect(readDeletedOptions).to.include.keys([ "query", "select", "customFind", "customCount", ]); expect(readDeletedOptions) .property("customFind") .to.equal("findOneDeleted"); expect(readDeletedOptions).property("customCount").to.equal(false); expect(rehabilitateOptions).to.be.an.instanceOf(Object); expect(rehabilitateOptions).to.include.keys(["query"]); expect(bulkRehabilitateOptions).to.be.an.instanceOf(Object); expect(bulkRehabilitateOptions).to.include.keys(["query", "bulk"]); expect(bulkRehabilitateOptions).property("bulk").to.equal(true); }); it("browses a collection without query object in request", function () { const mockRequest = {}; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(10); expect(result.pagination.totalDocs).to.equal(10); expect(result.pagination.limit).to.equal(10); expect(result.pagination.page).to.equal(1); expect(result.pagination.offset).to.equal(0); expect(result.pagination.pagingCounter).to.equal(1); expect(result.pagination.hasPrevPage).to.equal(false); expect(result.pagination.hasNextPage).to.equal(false); expect(result.pagination.prevPage).to.equal(null); expect(result.pagination.nextPage).to.equal(null); expect(result.pagination.totalPages).to.equal(1); }); }); it("hides an easteregg", function () { expect(true).to.be.true; // thank you for reading this code :) }); it("browses a limited collection", function () { const mockRequest = { query: { limit: 5 } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(5); }); }); it("selects the correct page", function () { const mockRequest = { query: { limit: 5, page: 2 } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(5); expect(result.pagination.page).to.equal(2); expect(result.pagination.hasPrevPage).to.equal(true); }); }); it("escapes a search request string correctly", function () { const mockRequest = { query: { search: ".*+?^${}()|[]\\" } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); const searchQuery = options.query.$or; expect(searchQuery.length).to.equal(3); expect(searchQuery[0]?.name?.$regex).to.equal( "\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\" ); }); it("executes a search request correctly", function () { const mockRequest = { query: { search: "5" } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(1); expect(result.docs[0].name).to.equal("ProductSoftDelete #5"); }); }); it("executes a search request with additional queryParams correctly", function () { const mockRequest = { query: { search: "Product", price: { lt: "50" }, currency: "EUR", sort: "name", }, }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(4); expect(result.docs[0].name).to.equal("ProductSoftDelete #1"); }); }); it("executes a search request on arrays containing strings", function () { const mockRequest = { query: { search: "awesome" } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(5); expect(result.docs[0].name).to.include("ProductSoftDelete #"); }); }); it("executes a search request on arrays containing objects with fields of type string", function () { const mockRequest = { query: { search: "Option" } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options).then((result) => { expect(result.docs).to.have.length(5); expect(result.docs[0].name).to.include("ProductSoftDelete #"); expect(result.docs[0].options).to.be.an.instanceOf(Array); expect(result.docs[0].options).to.have.length(1); expect(result.docs[0].options[0].name).to.include("Option"); }); }); it("executes a search request with utf-8 chars correctly", function () { return ProductSoftDelete.create([ { name: "ä" }, { name: "ö" }, { name: "ü" }, ]) .then(() => { const mockRequest = { query: { search: "ö" } }; const options = ProductSoftDelete.breadHelper().createBrowseOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options); }) .then((result) => { expect(result.docs).to.have.length(1); expect(result.pagination.totalDocs).to.equal(1); }); }); it("reads a document correctly", function () { return ProductSoftDelete.create({ name: "temp" }) .then((doc) => { const mockRequest = { params: { id: doc._id } }; const options = ProductSoftDelete.breadHelper().createReadOptions({ ...mockRequest, }); return ProductSoftDelete.read(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc.name).to.equal("temp"); }); }); it("edits a document correctly", function () { return ProductSoftDelete.create({ name: "temp" }) .then((doc) => { const mockRequest = { params: { id: doc._id }, body: { currency: "USD" }, }; const options = ProductSoftDelete.breadHelper().createEditOptions({ ...mockRequest, }); return ProductSoftDelete.edit(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc.name).to.equal("temp"); expect(doc.currency).to.equal("USD"); }); }); it("edits many documents correctly", function () { return ProductSoftDelete.create([ { name: "doc#1", currency: "EUR" }, { name: "doc#2", currency: "EUR" }, ]) .then((docs) => { const ids = docs.map((d) => d._id); const mockRequest = { body: { _ids: [...ids], _docs: [{ currency: "USD" }] }, }; const options = ProductSoftDelete.breadHelper().createEditOptions({ ...mockRequest, }); return ProductSoftDelete.edit(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(2); const doc0 = result.docs[0]; expect(doc0).to.be.an.instanceOf(Object); expect(doc0.name).to.equal("doc#1"); expect(doc0.currency).to.equal("USD"); const doc1 = result.docs[1]; expect(doc1).to.be.an.instanceOf(Object); expect(doc1.name).to.equal("doc#2"); expect(doc1.currency).to.equal("USD"); }); }); it("adds a document correctly", function () { const mockRequest = { body: { name: "temp", price: 5, currency: "USD" } }; const options = ProductSoftDelete.breadHelper().createAddOptions({ ...mockRequest, }); return ProductSoftDelete.add(options).then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc.name).to.equal("temp"); expect(doc.price).to.equal(5); expect(doc.currency).to.equal("USD"); }); }); it("adds many documents correctly", function () { const mockRequest = { body: { _docs: [ { name: "temp#1", price: 5, currency: "USD" }, { name: "temp#2", price: 50, currency: "EUR" }, ], }, }; const options = ProductSoftDelete.breadHelper().createAddOptions({ ...mockRequest, }); return ProductSoftDelete.add(options).then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result).to.have.property("createdCount"); expect(result).to.have.property("acknowledged"); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(2); const doc0 = result.docs[0]; expect(doc0).to.be.an.instanceOf(Object); expect(doc0.name).to.equal("temp#1"); expect(doc0.price).to.equal(5); expect(doc0.currency).to.equal("USD"); const doc1 = result.docs[1]; expect(doc1).to.be.an.instanceOf(Object); expect(doc1.name).to.equal("temp#2"); expect(doc1.price).to.equal(50); expect(doc1.currency).to.equal("EUR"); }); }); it("softDeletes a document correctly", function () { return ProductSoftDelete.create({ name: "temp" }) .then((doc) => { const mockRequest = { auth: { user: { _id: "647192388b1018356aab3d10" } }, params: { id: doc._id }, }; const options = ProductSoftDelete.breadHelper().createDeleteOptions({ ...mockRequest, }); return Promise.all([ ProductSoftDelete.softDelete(options), Promise.resolve(doc), ]); }) .then(([result, doc]) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); expect(result.acknowledged).to.equal(true); expect(result.modifiedCount).to.equal(1); const mockRequest = { params: { id: doc._id } }; const options = ProductSoftDelete.breadHelper().createReadDeletedOptions({ ...mockRequest, }); return ProductSoftDelete.read(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc.deleted).to.equal(true); expect(doc.deletedBy.toString()).to.equal("647192388b1018356aab3d10"); }); }); it("softDeletes many documents correctly", function () { return ProductSoftDelete.create([ { name: "a" }, { name: "b" }, { name: "c" }, ]) .then((docs) => { const mockRequest = { auth: { user: { _id: "647192388b1018356aab3d10" } }, body: { _ids: docs.map((d) => d._id) }, }; const options = ProductSoftDelete.breadHelper().createDeleteOptions({ ...mockRequest, }); return ProductSoftDelete.softDelete(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(3); expect(result.acknowledged).to.equal(true); expect(result.modifiedCount).to.equal(3); const mockRequest = { query: { query: `{ "name": { "$in": ["a", "b", "c"] } }` }, }; const options = ProductSoftDelete.breadHelper().createBrowseDeletedOptions({ ...mockRequest, }); return ProductSoftDelete.browse(options); }) .then((result) => { expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs[0].deleted).to.equal(true); expect(result.docs[1].deleted).to.equal(true); expect(result.docs[2].deleted).to.equal(true); expect(result.docs[0].deletedBy.toString()).to.equal( "647192388b1018356aab3d10" ); expect(result.docs[1].deletedBy.toString()).to.equal( "647192388b1018356aab3d10" ); expect(result.docs[2].deletedBy.toString()).to.equal( "647192388b1018356aab3d10" ); }); }); it("destroys a softDeleted document correctly", function () { return ProductSoftDelete.create({ name: "temp", deleted: true }) .then((doc) => { const mockRequest = { auth: { user: { _id: "647192388b1018356aab3d10" } }, params: { id: doc._id }, }; const options = ProductSoftDelete.breadHelper().createDeleteOptions({ ...mockRequest, }); return Promise.all([ ProductSoftDelete.destroy(options), Promise.resolve(doc), ]); }) .then(([result, doc]) => { expect(result).to.be.an.instanceOf(Object); expect(result.acknowledged).to.equal(true); expect(result.deletedCount).to.equal(1); return ProductSoftDelete.findById(doc._id); }) .then((doc) => { expect(doc).to.be.null; }); }); it("destroys many softDeleted documents correctly", function () { return ProductSoftDelete.create([ { name: "a", deleted: true }, { name: "b", deleted: true }, { name: "c", deleted: true }, ]) .then((docs) => { const mockRequest = { auth: { user: { _id: "647192388b1018356aab3d10" } }, body: { _ids: docs.map((d) => d._id) }, }; const options = ProductSoftDelete.breadHelper().createDeleteOptions({ ...mockRequest, }); return ProductSoftDelete.destroy(options); }) .then((result) => { expect(result).to.be.an.instanceOf(Object); expect(result.acknowledged).to.equal(true); expect(result.deletedCount).to.equal(3); }); }); it("rehabilitates a softDeleted document correctly", function () { return ProductSoftDelete.create({ name: "temp", deleted: true }) .then((doc) => { const mockRequest = { params: { id: doc._id } }; const options = ProductSoftDelete.breadHelper().createRehabilitateOptions({ ...mockRequest, }); return Promise.all([ ProductSoftDelete.rehabilitate(options), Promise.resolve(doc), ]); }) .then(([result, _doc]) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(1); expect(result.modifiedCount).to.equal(1); const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc._id.toString()).to.equal(_doc._id.toString()); }); }); it("rehabilitates many softDeleted documents correctly", function () { return ProductSoftDelete.create([ { name: "a", deleted: true }, { name: "b", deleted: true }, { name: "c", deleted: true }, ]) .then((docs) => { const mockRequest = { body: { _ids: docs.map((d) => d._id) } }; const options = ProductSoftDelete.breadHelper().createRehabilitateOptions({ ...mockRequest, }); return Promise.all([ ProductSoftDelete.rehabilitate(options), Promise.resolve(docs), ]); }) .then(([result, _docs]) => { expect(result).to.be.an.instanceOf(Object); expect(result.docs).to.be.an.instanceOf(Array); expect(result.docs).to.have.length(3); expect(result.modifiedCount).to.equal(3); const _doc = _docs[0]; const doc = result.docs[0]; expect(doc).to.be.an.instanceOf(Object); expect(doc._id.toString()).to.equal(_doc._id.toString()); }); }); it("prevents destruction of a document that has not been softDeleted before", function () { return ProductSoftDelete.create({ name: "temp", deleted: false }) .then((doc) => { const mockRequest = { auth: { user: { _id: "647192388b1018356aab3d10" } }, params: { id: doc._id }, }; const options = ProductSoftDelete.breadHelper().createDeleteOptions({ ...mockRequest, }); return ProductSoftDelete.destroy(options); }) .catch((error) => { expect(error).to.be.an.instanceOf(MongooseBreadError); expect(error.message).not.to.equal(null); expect(error.details).not.to.equal(null); expect(error.statusCode).not.to.equal(null); expect(error.result).not.to.equal(null); }); }); }); // #endregion with softDelete // #region without softDelete describe("without softDelete", function () { it("adds plugin methods to Model", function () { expect(Product.browse).to.be.an.instanceOf(Function); expect(Product.read).to.be.an.instanceOf(Function); expect(Product.edit).to.be.an.instanceOf(Function); expect(Product.add).to.be.an.instanceOf(Function); expect(Product.destroy).to.be.an.instanceOf(Function); }); it("adds plugin helper getter method to Model", function () { expect(Product.breadHelper).to.be.an.instanceOf(Function); }); it("adds plugin helper methods to Model via helper getter", function () { expect(Product.breadHelper().createBrowseOptions).to.be.an.instanceOf( Function ); expect(Product.breadHelper().createReadOptions).to.be.an.instanceOf( Function ); expect(Product.breadHelper().createEditOptions).to.be.an.instanceOf( Function ); expect(Product.breadHelper().createAddOptions).to.be.an.instanceOf( Function ); expect(Product.breadHelper().createDeleteOptions).to.be.an.instanceOf( Function ); }); it("leaves the dependency methods intact", function () { expect(Product.paginate).to.be.an.instanceOf(Function); }); it("does not add softDelete methods to Model", function () { expect(Product.softDelete).to.equal(undefined); expect(Product.rehabilitate).to.equal(undefined); }); it("does not add softDelete plugin helper methods to Model via helper getter", function () { expect(Product.breadHelper().createBrowseDeletedOptions).to.equal( undefined ); expect(Product.breadHelper().createReadDeletedOptions).to.equal( undefined ); expect(Product.breadHelper().createRehabilitateOptions).to.equal( undefined ); }); it("throws Error if invalid request is passed to helper methods", function () { let mockRequest, fn; const helper = Product.breadHelper(); const { createReadOptions, createEditOptions, createDeleteOptions } = helper; mockRequest = {}; fn = createReadOptions.bind(helper, { ...mockRequest }); expect(fn, "readOptions - empty request").to.throw( 'mongooseBread helper "createReadOptions" expects request.params.id to be set' ); fn = createEditOptions.bind(helper, { ...mockRequest }); expect(fn, "editOptions - empty request").to.throw( 'mongooseBread helper "createEditOptions" expects request.body to be set' ); fn = createDeleteOptions.bind(helper, { ...mockRequest }); expect(fn, "deleteOptions - empty request").to.throw( 'mongooseBread helper "createDeleteOptions" expects request.body to be set' ); mockRequest = { params: { id: "invalid id" } }; fn = createReadOptions.bind(helper, { ...mockRequest }); expect(fn, "readOptions - invalid params.id").to.throw( 'mongooseBread helper "createReadOptions" expects request.params.id to be a valid ObjectId' ); fn = createEditOptions.bind(helper, { ...mockRequest }); expect(fn, "editOptions - invalid params.id").to.throw( 'mongooseBread helper "createEditOptions" expects request.params.id to be a valid ObjectId' ); fn = createDeleteOptions.bind(helper, { ...mockRequest }); expect(fn, "deleteOptions - invalid params.id").to.throw( 'mongooseBread helper "createDeleteOptions" expects request.params.id to be a valid ObjectId' ); mockRequest = { body: {} }; fn = createReadOptions.bind(helper, { ...mockRequest }); expect(fn, "readOptions - empty request.body").to.throw( 'mongooseBread helper "createReadOptions" expects request.params.id to be set' ); fn = createEditOptions.bind(helper, { ...mockRequest }); expect(fn, "editOptions - empty request.body").to.throw( 'mongooseBread helper "createEditOptions" expects request.body._ids to be set' ); fn = createDeleteOptions.bind(helper, { ...mockRequest }); expect(fn, "deleteOptions - empty request.body").to.throw( 'mongooseBread helper "createDeleteOptions" expects request.body._ids to be set' ); mockRequest = { body: { _ids: "invalid type - should be array but is string" }, }; fn = createReadOptions.bind(helper, { ...mockRequest }); expect(fn, "readOptions - invalid body._ids").to.throw( 'mongooseBread helper "createReadOptions" expects request.params.id to be set' ); fn = createEditOptions.bind(helper, { ...mockRequest }); expect(fn, "editOptions - invalid body._ids").to.throw( 'mongooseBread helper "createEditOptions" expects request.body._ids to be an array' ); fn = createDeleteOptions.bind(helper, { ...mockRequest }); expect(fn, "deleteOptions - invalid body._ids").to.throw( 'mongooseBread helper "createDeleteOptions" expects request.body._ids to be an array' ); mockRequest = { params: { id: "64719c5415594e6bc75b6edc" }, body: [] }; fn = createEditOptions.bind(helper, { ...mockRequest }); expect( fn, "editOptions - valid params.id & body is empty Array" ).to.throw( 'mongooseBread helper "createEditOptions" expects request.body to be an Object' ); fn = createDeleteOptions.bind(helper, { ...mockRequest }); expect( fn, "deleteOptions - valid params.id & body is empty Array" ).to.throw( 'mongooseBread helper "createDeleteOptions" expects request.body to be an Object' ); }); it("parses search requests with helper methods correctly", function () { let mockRequest = { query: { search: "product" } }; const browseOptions = Product.breadHelper().createBrowseOptions({ ...mockRequest, }); expect(browseOptions).to.be.an.instanceOf(Object); expect(browseOptions).to.include.keys(["query", "paginateOptions"]); expect(browseOptions.query.$or).to.be.of.length( 2, "searchableField for path of type Number and nonExistingField have not been removed" ); }); it("removes searchableFields that ar not present in Schema.path", function () { let mockRequest = { query: { search: "product" } }; const browseOptions = Product.breadHelper().createBrowseOptions({ ...mockRequest, }); expect(browseOptions).to.be.an.instanceOf(Object); expect(browseOptions).to.include.keys(["query", "paginateOptions"]); expect(browseOptions.query.$or).to.be.of.length( 2, "searchableField for path of type Number and nonExistingField have not been removed" ); }); it("parses requests with helper methods correctly", function () { let mockRequest = {}; const browseOptions = Product.breadHelper().createBrowseOptions({ ...mockRequest, }); mockRequest = { params: { id: "6470bd3ff7cfba0acb59c044" } }; const readOptions = Product.breadHelper().createReadOptions({ ...mockRequest, }); mockRequest = { params: { id: "6470bd3ff7cfba0acb59c044" }, body: { price: 1 }, }; const editOptions = Product.breadHelper().createEditOptions({ ...mockRequest, }); mockRequest = { body: { price: 1 } }; const addOptions = Product.breadHelper().createAddOptions({ ...mockRequest, }); mockRequest = { params: { id: "6470bd3ff7cfba0acb59c044" } }; const deleteOptions = Product.breadHelper().createDeleteOptions({ ...mockRequest, }); mockRequest = { body: { _ids: ["6470bd3ff7cfba0acb59c044"], _docs: [{ price: 1 }, { price: 100 }], }, }; const bulkEditOptions = Product.breadHelper().createEditOptions({ ...mockRequest, }); mockRequest = { body: { _ids: ["6470bd3ff7cfba0acb59c044"] } }; const bulkDeleteOptions = Product.breadHelper().createDeleteOptions({ ...mockRequest, }); mockRequest = { body: { _docs: [{ price: 1 }, { price: 100 }] } }; const bulkAddOptions = Product.breadHelper().createAddOptions({ ...mockRequest, }); expect(browseOptions).to.be.an.instanceOf(Object); expect(browseOptions).to.include.keys(["query", "paginateOptions"]); expect(readOptions).to.be.an.instanceOf(Object); expect(readOptions).to.include.keys(["query", "select", "customFind"]); expect(editOptions).to.be.an.instanceOf(Object); expect(editOptions).to.include.keys(["query", "payload"]); expect(addOptions).to.be.an.instanceOf(Object); expect(addOptions).to.include.keys(["payload"]); expect(deleteOptions).to.be.an.instanceOf(Object); expect(deleteOptions).to.include.keys(["query", "userId"]); expect(deleteOptions).property("userId").to.equal(null); expect(bulkEditOptions).to.be.an.instanceOf(Object); expect(bulkEditOptions).to.include.keys(["bulk", "payload", "query"]); expect(bulkEditOptions).property("bulk").to.equal(true); expect(bulkEditOptions).property("payload").to.have.keys(["price"]); expect(bulkEditOptions) .property("payload") .to.be.an.instanceOf(Object) .with.property("price") .which.equals(100); expect(bulkAddOptions).to.be.an.instanceOf(Object); expect(bulkAddOptions).to.include.keys(["bulk", "payload"]); expect(bulkAddOptions).property("bulk").to.equal(true); expect(bulkAddOptions) .property("payload") .to.be.an.instanceOf(Array) .with.length(2) .to.have.deep.members([{ price: 1 }, { price: 100 }]); expect(bulkDeleteOptions).to.include.keys(["bulk", "query", "userId"]); expect(bulkDeleteOptions).to.be.an.instanceOf(Object); expect(bulkDeleteOptions).property("userId").to.equal(null); expect(bulkDeleteOptions).property("bulk").to.equal(true); }); it("parses query params with comparison operators in helper methods correctly", function () { const inBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { _id: { in: ["123"] } }, }); const ninBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { _id: { nin: ["123"] } }, }); const eqBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { price: { eq: 100 } }, }); const gtBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { price: { gt: 100 } }, }); const gteBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { price: { gte: 100 } }, }); const ltBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { price: { lt: 100 } }, }); const lteBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { price: { lte: 100 } }, }); const neBrowseOptions = Product.breadHelper().createBrowseOptions({ query: { price: { ne: 100 } }, }); expect(inBrowseOptions.query)