diff --git a/apps/api/.env b/apps/api/.env index ecaf5a7c..0f84af61 100644 --- a/apps/api/.env +++ b/apps/api/.env @@ -1,3 +1,5 @@ +ENV=dev +TRUSTED_ORIGINS=[] ACCESS_TOKEN_SECRET=[YOUR_ACCESS_TOKEN_SECRET_HERE] REFRESH_TOKEN_SECRET=[YOUR_REFRESH_TOKEN_SECRET_HERE] -MONGO_URL=[YOUR_MONGO_CONNECTION_STRING_HERE] \ No newline at end of file +MONGO_URL=[YOUR_MONGO_CONNECTION_STRING_HERE] diff --git a/apps/api/README.md b/apps/api/README.md index f172a610..6066cc0e 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -21,34 +21,7 @@ ## Challenges -### Session 01 - -- Create `route` for `posts` endpoint with the following methods: - - `GET /posts` Return an array of all the posts with status code 200 - - `GET /posts/category/:category` Return an array of all the posts by category with status code 200 - - `GET /posts/:id` Return a post by id with category object and each comment object in the array with status code 200 - - `POST /posts` Create a new post and return the created post with status code 201 - - `POST /posts/:id/comments` Create a comment inside the post and return the comment with status code 201 - - `PATCH /posts/:id` Update post information and return the updated post with status code 200 - - `DELETE /posts/:id` Delete the post and return the deleted post with status code 200 or 204 if you decide to not return anything - * *Add 404 validation where needed* - -- Post model - - id: string - - title: string - - image: string - - description: string - - category: string *Id of the category* - - comments: array *Array of comment ids* - -- Comment model - - id: string - - author: string - - content: string - -### Session 02 - -- Refactor the code from last session to add a post controller +### Session * ## How to diff --git a/apps/api/src/config/.gitkeep b/apps/api/src/config/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/config/corsConfig.ts b/apps/api/src/config/corsConfig.ts index 045cf517..dd2e6594 100644 --- a/apps/api/src/config/corsConfig.ts +++ b/apps/api/src/config/corsConfig.ts @@ -1,15 +1,10 @@ -const allowedOrigins = ['http://localhost:4200', 'http://localhost:3000']; - -export const corsOptions = { - origin: (origin, callback) => { - // Note: origin will be undefined from same route in local development - if (allowedOrigins.indexOf(origin) !== -1 || !origin) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - optionsSuccessStatus: 200 -}; - -export default { corsOptions }; +export const corsOptions = { + origin: (origin, callback) => { + if (process.env.ENV === "dev" || process.env.TRUSTED_ORIGINS.includes(origin)) { + return callback(null, true) + } + + callback(new Error("Not Allowed By Cors")) + }, + OptionsSuccessStatus: 200 +} diff --git a/apps/api/src/controllers/.gitkeep b/apps/api/src/controllers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/controllers/category.ts b/apps/api/src/controllers/category.ts index 59294b96..999663f1 100644 --- a/apps/api/src/controllers/category.ts +++ b/apps/api/src/controllers/category.ts @@ -1,110 +1,69 @@ -// Initialize categories array to save data in memory -const categories = []; - -export const getCategory = (id: string) => { - return categories.find((p) => p.id === id); -}; - -// Get all categories -const getCategories = (req, res) => { - // Return all the categories with a 200 status code - res.status(200).json(categories); -}; - -// Get category by id -const getCategoryById = (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Check if we have a category with that id - const category = getCategory(id); - - if (!category) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - // Note: Remember that json method doesn't interrupt the workflow - // therefore is important to add a "return" to break the process - } - - // Return the category with a 200 status code - res.status(200).json(category); -}; - -// Create category -const createCategory = (req, res) => { - // Retrieve the name from the request body - const { name } = req.body; - - if (!name) { - // If name is empty or undefined return a 400 status code with a message - return res.status(400).json({ message: 'The name is required.' }); - } - - // Generate a new category - const newCategory = { - id: Date.now().toString(), // Convert id to string to match the value in get by id endpoint - name - }; - // Add the new category to our array - categories.push(newCategory); - - // Return the created category with a 201 status code - res.status(201).json(newCategory); -}; - -// Update category -const updateCategory = (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } - - // Generate a copy of our cateogory - const updatedCategory = { ...categories[categoryIndex] }; - // Retrieve the name from the request body - const { name } = req.body; - - // Check if we have a name, if so update the property - if (name) { - updatedCategory.name = name; - } - - // Update the category in our array - categories[categoryIndex] = updatedCategory; - - // Return the updated category with a 200 status code - res.status(200).json(updatedCategory); -}; - -// Delete category -const deleteCategory = (req, res) => { - // Retrieve the id from the route params - const { id } = req.params; - // Retrieve the index of the category in the array - const categoryIndex = categories.findIndex((p) => p.id === id); - - // "findIndex" will return -1 if there is no match - if (categoryIndex === -1) { - // If we don't find the category return a 404 status code with a message - return res.status(404).json({ message: 'Category not found' }); - } - - // Remove the category from the array - categories.splice(categoryIndex, 1); - - // Return a 204 status code - res.status(204).send(); -}; - -export default { - getCategories, - getCategoryById, - createCategory, - updateCategory, - deleteCategory -}; +import type { Category } from "../models/types" + +export const categories: Category[] = [] + +const findCategory = (id: string): Category | void => { + return categories.find(el => el.id === id) +} + +const getCategory = (req, res): void => { + const { id } = req.params; + const currentCategory = findCategory(id) + + if (!currentCategory) return res.status(404).json({message: "Category not found"}) + + res.status(200).json(currentCategory); +} + +const updateCategory = (req, res) => { + const { id } = req.params; + const categoryIndex = categories.findIndex(el => el.id === id) + if (categoryIndex < 0) return res.status(404).json({message: "Category not found"}) + + const { name } = req.body; + if (!name) return res.status(401).json({ message: "Name is required!" }) + + categories[categoryIndex].name = name; + + res.status(203).json(categories[categoryIndex]); +} + +const deleteCategory = (req, res) => { + const { id } = req.params; + const categoryIndex = categories.findIndex(el => el.id === id) + if (categoryIndex < 0) return res.status(404).json({message: "Category not found"}) + + const currentCategory = categories[categoryIndex] + + categories.splice(categoryIndex, 1) + + res.status(200).json({status: "Item Deleted", item: currentCategory}); +} + + +const getAllCategories = (req, res) => { + res.status(200).json(categories); +} + +const createCategory = (req, res) => { + const { name } = req.body; + + if (!name) return res.status(401).json({ message: "Name is required!" }) + + const newCategory: Category = { + id: Date.now().toString(), + name + } + + categories.push(newCategory) + + res.status(201).json(newCategory) +} + +export default { + getCategory, + updateCategory, + deleteCategory, + getAllCategories, + createCategory, +} diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts new file mode 100644 index 00000000..c883316a --- /dev/null +++ b/apps/api/src/controllers/post.ts @@ -0,0 +1,143 @@ +import type { Post, Comment, PostResponse } from "../models/types" +import { categories } from "../controllers/category" + +const posts: Post[] = [] +const comments: Comment[] = [] + +const buildResponsePost = (post: Post): PostResponse => { + return { + ...post, + category: categories.filter(cat => post.category.includes(cat.id)), + comments: comments.filter(com => post.category.includes(com.id)) + } as PostResponse +} + +const findPost = (id: string): Post | void => { + return posts.find(el => el.id === id) +} + +const getPost = (req, res): void => { + const { id } = req.params; + const currentPost = findPost(id) + + if (!currentPost) return res.status(404).json({message: "Post not found"}) + + res.status(200).json(buildResponsePost(currentPost)); +} + +const updatePost = (req, res) => { + const { id } = req.params; + const postIndex = posts.findIndex(el => el.id === id) + if (postIndex < 0) return res.status(404).json({message: "Post not found"}) + + const post = req.body; + if (!post) return res.status(401).json({ message: "Name is required!" }) + + Object.keys(post).forEach(key => { + if (!posts[postIndex][key]) return + posts[postIndex][key] = post[key] + }) + + res.status(203).json(buildResponsePost(posts[postIndex])); +} + +const deletePost = (req, res) => { + const { id } = req.params; + const postIndex = categories.findIndex(el => el.id === id) + if (postIndex < 0) return res.status(404).json({message: "Post not found"}) + + const currentPost = buildResponsePost(posts[postIndex]) + + posts.splice(postIndex, 1) + + res.status(200).json({status: "Item Deleted", item: currentPost}); +} + +const createComment = (req, res) => { + const { id } = req.params; + const currentPost = findPost(id) + if (!currentPost) return res.status(404).json({message: "Post not found"}) + + const {comment} = req.body; + if (!comment.author || !comment.content) return res.status(401).json({ message: "Invalid Comment!" }) + + const commentId = Date.now().toString() + comments.push({ + id, + author: comment.author, + content: comment.content, + }) + + currentPost.comments.push(commentId) + + res.status(203).json(buildResponsePost(currentPost)); +} + + +const getAllPosts = (req, res) => { + res.status(200).json(posts.map(post => buildResponsePost(post))); +} + +const createPost = (req, res) => { + const { + title, + image, + description, + category, + comments, + } = req.body; + + if ( + !title + || !image + || !description + || !category + ) return res.status(401).json({ message: "Invalid Post!" }) + + const newComments: string[] = [] + + comments.forEach(({author, content}) => { + if (!author || !content) res.status(401).json({ message: "Invalid Comment!" }) + + const id = Date.now().toString() + + newComments.push(id) + + comments.push({ + id, + author, + content, + }) + }); + + const newPost: Post = { + ...req.body, + id: Date.now().toString(), + comments: newComments, + } + + posts.push(newPost) + + res.status(201).json(buildResponsePost(newPost)) +} + +const getAllPostsByCategory = (req, res): void => { + const { category } = req.params; + let categoryPosts = posts.filter(post => post.category.includes(category)) + + if (!categoryPosts.length) return res.status(404).json({message: "Category has no posts"}) + + categoryPosts = categoryPosts.map(post => buildResponsePost(post)) + + res.status(200).json(categoryPosts); +} + +export default { + getPost, + updatePost, + deletePost, + createComment, + getAllPosts, + createPost, + getAllPostsByCategory +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 7f6b1e10..3eec0782 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,21 +1,26 @@ -import cors from 'cors'; import express from 'express'; import helmet from 'helmet'; +import cors from 'cors'; import { corsOptions } from './config/corsConfig'; - import categories from './routes/categories'; +import posts from './routes/posts'; const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; const app = express(); -app.use(express.json()); -app.use(helmet()); -app.use(cors(corsOptions)); +app.use(express.json()) +app.use(helmet()) +app.use(cors(corsOptions)) -app.use('/api/categories', categories); +app.use('/api/post', posts) +app.use('/api/categories', categories) + +app.get('/', (req, res) => { + res.send({ message: 'Hello MFEE!' }); +}); app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); diff --git a/apps/api/src/models/types.ts b/apps/api/src/models/types.ts new file mode 100644 index 00000000..09904544 --- /dev/null +++ b/apps/api/src/models/types.ts @@ -0,0 +1,24 @@ +export type Category = { + id: string, + name: string; +} + +export type Comment = { + id: string + author: string + content: string +} + +export type Post = { + id: string + title: string + image: string + description: string + category: Category['id'][] + comments: Comment['id'][] +} + +export type PostResponse = Post & { + category: Category[] + comments: Comment[] +} diff --git a/apps/api/src/routes/.gitkeep b/apps/api/src/routes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/routes/categories.ts b/apps/api/src/routes/categories.ts index 78e120a1..1573de98 100644 --- a/apps/api/src/routes/categories.ts +++ b/apps/api/src/routes/categories.ts @@ -1,22 +1,16 @@ -import express from 'express'; - -import categoryController from '../controllers/category'; - -const router = express.Router(); - -// Get all categories -router.get('/', categoryController.getCategories); - -// Get category by id -router.get('/:id', categoryController.getCategoryById); - -// Create category -router.post('/', categoryController.createCategory); - -// Update category -router.patch('/:id', categoryController.updateCategory); - -// Delete category -router.delete('/:id', categoryController.deleteCategory); - -export default router; +import express from 'express'; +import categoryController from '../controllers/category'; + +const router = express.Router(); + +router.route('/:id') + .get(categoryController.getCategory) + .patch(categoryController.updateCategory) + .delete(categoryController.deleteCategory) + + +router.route('/') + .get(categoryController.getAllCategories) + .post(categoryController.createCategory) + +export default router diff --git a/apps/api/src/routes/posts.ts b/apps/api/src/routes/posts.ts new file mode 100644 index 00000000..313bd76f --- /dev/null +++ b/apps/api/src/routes/posts.ts @@ -0,0 +1,21 @@ +import express from 'express'; +import postController from '../controllers/post'; + +const router = express.Router(); + +router.route('/:id') + .get(postController.getPost) + .patch(postController.updatePost) + .delete(postController.deletePost) + +router.route("/:id/comments") + .post(postController.createComment) + +router.route('/') + .get(postController.getAllPosts) + .post(postController.createPost) + +router.route("/category/:category") + .get(postController.getAllPostsByCategory) + +export default router