mongodbexpressmongoosemongodb-querymongoose-schema

How to have mongoDB populate subfields of transactions collection, for the sender and receiver, based on their _ids provided from the users collection


Here's my Mongoose Schema for the three collections users, accounts and transactions respectively:

const userSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true,
        trim: true,
        lowercase: true,
        minLength: 3,
        maxLength: 30
    },
    password: {
        type: String,
        required: true,
        minLength: 6
    },
    firstName: {
        type: String,
        required: true,
        trim: true,
        maxLength: 50
    },
    lastName: {
        type: String,
        required: true,
        trim: true,
        maxLength: 50
    }
});

const accountSchema = new mongoose.Schema({
    user: {
        type: mongoose.Schema.Types.ObjectId, 
        ref: "User"
    },
    balance: {
        type: Number,
        minLength: 0,
        required: true
    }
})

const transactionSchema = new mongoose.Schema({
    sender: {
        _id: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "User",
            required: true
        },
        firstName: {
            type: String,
            required: true
        },
        lastName: {
            type: String,
            required: true
        }
    },
    receiver: {
        _id: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "User",
            required: true
        },
        firstName: {
            type: String,
            required: true
        },
        lastName: {
            type: String,
            required: true
        }
    },
    amount: {
        type: Number,
        required: true
    }
});

The transactionsSchema was defined as such with the hope that mongoose will figure out, due to the _id's ref: "User" property (don't judge me on this one, chatGPT advised me so. And that, you can judge me on), to automatically populate the firstName and lastName fields for the sender and receiver.

The transactionsSchema, as it is defined currently, does not allow the following money transfer logic to happen:

accountRouter.post("/transfer", authMiddleware, async (req, res) => {
    const session = await mongoose.startSession();

    session.startTransaction();
    const { amount, to } = req.body;
    
    //...some logic that's not relevant to this question
    //...

    // Perform the transfer
    await AccountModel.updateOne({ user: req.body.userId }, { $inc: { balance: -amount } }).session(session);
    await AccountModel.updateOne({ user: to }, { $inc: { balance: amount } }).session(session);
    await TransactionModel.create({ sender: {_id: req.body.userId}, receiver: {_id: to}, amount: amount });

    // Commit the transaction
    await session.commitTransaction();
    res.json({
        message: "Transfer successful"
    });
});

It throws an error that firstName and lastName are required (as is specified in the schema, i guess). I tried removing the required property and in that case, the transaction works...but no firstName or lastName for the sender/receiver exist in the newly created record.

so yeah, the question is: how do I only give the _ids of the sender and receiver in the request, and have the _id, firstName, lastName all be saved in the record created in the transactions collection?

purpose: I want to create a page where the user can view their recent transactions, and so it'll be good to have the names in the transactions collection itself, otherwise i'll have to make a separate DB call to fetch that.

Thank you so much for reaching this point. Peace be with you.


Solution

  • You can modify your your transactionSchema so that sender and receiver are just keys whose value is an ObjectId that corresponds to a valid User._id.

    const transactionSchema = new mongoose.Schema({
       sender: {
          type: mongoose.Schema.Types.ObjectId,
          ref: "User",
          required: true
       },
       receiver: {
          type: mongoose.Schema.Types.ObjectId,
          ref: "User",
          required: true
       },
       amount: {
          type: Number,
          required: true
       }
    });
    

    Now when you create the Transaction document you can simply do:

    await TransactionModel.create({ 
       sender: req.body.userId,
       receiver: to,
       amount: amount
    });
    

    Whenever you make a query on the transactions collection and you want to get the details of each of those sender and receiver documents you can use populate like so:

    const transaction = await TransactionModel.find()
    .populate('sender')
    .populate('receiver');
    

    And if you only want the firstName and lastName you can do:

    const transaction = await TransactionModel.find()
    .populate({path: 'sender', select: 'firstName lastName'})
    .populate({path: 'receiver', select: 'firstName lastName'});