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

feat(collection): add colleciton level document mapping/unmapping #1698

Merged
merged 1 commit into from
May 10, 2018
Merged

feat(collection): add colleciton level document mapping/unmapping #1698

merged 1 commit into from
May 10, 2018

Conversation

j
Copy link
Contributor

@j j commented May 3, 2018

This is a WIP to add mapping / unmapping on a collection level.

This can give document-mapper libraries a quicker way to map documents so that they don't have to wrap every collection's methods. (i.e. type-mongo-mapper)

Features to be implemented:

  • Mapping: Allow all collection methods that return plain documents (ignoring aggregations) to be "mapped" (i.e. converting a document to a class).
  • Unmapping: Allow all collection methods that insert or update plain documents (without update modifiers) to be unmapped (i.e. converting a class to a document).

Basic Example:

class User {
    public _id;
    public firstName;
    public lastName;
    public createdAt;

    constructor(doc) {
        this._id = doc._id || new ObjectID();
        this.firstName = doc.firstName;
        this.lastName = doc.lastName;
        this.createdAt = doc.createdAt || new Date();
    }

    get id() {
        return this._id.toString();
    }

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

const collection = db.collection('users', {
    map: (doc) => new User(doc),
    unmap: (user) => ({
        _id: user._id,
        firstName: doc.firstName,
        lastName: doc.lastName,
        createdAt: doc.createdAt,
    })
});

const user = await collection.findOne({ /* ... */ });
assert.true(user instanceof User); // true

const cursor = await collection.find({ /* ... */ }).toArray();
assert.true(user[0] instanceof User); // true

const { ok, value } = await collection.findOneAndUpdate({ /* ... */ });
assert.true(value instanceof User); // true

const user = new User({ firstName: 'John', lastName: 'Doe' });
await collection.insertOne(user); // calls unmap option.. createdAt & the ID were created on the constructor, so it'd already exist.

Discussion: https://jira.mongodb.org/projects/NODE/issues/NODE-1450?filter=allissues
Related: https://github.com/mongodb/js-bson/issues/211

Also, my heart wants this to belong in the bson library, but right now BSON serialization seems to be handed over any javascript object instead of just the documents, so it's (de)serializing more than it should? (I'm unsure of how the internals work exactly). I know the C# BSON driver has advanced class mapping, so that's what made me believe this type of thing can go there. And I believe the serialization option above can be done through a toBSON() function on the class., but it'd still be nice to be more explicit on the collection level and let mapping libraries handle it for you.

@j
Copy link
Contributor Author

j commented May 4, 2018

/ping @mbroadst thoughts?

@daprahamian
Copy link
Contributor

@j heads up: @mbroadst is on vacation for the next few weeks, so it may take a while to circle around to this.

Copy link
Contributor

@daprahamian daprahamian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good implementation. Only a few items I noted below.

Additionally, can you make sure to update the jsdoc descriptions for any relevant methods that this applies to?

As for the naming conventions, I am partial to map/unmap.

@@ -0,0 +1,170 @@
'use strict';
var test = require('./shared').assert;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for writing tests!

  1. Since we are testing against Node >= 4, please use ES6 syntax where possible. That includes

    • const where possible, let otherwise.
    • class instead of raw functions and prototypes.
    • template strings (` `) instead of string concatenation
  2. instead of using our old assert library (which we mostly keep around for legacy reason), please use chai.expect for your assertions.

const cursor = this.s.topology.cursor(this.s.namespace, findCommand, newOptions);

