Search code examples
mongodbexpressmongoosemongodb-querymongoose-schema

Mongoose Schema: making an object within an array have a unique field for the document


I am not trying to make a unique array element, but rather ensure that if I have an array of pairs (which are objects) all have a different value for code.

Here is my schema's definition:

const redirectSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
    },
    name: {
        type: String,
        required: true,
        unique: true
    },
    default_link: {
        type: String,
        required: true,
    },
    pairs: [
        {
            code: { // I want this field specifically to be unique within the document only
                type: String,
                required: true
            },
            site: {
                type: String,
                required: true
            }

        }
    ],
    date: {
        type: Date,
        required: true,
        default: Date.now
    }
})

So for example, inserting a pair like so is OK.

{
    code: "US",
    site: "google.com"
}

But inserting another pair with the same code within this document should NOT be okay, regardless of the site.

{
    code: "US", // Should not work, since the code "US" already exists within the array of pairs
    site: "amazon.com"
}

I'm trying to do this using Redirect.findOne() and Redirect.updateOne() but I've either been getting compiler errors or the check will only be performed after a duplicate is already present.

Here's my route so far (allows duplicate codes):

// Add pair to given redirect
router.post('/:name/insert_pair', verifyToken, async (request, response) => {
    const token = request.header("auth-token")
    const usernameFromToken = jwt.decode(token).username

    const { code, site } = request.body

    // Remember: you may edit only your own content
    try {
        const edited = await Redirect.findOneAndUpdate(
            {
                username: usernameFromToken,
                name: request.params.name
            },
            {
                $push: {
                    pairs: { code, site }
                }
            }
        )

        const specific = await Redirect.findOne(
            {
                username: usernameFromToken,
                name: request.params.name
            })

        response.send(specific)
    } catch (error) {
        response.status(400).json(error)
    }

Solution

  • The issue in my code laid in the fact that I was using a forEach look to traverse the pair objects within my document's array.

    Since I'm using an async function for my POST route, I did some searching and found that forEach does not play well within an async function. I switched to using an oldschool for (let i = 0; loop and it now works fine!

    Here's my code.

    1. Get the document and check the pairs array

    2. If pairs contains an object that has the requested code, return an error message.

    3. If not, add the pair object to the document.

    // Add pair to given redirect
    router.post('/:name/insert_pair', verifyToken, async (request, response) => {
        const token = request.header("auth-token")
        const usernameFromToken = jwt.decode(token).username
    
        const { code, site } = request.body
    
        // Remember: you may edit only your own content
        try {
            // Get the object as plain JSON and check if it contains the code
            const check = await Redirect.findOne(
                {
                    username: usernameFromToken,
                    name: request.params.name
                }
            ).lean()
    
            // SOLUTION: DON'T USE FOREACH WITHIN ASYNC!
            for (let i = 0; i < check.pairs.length; i++) {
                if (check.pairs[i].code === code) {
                    return response.status(400).json({
                        message: `${request.params.name} already contains an entry for ${code}`
                    })
                }
            }
    
            // TODO BUG: ONLY WORKS AFTER 1 DUPLICATE HAS ALREADY BEEN MADE.
            // await check.pairs.forEach((pair) => {
            //     if (pair.code === code) {
            //         return response.status(400).json({
            //             message: `${request.params.name} already contains an entry for ${code}`
            //         })
            //     }
            // })
    
            // Make the edit
            await Redirect.findOneAndUpdate(
                {
                    username: usernameFromToken,
                    name: request.params.name
                },
                {
                    $push: {
                        pairs: { code, site }
                    }
                }
            )
    
            // Get it to display it on screen
            const display = await Redirect.findOne(
                {
                    username: usernameFromToken,
                    name: request.params.name
                })
    
            response.json(display)
        } catch (error) {
            response.status(400).json(error)
        }
    })