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

WIP: Map type #6287

Merged
merged 10 commits into from
Apr 12, 2018
Merged

WIP: Map type #6287

merged 10 commits into from
Apr 12, 2018

Conversation

vkarpov15
Copy link
Collaborator

@vkarpov15 vkarpov15 commented Mar 27, 2018

Re: #681, #4353, and several other issues, we're planning on adding a Map type that builds on ES6 maps. There's some more work for this PR already in the 5.1 branch in f828de3.

The primary motivation for maps is to support arbitrary keys while retaining change tracking and validation. Proxies would be an alternative implementation, but I'm not confident we could implement a pure proxy solution in a reasonable time frame. As a consequence, users will have to use Map.prototype.get() to get keys and Map.prototype.set() to set keys, which makes change tracking much easier but adds some overhead. However, devs that use maps should already be used to this syntax, so mongoose maps will feel familiar.

There's a noteworthy caveat that, under the hood, maps become objects when they're stored in MongoDB because there's no notion of a map type in BSON.

Still lots of work to be done to make sure maps work right with queries. @varunjayaraman @lineus I would love to hear your thoughts.

Here's a sample usage from the test case:

    let nestedValidateCalls = [];
    let validateCalls = [];
    const TestSchema = new mongoose.Schema({
      v: {
        type: Map,
        of: {
          type: Number,
          validate: function(v) {
            nestedValidateCalls.push(v);
            return v < 4;
          }
        },
        validate: function(v) {
          validateCalls.push(v);
          return true;
        }
      }
    });

    const Test = db.model('MapTest', TestSchema);

    return co(function*() {
      const doc = yield Test.create({ v: { x: 1 } });
      assert.deepEqual(nestedValidateCalls, [1]);
      assert.equal(validateCalls.length, 1);
      assert.equal(validateCalls[0].get('x'), 1);

      assert.ok(doc.v instanceof Map);

      let threw = false;

      try {
        yield Test.create({ v: { notA: 'number' } });
      } catch (error) {
        threw = true;
        assert.ok(!error.errors['v']);
        assert.ok(error.errors['v.notA']);
      }
      assert.ok(threw);

      doc.v.set('y', 5);

      threw = false;
      try {
        yield doc.save();
      } catch (error) {
        threw = true;
        assert.ok(!error.errors['v']);
        assert.ok(error.errors['v.y']);
      }
      assert.ok(threw);
    });

@vkarpov15 vkarpov15 added new feature This change adds new functionality, like a new method or class discussion If you have any thoughts or comments on this issue, please share them! labels Mar 27, 2018
@vkarpov15 vkarpov15 added this to the 5.1 milestone Mar 27, 2018
@vkarpov15 vkarpov15 self-assigned this Mar 27, 2018
@lineus
Copy link
Collaborator

lineus commented Mar 30, 2018

@vkarpov15 I haven't used Maps yet, but I'm going to see if I can wrap my head around them (specifically with respect to mongoose) in the next day or so. I've been reading about them but I have a ways to go before I can add much value to the conversation. I'll be watching for sure, and if there's anything you think I can do to help just say the word.

@vkarpov15
Copy link
Collaborator Author

@lineus the particular details of maps are interesting but I think the biggest win here is that we can support objects with arbitrary keys. So:

const TestSchema = new mongoose.Schema({
  myMap: {
    type: Map,
    of: Number
  }
});

Would mean that doc.myMap.set('key1', 1), doc.myMap.set('key2', 2), etc. are valid. Even if you aren't an expert in maps, I'd appreciate any thoughts you have on that and any testing you could do.

@vkarpov15 vkarpov15 merged commit 1e7093c into 5.1 Apr 12, 2018
@vkarpov15 vkarpov15 deleted the gh681 branch April 12, 2018 22:11
@simonrichard
Copy link

simonrichard commented Jul 12, 2018

This feature was implemented in version 5.1.0 but still appears to be unavailable as of 5.2.3. Am I missing something?

Edit: Turns out I was using the wrong command (npm show mongoose version) to get the installed version. Thanks @lineus for helping me figure it out :-)

@lineus
Copy link
Collaborator

lineus commented Jul 12, 2018

@simonrichard it should be available. Can you open an issue with a complete code sample? Or if you'd rather, join us in either Gitter.im or Slack.

@3lo1i
Copy link

3lo1i commented Dec 27, 2018

Can Map of ObjectIds be populated?

const SomeSchema = new mongoose.Schema({
  myMap: {
    type: Map,
    of: {
      type: ObjectId,
      ref: 'SomeRef'
  }
});

const SomeModel = mongoose.model('SomeModel', SomeSchema);

const populatedDoc = await SomeModel.findOne({...}).populate('myMap').exec();

Now it does not seem to work. Workaround for this is simple, but it would be awesome to have this feature.

@vkarpov15
Copy link
Collaborator Author

@3lo1i yeah it can be populated, the below script prints 'foo' as expected in Mongoose 5.4.2:

const assert = require('assert');
const mongoose = require('mongoose');
mongoose.set('debug', true);

const GITHUB_ISSUE = `gh6287`;
const connectionString = `mongodb://localhost:27017/${ GITHUB_ISSUE }`;
const { Schema } = mongoose;

run().then(() => console.log('done')).catch(error => console.error(error.stack));

async function run() {
  await mongoose.connect(connectionString);
  await mongoose.connection.dropDatabase();

  const SomeSchema = new mongoose.Schema({
    myMap: {
      type: Map,
      of: {
        type: mongoose.ObjectId,
        ref: 'SomeRef'
      }
    }
  });

  const SomeModel = mongoose.model('SomeModel', SomeSchema);
  const SomeRef = mongoose.model('SomeRef', new Schema({ name: String }));

  const doc1 = await SomeRef.create({ name: 'foo' });
  const doc2 = await SomeModel.create({ myMap: { test: doc1._id } });

  const populatedDoc = await SomeModel.findOne({}).populate('myMap').exec();

  console.log(populatedDoc.myMap.get('test').name); // foo
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion If you have any thoughts or comments on this issue, please share them! new feature This change adds new functionality, like a new method or class
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants