Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for virtual population with mongoose adapter #354

Merged
merged 11 commits into from
Jul 12, 2023
30 changes: 22 additions & 8 deletions packages/moleculer-db-adapter-mongoose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,17 +310,31 @@ class MongooseDbAdapter {
* Convert DB entity to JSON object
*
* @param {any} entity
* @param {Context} ctx - moleculer context
* @returns {Object}
* @memberof MongooseDbAdapter
*/
entityToObject(entity) {
let json = entity.toJSON();
if (entity._id && entity._id.toHexString) {
json._id = entity._id.toHexString();
} else if (entity._id && entity._id.toString) {
json._id = entity._id.toString();
}
return json;
entityToObject(entity, ctx) {
const fieldsToPopulate = _.get(ctx, "params.populate", []);
const virtualFields = Object.values(_.get(this, "model.schema.virtuals", {}))
.reduce((acc, virtual) => _.get(virtual, "options.ref") ? [...acc, virtual.path] : acc, []);
const virtualsToPopulate = _.intersection(fieldsToPopulate, virtualFields);
const options = {skipInvalidIds: true, lean: true};
const transform = (doc) => doc._id;
const populate = virtualsToPopulate.map(path => ({path, select: "_id", options, transform}));

return Promise.resolve(populate.length > 0 ? entity.populate(populate) : entity)
.then(entity => {
const json = entity.toJSON();

if (entity._id && entity._id.toHexString) {
json._id = entity._id.toHexString();
} else if (entity._id && entity._id.toString) {
json._id = entity._id.toString();
}

return json;
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use strict";

const { ServiceBroker } = require("moleculer");
const DbService = require("../../../moleculer-db/src");
const MongooseStoreAdapter = require("../../src");
const User = require("../models/users");
const Post = require("../models/posts");

describe("Test virtuals population feature", () => {
// Create broker
const broker = new ServiceBroker({
logger: console,
logLevel: "error",
});

beforeAll(async () => {
// Load posts service
broker.createService(DbService, {
name: "posts",
adapter: new MongooseStoreAdapter("mongodb://localhost"),
model: Post.Model,
settings: {
populates: {
author: "users.get",
},
},
});

// Load users service
broker.createService(DbService, {
name: "users",
adapter: new MongooseStoreAdapter("mongodb://localhost"),
model: User.Model,
settings: {
populates: {
posts: "posts.get",
lastPost: "posts.get",
},
},
});

await broker.start();
});

afterAll(async () => {
await broker.stop();
});

beforeEach(async () => {
// clean collection for replayability
await Post.Model.deleteMany({});
await User.Model.deleteMany({});
});

it("Should populate virtuals", async () => {
const _user = await User.Model.create({
firstName: "John",
lastName: "Doe",
});

const _post1 = await Post.Model.create({
title: "post_1",
content: "content 1",
author: _user._id,
});

const _post2 = await Post.Model.create({
title: "post_2",
content: "content 2",
author: _user._id,
});

const user = await broker.call("users.get", {
id: _user.id,
populate: ["posts", "lastPost", "postCount"],
});

expect(user).toHaveProperty("firstName", "John");
expect(user).toHaveProperty("lastName", "Doe");
// virtual function without populate
expect(user).toHaveProperty("fullName", "John Doe");
// virtual populate with ref and count option
expect(user).toHaveProperty("postCount", 2);
// virtual populate with ref
expect(user).toHaveProperty("posts");
expect(user.posts).toHaveLength(2);
expect(user.posts.map((p) => p._id)).toEqual([_post2.id, _post1.id]);
// virtual populate with justOne option set to "true"
expect(user).toHaveProperty("lastPost");
expect(user.lastPost).toHaveProperty("_id", _post2.id);
});
});
3 changes: 2 additions & 1 deletion packages/moleculer-db-adapter-mongoose/test/models/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ let PostSchema = new Schema({
default: 0
},
author: {
type: Schema.ObjectId
type: Schema.ObjectId,
ref: "User"
}

}, {
Expand Down
60 changes: 60 additions & 0 deletions packages/moleculer-db-adapter-mongoose/test/models/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use strict";

const {Schema, model} = require("mongoose");

const UserSchema = new Schema(
{
firstName: {
type: String,
required: true,
trim: true,
},
lastName: {
type: String,
required: true,
trim: true,
},
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
virtuals: {
fullName: {
get() {
return `${this.firstName} ${this.lastName}`;
},
},
posts: {
options: {
ref: "Post",
localField: "_id",
foreignField: "author",
options: { sort: { createdAt: -1 } },
},
},
postCount: {
options: {
ref: "Post",
localField: "_id",
foreignField: "author",
count: true,
},
},
lastPost: {
options: {
ref: "Post",
localField: "_id",
foreignField: "author",
justOne: true,
options: { sort: { createdAt: -1 } },
},
},
},
}
);

module.exports = {
Model: model("User", UserSchema),
Schema: UserSchema,
};
Original file line number Diff line number Diff line change
Expand Up @@ -658,18 +658,18 @@ if (process.versions.node.split(".")[0] < 14) {
});
});

it("call doc.toJSON", () => {
it("call doc.toJSON", async () => {
doc.toJSON.mockClear();
doc._id.toHexString.mockClear();
adapter.entityToObject(doc);
await adapter.entityToObject(doc);
expect(doc.toJSON).toHaveBeenCalledTimes(1);
expect(doc._id.toHexString).toHaveBeenCalledTimes(1);
});

it("call entityToObject on doc without ObjectID", () => {
it("call entityToObject on doc without ObjectID", async () => {
docIdString.toJSON.mockClear();
docIdString._id.toString.mockClear();
adapter.entityToObject(docIdString);
await adapter.entityToObject(docIdString);
expect(docIdString.toJSON).toHaveBeenCalledTimes(1);
expect(docIdString._id.toString).toHaveBeenCalledTimes(1);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/moleculer-db/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ module.exports = {
return Promise.resolve(docs)

// Convert entity to JS object
.then(docs => docs.map(doc => this.adapter.entityToObject(doc)))
.then(docs => Promise.all(docs.map(doc => this.adapter.entityToObject(doc, ctx))))

// Apply idField
.then(docs => docs.map(doc => this.adapter.afterRetrieveTransformID(doc, this.settings.idField)))
Expand Down
8 changes: 4 additions & 4 deletions packages/moleculer-db/test/unit/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ describe("Test transformDocuments method", () => {
expect(res).toBe(doc);

expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(1);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(doc);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(doc, ctx);

expect(service.encodeID).toHaveBeenCalledTimes(1);
expect(service.encodeID).toHaveBeenCalledWith(doc._id);
Expand All @@ -565,7 +565,7 @@ describe("Test transformDocuments method", () => {
expect(res).toBe(doc);

expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(1);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(doc);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(doc, ctx);

expect(service.encodeID).toHaveBeenCalledTimes(1);
expect(service.encodeID).toHaveBeenCalledWith(doc._id);
Expand Down Expand Up @@ -602,8 +602,8 @@ describe("Test transformDocuments method", () => {
expect(res).toEqual(docs);

expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(2);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(docs[0]);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(docs[1]);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(docs[0], ctx);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(docs[1], ctx);

expect(service.encodeID).toHaveBeenCalledTimes(2);
expect(service.encodeID).toHaveBeenCalledWith(docs[0]._id);
Expand Down