diff --git a/completed/recipes/api.js b/completed/recipes/api.js index d5a7ab2..726e984 100644 --- a/completed/recipes/api.js +++ b/completed/recipes/api.js @@ -25,7 +25,7 @@ const pool = new pg.Pool({ user: "postgres", host: "localhost", database: "recipeguru", - password: "lol", + password: "ryan", port: 5432, }); diff --git a/index.js b/index.js index 0ee9275..cf520da 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,19 @@ const path = require("path"); const completed = require("./completed"); const recipes = require("./recipes/api"); const ingredients = require("./ingredients/api"); +const movies = require("./paginationPractice") +const rateLimit = require('express-rate-limit') + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + limit: 5, // Limit each IP to 100 requests per `window` (here, per 15 minutes) + message: "Too many requests from this IP, please try again after 15 minutes", + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}) -const app = express(); +const app = express(); app.get("/", (_, res) => res.sendFile(path.join(__dirname, "./index.html"))); app.get("/style.css", (_, res) => res.sendFile(path.join(__dirname, "./style.css")) @@ -19,6 +29,7 @@ app.use("/images", express.static("./images")); app.use("/completed", completed); app.use("/recipes", recipes); app.use("/ingredients", ingredients); +app.use("/movies", limiter, movies); app.get("/hello", (req, res) => res.json({ status: "ok" })); diff --git a/ingredients/api.js b/ingredients/api.js index c507aae..b3b6f25 100644 --- a/ingredients/api.js +++ b/ingredients/api.js @@ -1,7 +1,7 @@ const path = require("path"); const express = require("express"); const router = express.Router(); - +const pg = require("pg"); // client side static assets router.get("/", (_, res) => res.sendFile(path.join(__dirname, "./index.html"))); router.get("/client.js", (_, res) => @@ -14,13 +14,28 @@ router.get("/client.js", (_, res) => // connect to postgres +const pool = new pg.Pool({ + user: "postgres", + host: "localhost", + password: "ryan", + database: "recipeguru", + port: 5432 +}) + router.get("/type", async (req, res) => { const { type } = req.query; console.log("get ingredients", type); // return all ingredients of a type - - res.status(501).json({ status: "not implemented", rows: [] }); + const query = 'SELECT * FROM ingredients where type=$1'; + const values = [type]; + const result = await pool.query(query, values); + if ( result ) { + res.status(200).json({ status: "success", rows: result.rows }); + } + else { + res.status(500).json({ status: "error", rows: [] }); + } }); router.get("/search", async (req, res) => { @@ -29,9 +44,18 @@ router.get("/search", async (req, res) => { console.log("search ingredients", term, page); // return all columns as well as the count of all rows as total_count + const query = 'SELECT *, COUNT(*) OVER() AS total_count from ingredients where title ILIKE $1 OFFSET $2 LIMIT 5'; + + const values = [`%${term}%`, page * 5]; // make sure to account for pagination and only return 5 rows at a time + const result = await pool.query(query, values); - res.status(501).json({ status: "not implemented", rows: [] }); + if ( result ) { + res.status(200).json({ status: "success", rows: result.rows }); + } + else { + res.status(500).json({ status: "error", rows: [] }); + } }); /** diff --git a/package-lock.json b/package-lock.json index edde49b..b94a588 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "express": "^4.18.1", + "express-rate-limit": "^7.5.0", "image-downloader": "^4.3.0", "pg": "^8.7.3" }, @@ -299,6 +300,20 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1365,6 +1380,12 @@ "vary": "~1.1.2" } }, + "express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "requires": {} + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", diff --git a/package.json b/package.json index 5689777..a6b279c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "dependencies": { "express": "^4.18.1", + "express-rate-limit": "^7.5.0", "image-downloader": "^4.3.0", "pg": "^8.7.3" }, diff --git a/paginationPractice.js b/paginationPractice.js new file mode 100644 index 0000000..1708399 --- /dev/null +++ b/paginationPractice.js @@ -0,0 +1,59 @@ +const pg = require("pg"); +const express = require("express"); +const router = express.Router(); +const pool = new pg.Pool({ + user: "postgres", + host: "localhost", + password: "lol", + database: "omdb", + port: 5432 +}) + +router.get("/", async (req, res) => { + //Implementing seek based pagination + const limit = 10; + const cursor = req.query.cursor; + let query = `SELECT * FROM movies ORDER BY id ASC LIMIT ${limit + 1}`; + + if (cursor) { + query = `SELECT * FROM movies WHERE id > $1 ORDER BY id ASC LIMIT ${limit + 1}`; + } + + try { + const result = cursor ? await pool.query(query, [cursor]) : await pool.query(query); + const movies = result.rows; + let nextCursor = null; + if (movies.length > limit) { + nextCursor = movies.pop().id; + } + // Implementing Hateoas rules + const baseUrl = `http://${req.headers.host}${req.baseUrl}`; + console.log(baseUrl); + if (cursor) { + const prevCursor = cursor - limit; + res.json({ + movies, + links: { + self: `${baseUrl}?cursor=${cursor}`, + next: `${baseUrl}?cursor=${nextCursor}`, + prev: `${baseUrl}?cursor=${prevCursor}` + } + }); + } else { + res.json({ + movies, + links: { + self: `${baseUrl}`, + next: `${baseUrl}?cursor=${nextCursor}` + } + }); + } + + } + catch (err) { + console.log(err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +module.exports = router; diff --git a/practice.sql b/practice.sql new file mode 100644 index 0000000..d5c91bf --- /dev/null +++ b/practice.sql @@ -0,0 +1,26 @@ +SELECT +i.title as ingredient_title, +i.image as ingredient_image, +r.title as recipe_title, +r.body as recipe_body +from recipe_ingredients ri + +inner join ingredients i +on i.id = ri.ingredient_id + +inner join recipes r +on r.recipe_id = ri.recipe_id + + +SELECT +m.name, +ARRAY(SELECT ecn.name from english_category_names ecn +INNER JOIN +movie_keywords mk, +on +mk.category_id = ecn.category_id +WHERE +mk.movie_id = m.id +LIMIT 5) as keywords +FROM movies m +where name like '%star wars%' diff --git a/recipes/api.js b/recipes/api.js index c8b058e..122dd47 100644 --- a/recipes/api.js +++ b/recipes/api.js @@ -1,6 +1,7 @@ const path = require("path"); const express = require("express"); const router = express.Router(); +const pg = require("pg"); // client side static assets router.get("/", (_, res) => res.sendFile(path.join(__dirname, "./index.html"))); @@ -22,6 +23,13 @@ router.get("/detail", (_, res) => */ // connect to postgres +const pool = new pg.Pool({ + user: "postgres", + host: "localhost", + password: "ryan", + database: "recipeguru", + port: 5432 +}) router.get("/search", async function (req, res) { console.log("search recipes"); @@ -29,8 +37,15 @@ router.get("/search", async function (req, res) { // return recipe_id, title, and the first photo as url // // for recipes without photos, return url as default.jpg + const query = "SELECT DISTINCT ON (recipes.recipe_id) recipes.recipe_id, title, coalesce(recipes_photos.url, 'default.jpg') from recipes LEFT JOIN recipes_photos ON recipes.recipe_id = recipes_photos.recipe_id"; + const result = await pool.query(query); - res.status(501).json({ status: "not implemented", rows: [] }); + if (result) { + res.status(200).json({ status: "success", rows: result.rows }); + } + else { + res.status(500).json({ status: "error", rows: [] }); + } }); router.get("/get", async (req, res) => { @@ -42,16 +57,29 @@ router.get("/get", async (req, res) => { // name the ingredient type `ingredient_type` // name the ingredient title `ingredient_title` // + const ingredientPromise = pool.query(`SELECT i.title AS ingredient_title, i.image as ingredient_image, i.type as ingredient_type FROM recipe_ingredients ri INNER JOIN ingredients i on ri.ingredient_id = i.id WHERE ri.recipe_id = $1`, [recipeId]); // // return all photo rows as photos // return the title, body, and url (named the same) // // + const photoPromise = pool.query(`SELECT title, body, COALESCE(i.url, 'default.jpg') as url from recipes r LEFT JOIN recipes_photos i on r.recipe_id = i.recipe_id WHERE r.recipe_id = $1`, [recipeId]); // return the title as title // return the body as body // if no row[0] has no photo, return it as default.jpg - res.status(501).json({ status: "not implemented" }); + const [{ rows: photosRows }, { rows: ingredientsRows }] = await Promise.all([ + photoPromise, + ingredientPromise, + ]); + console.log(photosRows); + + res.json({ + ingredients: ingredientsRows, + photos: photosRows.map((photo) => photo.url), + title: photosRows[0].title, + body: photosRows[0].body, + }); }); /** * Student code ends here