diff --git a/README.md b/README.md index 35019cd8a..9f511b9ef 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ # Project Mongo API -Replace this readme with your own information about your project. +This is an API built using Node.js, Express, and MongoDB, designed to provide information about Harry Potter characters. The API allows users to retrieve details about characters, filter them by various attributes, and query individual characters based on their unique IDs or names. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +## The problem & process -## The problem +- I began by importing my JSON file containing Harry Potter characters and defining a Mongoose model. Next, I created an environment function to seed the database. Since I had previously created routes for a similar API using plain JavaScript, I reused those routes and endpoints but adapted them to work with Mongoose. -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +- I wanted to allow users to filter characters by role, house, and year using query parameters, enabling them to filter by multiple criteria at once. To achieve this, I created a dynamic query object using conditional statements. With Mongoose's find method, the query object enabled filtering on individual attributes or a combination of them. + +- To make the filtering case-insensitive, I used regular expressions with the i flag, allowing users to enter queries in both lowercase and uppercase letters. + +- Initially, I used the id from the JSON file to filter characters by their unique identifier. However, since MongoDB automatically generates an ObjectId, I decided to remove the custom id field from the JSON file. Instead, I used new mongoose.Types.ObjectId to generate new IDs and implemented filtering based on the default ObjectId. ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +https://project-mongo-api-8bgk.onrender.com/ + diff --git a/data/harry-potter-characters.json b/data/harry-potter-characters.json new file mode 100644 index 000000000..016bec36c --- /dev/null +++ b/data/harry-potter-characters.json @@ -0,0 +1,352 @@ +[ + { + "name": "Harry Potter", + "house": "Gryffindor", + "role": "Student", + "rating": 9.8, + "yearIntroduced": 1997 + }, + { + "name": "Hermione Granger", + "house": "Gryffindor", + "role": "Student", + "rating": 9.7, + "yearIntroduced": 1997 + }, + { + "name": "Ron Weasley", + "house": "Gryffindor", + "role": "Student", + "rating": 9.5, + "yearIntroduced": 1997 + }, + { + "name": "Albus Dumbledore", + "house": "Gryffindor", + "role": "Headmaster", + "rating": 9.9, + "yearIntroduced": 1997 + }, + { + "name": "Severus Snape", + "house": "Slytherin", + "role": "Professor", + "rating": 9.6, + "yearIntroduced": 1997 + }, + { + "name": "Draco Malfoy", + "house": "Slytherin", + "role": "Student", + "rating": 8.7, + "yearIntroduced": 1997 + }, + { + "name": "Minerva McGonagall", + "house": "Gryffindor", + "role": "Professor", + "rating": 9.8, + "yearIntroduced": 1997 + }, + { + "name": "Rubeus Hagrid", + "house": "Gryffindor", + "role": "Keeper of Keys", + "rating": 9.4, + "yearIntroduced": 1997 + }, + { + "name": "Lord Voldemort", + "house": "Slytherin", + "role": "Antagonist", + "rating": 9.2, + "yearIntroduced": 1997 + }, + { + "name": "Sirius Black", + "house": "Gryffindor", + "role": "Order of the Phoenix", + "rating": 9.5, + "yearIntroduced": 1999 + }, + { + "name": "Remus Lupin", + "house": "Gryffindor", + "role": "Professor", + "rating": 9.6, + "yearIntroduced": 1999 + }, + { + "name": "Ginny Weasley", + "house": "Gryffindor", + "role": "Student", + "rating": 8.9, + "yearIntroduced": 1997 + }, + { + "name": "Neville Longbottom", + "house": "Gryffindor", + "role": "Student", + "rating": 8.8, + "yearIntroduced": 1997 + }, + { + "name": "Luna Lovegood", + "house": "Ravenclaw", + "role": "Student", + "rating": 9.3, + "yearIntroduced": 2003 + }, + { + "name": "Cho Chang", + "house": "Ravenclaw", + "role": "Student", + "rating": 7.8, + "yearIntroduced": 1999 + }, + { + "name": "Bellatrix Lestrange", + "house": "Slytherin", + "role": "Death Eater", + "rating": 9.0, + "yearIntroduced": 1999 + }, + { + "name": "Fred Weasley", + "house": "Gryffindor", + "role": "Student", + "rating": 8.5, + "yearIntroduced": 1997 + }, + { + "name": "George Weasley", + "house": "Gryffindor", + "role": "Student", + "rating": 8.5, + "yearIntroduced": 1997 + }, + { + "name": "Lucius Malfoy", + "house": "Slytherin", + "role": "Death Eater", + "rating": 8.2, + "yearIntroduced": 1997 + }, + { + "name": "Nymphadora Tonks", + "house": "Hufflepuff", + "role": "Auror", + "rating": 9.1, + "yearIntroduced": 1999 + }, + { + "name": "Cedric Diggory", + "house": "Hufflepuff", + "role": "Student", + "rating": 8.4, + "yearIntroduced": 1999 + }, + { + "name": "Molly Weasley", + "house": "Gryffindor", + "role": "Order of the Phoenix", + "rating": 9.2, + "yearIntroduced": 1997 + }, + { + "name": "Arthur Weasley", + "house": "Gryffindor", + "role": "Ministry Worker", + "rating": 8.7, + "yearIntroduced": 1997 + }, + { + "name": "Peter Pettigrew", + "house": "Gryffindor", + "role": "Death Eater", + "rating": 6.8, + "yearIntroduced": 1997 + }, + { + "name": "Fleur Delacour", + "house": "Beauxbatons", + "role": "Triwizard Champion", + "rating": 8.3, + "yearIntroduced": 2000 + }, + { + "name": "Viktor Krum", + "house": "Durmstrang", + "role": "Triwizard Champion", + "rating": 8.1, + "yearIntroduced": 2000 + }, + { + "name": "Moaning Myrtle", + "house": "Ravenclaw", + "role": "Ghost", + "rating": 7.5, + "yearIntroduced": 1997 + }, + { + "name": "Nearly Headless Nick", + "house": "Gryffindor", + "role": "Ghost", + "rating": 7.8, + "yearIntroduced": 1997 + }, + { + "name": "Dobby", + "house": "N/A", + "role": "House Elf", + "rating": 9.7, + "yearIntroduced": 1998 + }, + { + "name": "Kreacher", + "house": "N/A", + "role": "House Elf", + "rating": 8.0, + "yearIntroduced": 1997 + }, + { + "name": "Horace Slughorn", + "house": "Slytherin", + "role": "Professor", + "rating": 8.5, + "yearIntroduced": 2006 + }, + { + "name": "Gilderoy Lockhart", + "house": "Ravenclaw", + "role": "Professor", + "rating": 6.5, + "yearIntroduced": 1992 + }, + { + "name": "Argus Filch", + "house": "N/A", + "role": "Caretaker", + "rating": 5.0, + "yearIntroduced": 1997 + }, + { + "name": "Peeves", + "house": "N/A", + "role": "Poltergeist", + "rating": 7.0, + "yearIntroduced": 1997 + }, + { + "name": "Barty Crouch Jr.", + "house": "Slytherin", + "role": "Death Eater", + "rating": 8.1, + "yearIntroduced": 1994 + }, + { + "name": "Fenrir Greyback", + "house": "N/A", + "role": "Werewolf", + "rating": 7.2, + "yearIntroduced": 1997 + }, + { + "name": "Dolores Umbridge", + "house": "Slytherin", + "role": "Professor", + "rating": 6.0, + "yearIntroduced": 1995 + }, + { + "name": "Mad-Eye Moody", + "house": "Hufflepuff", + "role": "Auror", + "rating": 9.0, + "yearIntroduced": 1995 + }, + { + "name": "Kingsley Shacklebolt", + "house": "Hufflepuff", + "role": "Auror", + "rating": 9.2, + "yearIntroduced": 1995 + }, + { + "name": "Rita Skeeter", + "house": "Ravenclaw", + "role": "Journalist", + "rating": 7.5, + "yearIntroduced": 1992 + }, + { + "name": "The Grey Lady (Helena Ravenclaw)", + "house": "Ravenclaw", + "role": "Ghost", + "rating": 8.3, + "yearIntroduced": 1997 + }, + { + "name": "The Bloody Baron", + "house": "Slytherin", + "role": "Ghost", + "rating": 8.0, + "yearIntroduced": 1991 + }, + { + "name": "Salazar Slytherin", + "house": "Slytherin", + "role": "Founder", + "rating": 8.7, + "yearIntroduced": 1991 + }, + { + "name": "Godric Gryffindor", + "house": "Gryffindor", + "role": "Founder", + "rating": 9.5, + "yearIntroduced": 1991 + }, + { + "name": "Helga Hufflepuff", + "house": "Hufflepuff", + "role": "Founder", + "rating": 8.9, + "yearIntroduced": 1991 + }, + { + "name": "Rowena Ravenclaw", + "house": "Ravenclaw", + "role": "Founder", + "rating": 9.2, + "yearIntroduced": 1991 + }, + { + "name": "Horace Slughorn", + "house": "Slytherin", + "role": "Professor", + "rating": 8.4, + "yearIntroduced": 1997 + }, + { + "name": "Narcissa Malfoy", + "house": "Slytherin", + "role": "Mother", + "rating": 7.9, + "yearIntroduced": 1997 + }, + { + "name": "Astoria Greengrass", + "house": "Slytherin", + "role": "Wife", + "rating": 7.4, + "yearIntroduced": 2007 + }, + { + "name": "Bill Weasley", + "house": "Gryffindor", + "role": "Order of the Phoenix", + "rating": 8.7, + "yearIntroduced": 1994 + } +] \ No newline at end of file diff --git a/package.json b/package.json index 6830a48aa..0e1445df8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", + "dotenv": "^16.4.7", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", "mongoose": "^8.0.0", "nodemon": "^3.0.1" } diff --git a/server.js b/server.js index 647e7b144..dcdd46c55 100644 --- a/server.js +++ b/server.js @@ -1,35 +1,121 @@ import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import dotenv from "dotenv"; +import listEndpoints from "express-list-endpoints"; +import harryPotterCharactersData from "./data/harry-potter-characters.json"; -// If you're using one of our datasets, uncomment the appropriate import below -// to get started! -// import avocadoSalesData from "./data/avocado-sales.json"; -// import booksData from "./data/books.json"; -// import goldenGlobesData from "./data/golden-globes.json"; -// import netflixData from "./data/netflix-titles.json"; -// import topMusicData from "./data/top-music.json"; +dotenv.config(); -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; +//using the Atlas URL from .env, or fall back to local MongoDB localhost +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/harrypottercharacters"; mongoose.connect(mongoUrl); -mongoose.Promise = Promise; -// Defines the port the app will run on. Defaults to 8080, but can be overridden -// when starting the server. Example command to overwrite PORT env variable value: -// PORT=9000 npm start -const port = process.env.PORT || 8080; +// Defining a Mongoose model for a Harry Potter character +const HarryPotterCharacter = mongoose.model("HarryPotterCharacter", { + id: Number, + name: String, + house: String, + role: String, + rating: Number, + yearIntroduced: Number +}); + +const port = process.env.PORT || 9000; const app = express(); +// If the RESET_DB environment variable is true, the database is seeded with JSON data +if (process.env.RESET_DB) { + const seedDatabase = async () => { + await HarryPotterCharacter.deleteMany({}); // Clears the existing database + harryPotterCharactersData.forEach((character) => { + const newCharacter = new HarryPotterCharacter(character); // Creating a new character instance + newCharacter.save(); + }); + }; + seedDatabase(); +}; + + // Add middlewares to enable cors and json body parsing app.use(cors()); app.use(express.json()); -// Start defining your routes here +// Middleware to handle 503 errors +app.use((req, res, next) => { + if (mongoose.connection.readyState === 1) { + next() + } else { + res.status(503).json({ error: "Service unavailable" }); + } +}); + +// Start defining your routes here (added listEndpoints) app.get("/", (req, res) => { - res.send("Hello Technigo!"); + res.json({ + message: "Welcome to the Harry Potter API!", + routes: listEndpoints(app), + }); +}); + +app.get("/harryPotterCharacters", async (req, res) => { + const { house, role, yearIntroduced } = req.query; // Extracting query params + + // Creating a dynamic query object to be able to filter on more than one thing at the time + const query = {}; //Initially an empty query object before the user has applied filters + if (house) query.house = new RegExp(house, "i"); //Depending on the query parameters provided - properties are dynamically added to the query object + if (role) query.role = new RegExp(role, "i"); + if (yearIntroduced) query.yearIntroduced = +yearIntroduced; + // "i" stands for case-insensitive matching: eg. makes the regular expression ignore differences between uppercase and lowercase letters + try { + // Query MongoDB using the dynamic query object + const characters = await HarryPotterCharacter.find(query); + + if (characters.length === 0) { + res.status(404).json({ message: "Sorry - no characters found" }); + } else { + res.json(characters); + } + } catch (error) { + res.status(400).json({ error: "Server error" }); + } +}); + + +app.get("/harryPotterCharacters/:id", async (req, res) => { + const { id } = req.params; // Extracting the id from the URL + + try { + // Converting the string to ObjectId using "new" + const character = await HarryPotterCharacter.findById(new mongoose.Types.ObjectId(id)); + if (character) { + res.status(200).json(character); + } else { + res.status(404).send("Character not found"); + } + } catch (error) { + res.status(400).json({ error: "Invalid ID format or server error", details: error.message }); + } }); +//finding the unique character name +app.get("/harryPotterCharacters/name/:name", async (req, res) => { + const name = req.params.name; // Extracting the name from the URL + + try { // "new RegExp(`^${name}$`, "i")" ensures case-insensitive matching + const harryPotterCharacterName = await HarryPotterCharacter.findOne({ name: new RegExp(`^${name}$`, "i") }); + if (harryPotterCharacterName) { + res.status(200).json(harryPotterCharacterName); + } else { + res.status(404).send("Sorry - no character found with that name"); + } + } catch (error) { + res.status(400).json({ error: "Server error" }); + } +}); + + // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); + console.log(`Server running on http://localhost:${port}`); });