// add a map function to the cursor if "deserialize" option is set
if (this.s.options.deserialize && typeof this.s.options.deserialize === 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this if condition can be condensed into just typeof this.s.options.deserialize === 'function'.

@@ -2253,6 +2253,11 @@ var findAndModify = function(self, query, sort, doc, options, callback) {
// Execute the command
self.s.db.command(queryObject, finalOptions, function(err, result) {
if (err) return handleCallback(callback, err, null);

if (result && result.value) {
Copy link
Contributor

@daprahamian daprahamian May 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like above, we can condense the if statement and function call to:

if (result && result.value && typeof self.s.options.deserialize === 'function') {
  result.value = self.s.options.deserialize(result.value);
}

And we can eliminate the use of deserializeDocument

});

client.connect(function(err, client) {
var db = client.db(configuration.db);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should be switched with the next one as follows:

client.connect(function(err, client) {
  expect(err).to.eql(null);
  const db = client.db(configuration.db);
  ...

If there is an error, client will be null|undefined, and the test will error out with the message TypeError: Cannot read property 'db' of undefined. By switching the lines, the error message will be clearer.

@j
Copy link
Contributor Author

j commented May 7, 2018

@daprahamian Okay, I'd happily make these changes.

And It's good news to be told to use more modern syntax. Are the drivers going to be rewritten using ES6 classes? It hurt my brain sometimes navigating through this code.

And I'm definitely down with map / unmap. I'd be for serialize/unserialize if it was BSON specific, but currently, it's too hard to get this into BSON. I just ended up committing to what I had already prototyped.

Another option I thought of, which is to be like another one of the drivers (I don't have time to look at which one at the moment), but it can look something like this:

const collection = db.collection('users');
const mappedCollection = collection.asMapCollection(mapFn, unmapFn);

// ... or

const mappedCollection = db.mappedCollection('users', mapFn, unmapFn);

And I can create a new MappedCollection class. I think for the most part, I'd just have to wrap most functions.

What are your thoughts or preferences?

@daprahamian
Copy link
Contributor

@j we're currently taking a piecemeal approach to rewriting the driver in ES6; when we touch a file or do a major refactor, we try to bring it as up to date as possible. There are already some portions of the code rewritten with ES6 classes, and we will likely continue in the future.

I'd hold off on the additional helpers you mentioned for right now, since it's basically sugar for db.collection('users', { map, unmap});

@j
Copy link
Contributor Author

j commented May 7, 2018

@daprahamian sounds good, I'll try to get the unmap portion done by EOD tomorrow (hopefully today if I get time), and do more refactoring.

Can you think of other areas I didn't touch that can possibly be mapped to a class on the top of your head? I want to make sure if you have "map/unmap" set, it's mapping everywhere a document ever gets returned (minus aggregations).

@daprahamian
Copy link
Contributor

@j I can't think of any other places that need the mapping.

@j
Copy link
Contributor Author

j commented May 7, 2018

@daprahamian should I stick to await/async (in tests) or keep it vanilla callback style?

@daprahamian
Copy link
Contributor

@j Do not use async/await. Our tests are run against Node 4, Node 6, Node 8, and Node 10, and async/await can't be parsed by Node 6 or Node 4.

Other than that, you can use either callbacks or Promises.

@j j changed the title [WIP] Collection level (de)serialization [WIP] Collection level (un)mapping May 7, 2018
@j
Copy link
Contributor Author

j commented May 7, 2018

@daprahamian I added unmap as well. I kind of want to add an option on the collection method so that you can opt out of the collection mapping if you want.

I ended up doing an "instanceof" check on the unmap function as you can see in the tests to allow me to sort of over-ride it.

I have to head out, so I'll get back on it tomorrow.

Copy link
Contributor

@daprahamian daprahamian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few styling issues.

doc._id = self.s.pkFactory.createPk();
}

return unmap !== false ? unmap(doc) : doc;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be an implicit boolean return unmap ? unmap(doc) : doc

}

return docs.map(function(doc) {
if (forceServerObjectId !== true && doc._id === void 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets make this if (forceServerObjectId !== true && doc._id == null) {. I'd like to remove as much void 0 from the codebase as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank goodnes ;)


// no need to modify the docs if server sets the ObjectId
// and unmap collection option is unset
if (forceServerObjectId === true && unmap === false) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (forceServerObjectId === true && !unmap)

}

getFullName() {
return this.firstName + ' ' + this.lastName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a string template:

getFullName() {
  return `${this.firstName}  ${this.lastName}`;
}

collection.findOne({}, function(err, doc) {
expect(err).to.be.null;

expect(doc).to.eql({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When testing against objects, we prefer to use .to.deep.equal instead of to.eql. It is clearer what is going on here.

@j
Copy link
Contributor Author

j commented May 8, 2018

@daprahamian I added unmap for over-riding or disabling collection level mapping. I didn't add an over-ride for finding but I can. If unmapping is really needed on a collection level, I figure you can just use the cursor directly. But if you want me to add that for consistency I can!

Let me know what you think.

@j
Copy link
Contributor Author

j commented May 8, 2018

Now that I think about it, should I remove the ability to over-ride on the collection method level? If someone wants an unmapped collection, they can do:

// collection with mapping
const mappedCollection = db.collection('users', {
    map: /* ... */,
    unmap: /* ... */,
});

// collection without mapping if they want to do things with raw documents
const collection = db.collection('users');

@j
Copy link
Contributor Author

j commented May 8, 2018

@daprahamian I went ahead and removed the unmap option from the cursor methods since you can just create another unmapped collection. I can bring it back.

If these changes all look good, I can clean up the commits into one and be good to go!

@j
Copy link
Contributor Author

j commented May 9, 2018

@daprahamian i have some time to finalize any changes today if you feel there are any, otherwise I can squash the commits and clean it up. Let me know.

Copy link
Contributor

@daprahamian daprahamian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 LGTM. Squash the commits and I can add it into the next beta release.

@j j changed the title [WIP] Collection level (un)mapping feat(collection): add colleciton level document mapping/unmapping May 9, 2018
@j
Copy link
Contributor Author

j commented May 9, 2018

🤘 done!

@j
Copy link
Contributor Author

j commented May 9, 2018

When the beta version that contains this is pushed, I'll work on top of it with my typescript mapping library and get a real feel of it. :)

@j
Copy link
Contributor Author

j commented May 9, 2018

@daprahamian sorry forgot to make the mention.

@daprahamian daprahamian merged commit d03335e into mongodb:master May 10, 2018
@daprahamian
Copy link
Contributor

daprahamian commented Jun 25, 2018

@j we unfortunately have to back this change out of the 3.1.0-beta branch before our next release. With mongodb/js-bson#253 and other approaches to document-class mapping on the table, we'd like to have further discussion before committing to any one API approach.

My plan right now is as follows:

  • Fork a branch with your changes branch here
  • Remove the changes from master
  • Create a new PR between your branch and master

daprahamian added a commit that referenced this pull request Jun 25, 2018
Reverts #1698. With mongodb/js-bson#253 and other approaches to
document-class mapping on the table, we'd like to have further
discussion before committing to any one API approach.

Reopens NODE-1450
daprahamian added a commit that referenced this pull request Jun 25, 2018
Reverts #1698. With mongodb/js-bson#253 and other approaches to
document-class mapping on the table, we'd like to have further
discussion before committing to any one API approach.

Reopens NODE-1450
daprahamian added a commit that referenced this pull request Jun 25, 2018
Reverts #1698. With mongodb/js-bson#253 and other approaches to
document-class mapping on the table, we'd like to have further
discussion before committing to any one API approach.

Reopens NODE-1450
@julien-c
Copy link
Contributor

This is really cool! We use something similar but it's sometimes a pain to maintain. Love this implementation @j <3

@mbroadst
Copy link
Member

@julien-c it’s coming! We just wanted a little more time to make a measured decision about it, sorry for the wait 😄

@j
Copy link
Contributor Author

j commented Jul 9, 2018

@daprahamian @mbroadst Quick question... I was working on top of the beta branch for a while and everything got borked and didn't realize it's because my feature was removed. I saw that PR in BSSON, which is an OK start? I just can't get the "target" option in the collection commands to be passed down to the BSON deserialize. Also, that PR didn't serialize either, so that might be a good one to add. I'd like to help with this, but I'm finding it a pain to figure out how to pass a client option down to the BSON methods.

Any clue where I should look? Code bounces around quite a lot and I want to put it in the right place.

PS. After working on top of my merged/reverted feature, everything was working pretty well :)

@mbroadst
Copy link
Member

mbroadst commented Jul 9, 2018

@j we are not actively working on the feature, but will be focusing on it next quarter (read: later this summer). We will make sure to keep you apprised, as we want feedback from those who it will be useful to, but in the meantime I recommend making an intermediary module to monkey patch your support into the driver. Sorry for the delay, but we want to make sure this is done right the first time, as we are on the hook for maintaining the feature in the future.

@j
Copy link
Contributor Author

j commented Jul 9, 2018

Sounds good. The PR right now in js-bson, he's using firstBatch and nextBatch to determine that it's a document. Is this the best way or are there other thoughts on how you'll want to do the class mapping? (mongodb/js-bson#253)

kiku-jw pushed a commit to kiku-jw/node-mongodb-native that referenced this pull request Feb 11, 2019
Reverts mongodb#1698. With mongodb/js-bson#253 and other approaches to
document-class mapping on the table, we'd like to have further
discussion before committing to any one API approach.

Reopens NODE-1450
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants