All but not more than in an array

If I have a model in a collection like:

{
  "items": [1,1,1,2,3]
}

I want to query the collection with a structure like such that [1,1,1] would return the above object, because it contains every thing in the query. However if the query was [1,1,1,1], I want it to reject, because there’s too many ones.

Does this query make sense? How would I accomplish this in mongo?

Sorry, but I can’t understand your question.

You want to return all documents in a Mongo collection that have exactly three 1’s in their items array? that’s how i understood the question, but i may be wrong.

I’m terrible at explaining myself. Let me be less vague.

I have this data structure.

{
  name: "Whatever",
  steps: [
    { time: 0, name: "A" },
    { time: 241, name: "B" },
    {time: 250, name: "B" }, // and so on
  ]
}

I want to search this collection with a given time and set of names, and get models that have steps before the given time, for all of the given names, but no more.

For example, querying time: 300, names:[“A”, “B”]` would return the model above, because it has A and B before 300.

However if I queried time: 300, names:[“A”, “A”, “B”]`, it wouldn’t because it doesn’t have 2 “A” before 300.

I posted an SO question here about it: https://stackoverflow.com/questions/57758786/mongo-query-an-array-of-objects-included-in-a-set

That does make it a bit clearer

Usually, if you want to match two properties in an embedded document array, you need to use $elemMatch, that doesn’t quite cover the requirement to have exactly two documents with the same name before a time though.

You might need to use $where, and pass a stringified javascript function to mongo to execute.

myCollection.find({
  $where: `function() {
      var matchingA = this.steps.filter(elem => elem.time <= 300 && elem.name == "A");
      var matchingB = this.steps.filter(elem => elem.time <= 300 && elem.name == "B");
      return matchingA.length === 2 && matchingB.length === 1;
    }`
});

Which will run for each document, checking that there are exactly two steps before 300 with the name “A” and exactly one step before 300 with the name “B”

I didn’t know you could pass in js functions.

I already wrote a function after the fact to do what I want, but if there’s a benefit here, I’ll move it into the query. I see it runs per-element in the array. Is there way to initialize a variable outside of that function to use as state tracking?

This behaves how I want it to:

  const builds = Build.find({}).fetch();
  return builds.filter(b => {
    const buildUnitCounts = {};
    const selectedUnitCounts = {};
    b.buildOrder.forEach(bo => {
      if(bo.time <= time) {
        buildUnitCounts[bo.unit] = buildUnitCounts[bo.unit] || 0;
        buildUnitCounts[bo.unit] += 1;
      }
    });
    selectedUnits.forEach(su => {
      selectedUnitCounts[su] = selectedUnitCounts[su] || 0;
      selectedUnitCounts[su] += 1;
    });
    return Object.keys(selectedUnitCounts).every(key => {
      return buildUnitCounts[key] >= selectedUnitCounts[key];
    });
  });

Just trying to think of how to translate that into the where query

To be pedantic, it’s run for every document in the collection, not for items in the array.

I would pass the variables into the function string like so:

const time = 300;
const selectedUnits = ["A", "A", "B"];
// Calculate once outside since it doesn't look like it depends on the individual documents
const selectedUnitCounts = selectedUnits.reduce((acc, curr) => {
  acc[curr] = acc[curr] || 0;
  acc[curr] += 1;
  return acc;
}, {});
Build.find({
  $where: `function() {
    // inject variables
    const selectedUnitCounts = ${JSON.stringify(selectedUnits)};
    const time = ${time};
    
    const buildUnitCounts = this.buildOrder.reduce((acc, bo) => {
      if (bo.time <= time) {
        const key = bo.unit
        acc[key] = acc[key] || 0;
        acc[key] += 1;
      }
      return acc
    }, {})
    return Object.keys(selectedUnitCounts).every(key => {
      return buildUnitCounts[key] >= selectedUnitCounts[key];
    });
  }`,
});