Search code examples
mongodbmongoosemongoose-schema

MongoDB, Mongoose: How to refer to a document inside an array of another document?


I have been working with MongoDB for just over a month, and I do not have much experience with Relational Databases as well. I would try to explain what I am trying to do below.

I have a schema in mongoose like this:

import mongoose, { model, Schema } from "mongoose";
import { IBudget } from "../types/budget";
import User from "./user.model";

const budgetSchema = new Schema<IBudget>({
  user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
  categories: {
    type: [
      {
        title: { type: "string", required: true, trim: true },
        amount: { type: "number", required: true, default: 0 },
        color: { type: "string", required: true },
        managed: { type: "boolean", required: true, default: true },
        editable: { type: "boolean", required: true, default: true },
        description: { type: "string", maxlength: 120 },
      },
    ],
    required: true,
    maxlength: [12, "Cannot have more than 12 categories"],
    minlength: [1, "Need at least 1 cateogry"],
  },
  month: {
    type: Number,
    required: [true, "Please add month"],
    min: 1,
    max: 12,
  },
  year: { type: Number, required: [true, "Please add the year"] },
});

const Budget = model("Budget", budgetSchema);

export default Budget;

With this, when I create budget document using Budget.create({...budgetObject}), I get the following document created in the database. enter image description here

I have another schema called Expense like this:

import mongoose, { model, Schema } from "mongoose";
import { IExpense } from "../types/expense";
import User from "./user.model";

const expenseSchema = new Schema<IExpense>(
  {
    title: { type: String, required: true, trim: true, maxlength: 40 },
    description: { type: String, trim: true, maxlength: 260 },
    expenseDate: { type: Date, default: Date.now },
    category: {
      type: String,
      required: [true, "Please add the category name"],
    },
    user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
    amount: { type: Number, required: true },
    reverted: { type: Boolean, required: true, default: false },
  },
  { timestamps: false }
);

const Expense = model("Expense", expenseSchema);
export default Expense;

My Question is: How do I refer the category of the expense document to one of the items in the categories array within the Budget document?

I know how to refer to other documents by using { type: mongoose.Schema.Types.ObjectId, required: true, ref: User }, as you have seen. But I don't understand how that would work for a subdocument or a document inside of an array field nested in another document.

I am not even sure whether this is possible, in which case, an alternative approach for the same behavior would be highly appreciated.


Solution

  • It looks like you've learnt a lot in a month :-).

    For your use case, instead of hardcoding the category field in the budgetSchema, you should first create a categorySchema like:

    import { model, Schema } from "mongoose";
    import { ICategory } from "../types/category";
    
    const categorySchema =
      new Schema() <
      ICategory >
      {
        title: { type: "string", required: true, trim: true },
        amount: { type: "number", required: true, default: 0 },
        color: { type: "string", required: true },
        managed: { type: "boolean", required: true, default: true },
        editable: { type: "boolean", required: true, default: true },
        description: { type: "string", maxlength: 120 },
      };
    
    const Category = model("Category", categorySchema);
    
    export default Category;
    

    Then in your budgetSchema, your categories field will be an array of categorySchema object ids as such:

    import mongoose, { model, Schema } from "mongoose";
    import { IBudget } from "../types/budget";
    import User from "./user.model";
    import Category from "./category.model";
    
    const budgetSchema =
      new Schema() <
      IBudget >
      {
        user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
        categories: [
          { type: mongoose.Schema.Types.ObjectId, required: true, ref: Category },
        ],
        month: {
          type: Number,
          required: [true, "Please add month"],
          min: 1,
          max: 12,
        },
        year: { type: Number, required: [true, "Please add the year"] },
      };
    
    const Budget = model("Budget", budgetSchema);
    
    export default Budget;
    

    Since the categories are now documents of a separate schema, you'll now be able to reference them in your expenseSchema like this:

    import mongoose, { model, Schema } from "mongoose";
    import { IExpense } from "../types/expense";
    import User from "./user.model";
    import Category from "./category.model";
    
    const expenseSchema =
      new Schema() <
      IExpense >
      ({
        title: { type: String, required: true, trim: true, maxlength: 40 },
        description: { type: String, trim: true, maxlength: 260 },
        expenseDate: { type: Date, default: Date.now },
        category: {
          type: mongoose.Schema.Types.ObjectId,
          required: [true, "Please add the category name"],
          ref: Category,
        },
        user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
        amount: { type: Number, required: true },
        reverted: { type: Boolean, required: true, default: false },
      },
      { timestamps: false });
    
    const Expense = model("Expense", expenseSchema);
    export default Expense;