Skip to content

Instantly share code, notes, and snippets.

@cklanac
Last active November 3, 2018 08:45
Show Gist options
  • Save cklanac/ccee3eea29a3af1c911ba658e6f37b22 to your computer and use it in GitHub Desktop.
Save cklanac/ccee3eea29a3af1c911ba658e6f37b22 to your computer and use it in GitHub Desktop.
Challenge 14: Mongo(ose) Relationships

For this challenge, you will add Folders to the Noteful app, associate the notes with the folders, create routes and tests for the folders and update the routes and test for the notes.

Requirements

  • Create folder schema, update notes schema and seed the database
  • Create folder endpoints and update notes endpoints
  • Update server.js mount folder router
  • Update the client

Create/Update schemas and seed the database

Your first challenge is to create a Folder Schema and Model, update Notes Schema with folderId and then seed the database.

Create a Folder Schema and Model

Since you created a similar schema and model for Notes in an earlier challenge we will keep the instructions brief.

  • Create a /models/folder.js file for the Schema and Model.
  • Create a simple folderSchema and define a name field with the following criteria:
    • Type is String
    • Required is true
    • Unique is true
  • Create timestamps fields createdAt and updatedAt using the Mongoose timestamps option
  • Configure the Mongoose toObject option to transform the doc:
    • Include virtuals like id to show in the output
    • Suppress the __v version key
    • Delete the _id from the return object
  • Create Folder model using the folderSchema and export it

Add folderId to the Note Schema

Now that you have defined a Folder Schema and Model, you can add a folderId field that reference to the Notes Schema. This field informs Mongoose that there is a relationship between Notes and Folders.

In /models/note.js, update the noteSchema as follows:

  • Define a folderId field:
    • Type is ObjectId
    • References Folder model
  folderId: { type: mongoose.Schema.Types.ObjectId, ref: 'Folder' }

Update Seed Data

Next you will create Folders seed data and update Notes seed data with folderId references, then modify and run /utils/seed-database.js to update your database with the new sample data.

  • Create /db/seed/folders.json file
  • Add seed data like the following example.
[
  {
    "_id": "111111111111111111111100",
    "name": "Archive"
  },
  {
    "_id": "111111111111111111111101",
    "name": "Drafts"
  },
  {
    "_id": "111111111111111111111102",
    "name": "Personal"
  },
  {
    "_id": "111111111111111111111103",
    "name": "Work"
  }
]
  • In /db/seed/notes.json, add folderId fields which reference a folder.

Below is a sample note with folderId set to the _id for Archive.

  {
    "_id": "000000000000000000000000",
    "title": "5 life lessons learned from cats",
    "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...",
    "folderId": "111111111111111111111100"
  }

Modify and Run seed-database.js Util

Now that you have created a Folder schema and model, updated the Notes schema and created seed data, you can modify and run the seed database util to update your database.

Below are the steps followed by sample code to help you along.

  • In /utils/seed-database.js
    • Require the folder model and seed data
    • Add Folder.insertMany() and pass it the seed data
    • Add Folder.createIndexes()

The Folder.createIndexes() tells Mongo to index the Folders data immediately. The index is used enforce the unique folder names rule you created in the schema.

Normally, the index is automatically created in the background. But that leaves open a small window of time when you could, in theory, seed the database and then create a folder with a duplicate name. Calling createIndexes() forces Mongo to create the index and prevents error from happening.

Below is a simple and reliable way to seed and index the database. The const and require statements, along with .then() and .catch() have been removed for brevity. You will need to add them in your implementation.

REMOVED FOR BREVITY

mongoose.connect(MONGODB_URI)
  .then(() => mongoose.connection.db.dropDatabase())
  .then(() => {
    return Promise.all([
      Note.insertMany(seedNotes),
      Folder.insertMany(seedFolders),
      Folder.createIndexes(),
    ]);
  })

REMOVED FOR BREVITY

Finally, run the seed database util to update your database.

node utils/seed-database.js

