skip to Main Content

In my data, one of the fields is an array and I want to return only a section of that array, and only that, no more fields. But when I’m using the $slice in projection, it will return all the fields.

My data:

{
  "_id": {
    "$oid": "641eca0f5687d0937c1041e2"
  },
  "email": "[email protected]",
  "password": "$2b$10$MBuDAi3k7jTvckTsqQliQeWeg18JebLaDdltzMJwJ92bwP7i84Ee2",
  "emailVerified": true,
  "transactions": [
    {
      "category": "t2",
      "subCategory": "bread",
      "type": "expense",
      "date": "2023-03-25T16:53:59.779Z",
      "amount": "1000",
      "id": {
        "$oid": "641f2727dc59db57eac9fa29"
      }
    },
    {
      "category": "t3",
      "subCategory": "bread",
      "type": "expense",
      "date": "2023-03-25T16:54:04.243Z",
      "amount": "1000",
      "id": {
        "$oid": "641f272cdc59db57eac9fa2a"
      }
    },
    {
      "category": "t4",
      "subCategory": "bread",
      "type": "expense",
      "date": "2023-03-25T16:54:08.780Z",
      "amount": "1000",
      "id": {
        "$oid": "641f2730dc59db57eac9fa2b"
      }
    }
  ]
}

And my query in node.js is like this:

const result = await users.findOne(
  { _id: new ObjectId(id) },
  { projection: { transactions: { $slice: [1, 2] } }}
);

With this projection, I’m getting all the fields in the result. If I change the projection to something like { transactions: { $slice: [1, 2] }, _id: 1 }, it will return only the transactions and _id. But I only want the transactions.

Update:

After some research, I found that this is possible with aggregate method. What aggregate is doing, is basically recreating the projection based on previous stage on the pipeline. But still, I don’t know about the projection on findOne method.

const aggCursor = users.aggregate<TransactionsPagRes>([
  {
    $match: { _id: new ObjectId(id) },
  },
  {
    $project: {
      _id: 0,
      transactions: { $slice: ['$transactions', 1, 2] },
    },
  },
]);

const result = await aggCursor.next();

2

Answers


  1. If you are fine with transactions being returned, you need to blacklist other properties you don’t need like this:

    db.collection.find({},
    {
      transactions: {
        $slice: [
          1,
          2
        ]
      },
      _id: 0,
      email: 0,
      password: 0,
      emailVerified: 0
    })
    

    It will return info as:

    [
      {
        "transactions": [
          {
            "amount": "1000",
            "category": "t3",
            "date": "2023-03-25T16:54:04.243Z",
            "id": ObjectId("641f272cdc59db57eac9fa2a"),
            "subCategory": "bread",
            "type": "expense"
          },
          {
            "amount": "1000",
            "category": "t4",
            "date": "2023-03-25T16:54:08.780Z",
            "id": ObjectId("641f2730dc59db57eac9fa2b"),
            "subCategory": "bread",
            "type": "expense"
          }
        ]
      }
    ]
    

    If you want to have a flat array of user’s transactions, aggregation with $unwind is a way to go:

    db.collection.aggregate([
      {
        $match: {
          _id: "641eca0f5687d0937c1041e2"
        },
        
      },
      {
        "$project": {
          transactions: {
            $slice: [
              "$transactions",
              1,
              2
            ]
          }
        }
      },
      {
        "$unwind": "$transactions"
      }
    ])
    

    Returns:

    [
      {
        "_id": "641eca0f5687d0937c1041e2",
        "transactions": {
          "amount": "1000",
          "category": "t3",
          "date": "2023-03-25T16:54:04.243Z",
          "id": ObjectId("641f272cdc59db57eac9fa2a"),
          "subCategory": "bread",
          "type": "expense"
        }
      },
      {
        "_id": "641eca0f5687d0937c1041e2",
        "transactions": {
          "amount": "1000",
          "category": "t4",
          "date": "2023-03-25T16:54:08.780Z",
          "id": ObjectId("641f2730dc59db57eac9fa2b"),
          "subCategory": "bread",
          "type": "expense"
        }
      }
    ]
    

    MongoDB Playground.

    Login or Signup to reply.
  2. Using the $slice operator does not appear to exclude other fields like a projection normally would, this seems to be unexpected behavior in MongoDB.

    A couple of choices to get the behavior you want:

    1. modify the projection passed to the find
      Since an projection will never implicitly exclude _id, you will need to modify this anyway. To exclude all of the other fields, either explicitly exclude every field, or include a field that does not actually exist.
    const result = await users.findOne(
      { _id: new ObjectId(id) },
      { projection: { 
         _id:0,
         _not_a_real_field: 1,
         transactions: { $slice: [1, 2] } 
      }}
    );
    

    Playground

    1. use an aggregation pipeline
      You will still need to explicitly exclude the _id, but would not need to reference a non-existent field to exclude the others
    [
      {$match: {_id: ObjectId("641eca0f5687d0937c1041e2")}}},
      {$project: {
          _id: 0,
          transactions: {$slice: ["$transactions",1,2]}
      }}
    ]
    

    Playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search