Search code examples
javascriptmongodbmongodb-nodejs-driver

Move elements within MongoDB document


Background:

A customer is an object that has a name field.

A line is an object that has the following fields:

  • inLine - an array of customers
  • currentCustomer - a customer
  • processed - an array of customers

The collection 'line' contains documents that are line objects.


Problem:

I'm trying to implement a procedure which would do the following:

  1. Push currentCustomer to processed
  2. Set currentCustomer to the 1st element in inLine
  3. Pop the 1st element of inLine

Since the new value of a field depends on the previous value of another, atomicity is important here.

What I tried so far:

Naive approach

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, {
    $set: {
        currentCustomer: '$inLine.0',
    },
    $pop: {
        inLine: -1,
    },
    $push: {
        processed: '$currentCustomer',
    },
});

However, currentCustomer is set to a string which is literally "$inLine.0" and processed has a string which is literally "$currentCustomer".

Aggregation approach

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, [{
    $set: {
        currentCustomer: '$inLine.0',
    },
    $pop: {
        inLine: -1,
    },
    $push: {
        processed: '$currentCustomer',
    },
}]);

However, I got the following error:

MongoError: A pipeline stage specification object must contain exactly one field.

Multi-stage aggregation approach

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, [{
    $set: {
        currentCustomer: '$inLine.0',
    },
}, {
    $pop: {
        inLine: -1,
    },
}, {
    $push: {
        processed: '$currentCustomer',
    },
}]);

However, $pop and $push are Unrecognized pipeline stage names.

I tried making it using only $set stages, but it ended up very ugly and I still couldn't get it to work.


Solution

  • Based on turivishal's answer, it was solved like so:

    db.collection('line').findOneAndUpdate({
        _id: new ObjectId(lineId),
    }, [{
        $set: {
            // currentCustomer = inLine.length === 0 ? null : inLine[0]
            currentCustomer: {
                $cond: [
                    { $eq: [{ $size: '$inLine' }, 0] },
                    null,
                    { $first: '$inLine' },
                ],
            },
            // inLine = inLine.slice(1)
            inLine: {
                $cond: [
                    { $eq: [{ $size: '$inLine' }, 0] },
                    [],
                    { $slice: ['$inLine', 1, { $size: '$inLine' }] },
                ],
            },
            // if currentCustomer !== null then processed.push(currentCustomer)
            processed: {
                $cond: [
                    {
                        $eq: ['$currentCustomer', null],
                    },
                    '$processed',
                    {
                        $concatArrays: [
                            '$processed', ['$currentCustomer'],
                        ],
                    }
                ],
            },
        },
    }]);