Confirm it worked correctly by verifying the data using Mongo shell or your favorite database GUI.

Time to commit your work. Did you remember to create a feature branch?

Create Folder endpoints and Integration Tests

Your next challenge is to create Folder endpoints and integration tests. Again, you've created similar endpoints and tests so we will keep the instructions brief. You can refer to your earlier work for additional details.

Create Folder Endpoints

Create a /routes/folders.js file for the endpoints and mount it on server.js. In the file, create the following endpoints.

  • GET all /folders
    • Sort by name
  • GET /folders by id
    • Validate the id is a Mongo ObjectId
    • Conditionally return a 200 response or a 404 Not Found
  • POST /folders to create a new folder
    • Validate the incoming body has a name field
    • Respond with a 201 status and location header
    • Catch duplicate key error code 11000 and respond with a helpful error message (see below for sample code)
  • PUT /folders by id to update a folder name
    • Validate the incoming body has a name field
    • Validate the id is a Mongo ObjectId
    • Catch duplicate key error code 11000 and respond with a helpful error message
  • DELETE /folders by id which deletes the folder AND the related notes
    • Respond with a 204 status

Handle duplicate key error code 11000

The current error handling process catches errors and returns them to the user. Usually this is fine, but returning database errors is considered a bad practice. Besides being a poor user-experience then might provide information that hackers can exploit. So, in .catch() you will check the error for code 11000, which is Mongo's code for "duplicate key error", then you will create a user-friendly error and status. Finally, call next(err) to trigger our error handler process to return the error to the user.

  • Update the appropriate query .catch() methods with the following code. Note, you should not update all queries, knowing/understanding which ones to update is part of the challenge.
  .catch(err => {
    if (err.code === 11000) {
      err = new Error('The folder name already exists');
      err.status = 400;
    }
    next(err);
  });

BONUS CHALLENGE(s)

After you have completed the rest of the challenge, comeback to this bonus challenge.

If you followed the instruction above, then deleting a folder performs a ON DELETE SET CASCADE style delete on Notes. In other words, deleting a folder deletes all the associated Notes. The challenge is to implement a ON DELETE SET NULL style delete. When a user deletes a folder you must find and $unset all notes with that folderId. This approach removes the folderId reference but keeps the Note.

Create Folder Integration tests

Next create integration tests to verify the Folder endpoints. Since this process is similar to the integration tests for Notes, we won't bore you with the details.

Create a /test/folders.js file and create integration tests for Folders based on the specs above. You can use the integration tests for Notes as a reference.

Update Notes endpoints to include folders

You are in the home stretch, your last challenge is to update the Notes endpoints include folder info and update the tests.

Update the Notes endpoints

Update the Notes endpoints to handle folders:

  • GET: /api/notes
    • Check if request contains folderId in the querystring and add a filter which to find notes with the given folderId
  • GET: /api/notes:id
    • No changes needed. Use Postman to confirm the folderId is returned in the response
  • POST: /api/notes
    • Check if request contains a folderId and verify it is a valid Mongo ObjectId, if not valid respond with an error
  • PUT: /api/notes
    • Check if request contains a folderId and verify it is a valid Mongo ObjectId, if not valid respond with an error
  • DELETE: /api/notes
    • No changes :)

Update Notes Integration Tests

Update /test/notes.js. You added new features to these endpoints and that functionality needs to be tested to ensure it continues to work properly.

  • Update the require statements to include folders.
  • Update the life-cycle hooks to seed the database with both notes and folders
  • Update the existing tests to properly handle and check for folderId

You can refer to the solution branches for "inspiration".

Update the Client

Activate folders search on client

Finally, uncomment the folders search on public/index.js.

  Promise.all([
    api.search('/api/notes'),
    api.search('/api/folders'),
    //api.search('/api/tags')
  ])

Solutions

You can view an example solution and compare the differences between branches

  • Solution: You can find the example solution in the /solution/02-folders branch in the upstream repo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment