From e32015b2dcddde3628cb17421768d99010a733b8 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Mon, 16 Sep 2024 15:32:53 -0700 Subject: [PATCH 01/14] updated readme --- apps/api/README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/api/README.md b/apps/api/README.md index 6066cc0e..658178b6 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -21,7 +21,52 @@ ## Challenges -### Session * +### 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 03 + +- N/A + +### Session 04 + +- N/A + +### Session 05 + +- Create MongoDB database +- Connect to MongoDB database using mongoose +- Create models for Post and Comment +- Refactor the controller to retrieve information from database + - *Tip: Use `populate` method to get data from reference id* +- **Extra** + - Remove post comments from database when you delete the post ## How to From b18ec9f0baa65b3dad1fdaeea51fb241e91a7669 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Mon, 16 Sep 2024 16:42:29 -0700 Subject: [PATCH 02/14] info comments --- apps/api/src/api.http | 1 + apps/api/src/main.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 apps/api/src/api.http diff --git a/apps/api/src/api.http b/apps/api/src/api.http new file mode 100644 index 00000000..095be522 --- /dev/null +++ b/apps/api/src/api.http @@ -0,0 +1 @@ +GET http://localhost:3000/ diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index bb421d83..093ce259 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -9,12 +9,12 @@ 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()); // middleware to parse json +app.use(helmet()); // middleware to secure the app / remove x-powered-by +app.use(cors(corsOptions)); // middleware to enable cors app.get('/', (req, res) => { - res.send({ message: 'Hello MFEE!' }); + res.send({ message: 'Hello MFEE! 22' }); }); app.listen(port, host, () => { From 80e439c2d4835f75dccba1ebd92aab2e0392aded Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Mon, 16 Sep 2024 22:06:12 -0700 Subject: [PATCH 03/14] posts api --- apps/api/src/api.http | 56 ++++++++++++- apps/api/src/config/corsConfig.ts | 4 +- apps/api/src/helpers/validators.ts | 25 ++++++ apps/api/src/main.ts | 9 +- apps/api/src/routes/categories.ts | 3 +- apps/api/src/routes/posts.ts | 128 +++++++++++++++++++++++++++++ apps/api/src/types/types.ts | 14 ++++ apps/api/tsconfig.json | 3 +- package-lock.json | 14 ++++ package.json | 1 + 10 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/helpers/validators.ts create mode 100644 apps/api/src/routes/posts.ts create mode 100644 apps/api/src/types/types.ts diff --git a/apps/api/src/api.http b/apps/api/src/api.http index 095be522..f34f287b 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -1 +1,55 @@ -GET http://localhost:3000/ +GET http://localhost:3000/api/categories + +### + +GET http://localhost:3000/api/note-valid-route + +### + +POST http://localhost:3000/api/categories +Content-Type: application/json + +{ + "name": "Other" +} + + +### + +GET http://localhost:3000/api/posts + +### + +POST http://localhost:3000/api/posts +Content-Type: application/json + +{ + "title": "Post Test Postman", + "image": "https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800", + "description": "Description from Postman", + "category": "1" +} + +### +POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments +Content-Type: application/json + +{ + "author": "MFEE", + "content": "Good content" +} + +### + +PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 +Content-Type: application/json + +{ + "title": "Test Postman New", + "description": "Description from Postman" +} + +### +DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 + +### \ No newline at end of file diff --git a/apps/api/src/config/corsConfig.ts b/apps/api/src/config/corsConfig.ts index 045cf517..b3b4edd9 100644 --- a/apps/api/src/config/corsConfig.ts +++ b/apps/api/src/config/corsConfig.ts @@ -1,9 +1,9 @@ const allowedOrigins = ['http://localhost:4200', 'http://localhost:3000']; export const corsOptions = { - origin: (origin, callback) => { + origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { // Note: origin will be undefined from same route in local development - if (allowedOrigins.indexOf(origin) !== -1 || !origin) { + if (!origin || allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); diff --git a/apps/api/src/helpers/validators.ts b/apps/api/src/helpers/validators.ts new file mode 100644 index 00000000..7b93b936 --- /dev/null +++ b/apps/api/src/helpers/validators.ts @@ -0,0 +1,25 @@ +import z from 'zod'; + +export const schema = z.object({ + title: z.string().min(1), + image: z.string().min(1).url(), + description: z.string().min(1), + category: z.string().min(1) +}); + +export const commentSchema = z.object({ + author: z.string().min(1), + content: z.string().min(1) +}); + +export const validatePost = (data: unknown) => { + return schema.safeParse(data); +}; + +export const validatePartialPost = (data: unknown) => { + return schema.partial().safeParse(data); +}; + +export const validateComment = (data: unknown) => { + return commentSchema.safeParse(data); +}; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index f2e728d3..5af0f367 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -5,6 +5,7 @@ import helmet from 'helmet'; 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; @@ -15,7 +16,13 @@ app.use(express.json()); // middleware to parse json app.use(helmet()); // middleware to secure the app / remove x-powered-by app.use(cors(corsOptions)); // middleware to enable cors -app.use('/api/categories', categories); +app.use('/api/categories', categories); // EXAMPLE +app.use('/api/posts', posts); + +// catch all non-existing routes +app.use((req, res) => { + res.status(404).json({ message: 'route not found' }); +}); app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); diff --git a/apps/api/src/routes/categories.ts b/apps/api/src/routes/categories.ts index 929c5552..da75acc6 100644 --- a/apps/api/src/routes/categories.ts +++ b/apps/api/src/routes/categories.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import express from 'express'; export const getCategory = (id: string) => { @@ -6,7 +7,7 @@ export const getCategory = (id: string) => { const router = express.Router(); // Initialize categories array to save data in memory -const categories = []; +const categories: any[] = []; // Get all categories router.get('/', (req, res) => { diff --git a/apps/api/src/routes/posts.ts b/apps/api/src/routes/posts.ts new file mode 100644 index 00000000..12b14cf6 --- /dev/null +++ b/apps/api/src/routes/posts.ts @@ -0,0 +1,128 @@ +import express from 'express'; +import Crypto from 'crypto'; +import { validateComment, validatePartialPost, validatePost } from '../helpers/validators'; +import { Post } from '../types/types'; + +const router = express.Router(); + +const POSTS: Post[] = [ + { + id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a9', + title: 'Post Test Postman', + image: + 'https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800', + description: 'Description from Postman', + category: '1', + comments: [ + { + id: Crypto.randomUUID(), + author: 'MFEE', + content: 'Good content' + } + ] + } +]; + +// 1. Get all posts +router.get('/', (req, res) => { + res.status(200).json(POSTS); +}); + +// 2. Get posts by category +router.get('/category/:category', (req, res) => { + const { category } = req.params; + const posts = POSTS.filter((p) => p.category === category); + if (!posts.length) { + return res.status(404).json({ message: `Posts not found for category ${category}` }); + } + res.status(200).json(posts); +}); + +// 3. Get post by id +router.get('/:id', (req, res) => { + const { id } = req.params; + const post = POSTS.find((p) => p.id === id); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + res.status(200).json(post); +}); + +// 4. Create post +router.post('/', (req, res) => { + const validationResult = validatePost(req.body); + if (validationResult.error) { + return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); + } + + // https://stackoverflow.com/questions/71185664/why-does-zod-make-all-my-schema-fields-optional + // Zod make all my schema fields optional + const newPost = { + id: Crypto.randomUUID(), + comments: [], + ...validationResult.data + }; + + POSTS.push(newPost); + + res.status(201).json(newPost); +}); + +// 5. Create post comment +router.post('/:id/comments', (req, res) => { + const validationResult = validateComment(req.body); + if (validationResult.error) { + return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); + } + + const { id } = req.params; + const postIndex = POSTS.findIndex((p) => p.id === id); + if (postIndex === -1) { + return res.status(404).json({ message: 'Cannot find post to add comment' }); + } + + const newComment = { + id: Crypto.randomUUID(), + ...validationResult.data + }; + + POSTS[postIndex].comments.push(newComment); + + res.status(201).json(newComment); +}); + +// 6. Update post + +router.patch('/:id', (req, res) => { + const validationResult = validatePartialPost(req.body); + if (validationResult.error) { + return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); + } + + const { id } = req.params; + const postIndex = POSTS.findIndex((p) => p.id === id); + if (postIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + const updatedPost = { ...POSTS[postIndex], ...validationResult.data }; + POSTS[postIndex] = updatedPost; + + res.status(200).json(updatedPost); +}); + +router.delete('/:id', (req, res) => { + const { id } = req.params; + const postIndex = POSTS.findIndex((p) => p.id === id); + if (postIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + POSTS.splice(postIndex, 1); + + res.status(204).send(); +}); + +export default router; diff --git a/apps/api/src/types/types.ts b/apps/api/src/types/types.ts new file mode 100644 index 00000000..784d28a5 --- /dev/null +++ b/apps/api/src/types/types.ts @@ -0,0 +1,14 @@ +export interface Post { + id: string; + title: string; + image: string; + description: string; + category: string; + comments: Comment[]; +} + +export interface Comment { + id: string; + author: string; + content: string; +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index c1e2dd4e..0e3d25aa 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -11,6 +11,7 @@ } ], "compilerOptions": { - "esModuleInterop": true + "esModuleInterop": true, + "strict": true } } diff --git a/package-lock.json b/package-lock.json index b27ba381..b4ac337f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "react-router-dom": "6.11.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "zod": "3.23.8", "zone.js": "~0.14.0" }, "devDependencies": { @@ -24106,6 +24107,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zone.js": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.2.tgz", @@ -41235,6 +41244,11 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" + }, "zone.js": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.2.tgz", diff --git a/package.json b/package.json index 221fc6d9..ae51fac2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-router-dom": "6.11.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "zod": "3.23.8", "zone.js": "~0.14.0" }, "devDependencies": { From 51b73fc370d2e5c28485d095a614876d91783a6a Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Mon, 16 Sep 2024 22:29:21 -0700 Subject: [PATCH 04/14] refactor to add post controller --- apps/api/src/api.http | 32 +++---- apps/api/src/controllers/category.ts | 15 +-- apps/api/src/controllers/post.ts | 135 +++++++++++++++++++++++++++ apps/api/src/routes/categories.ts | 2 - apps/api/src/routes/posts.ts | 119 ++--------------------- 5 files changed, 166 insertions(+), 137 deletions(-) create mode 100644 apps/api/src/controllers/post.ts diff --git a/apps/api/src/api.http b/apps/api/src/api.http index f34f287b..d8bbb9ba 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -1,28 +1,26 @@ -GET http://localhost:3000/api/categories - -### +### CATEGORIES -GET http://localhost:3000/api/note-valid-route +### GET categories +GET http://localhost:3000/api/categories -### +### GET not valid route +GET http://localhost:3000/api/not-valid-route +### POST category POST http://localhost:3000/api/categories Content-Type: application/json - { "name": "Other" } +### POSTS -### - +### GET posts GET http://localhost:3000/api/posts -### - +### POST post POST http://localhost:3000/api/posts Content-Type: application/json - { "title": "Post Test Postman", "image": "https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800", @@ -30,17 +28,15 @@ Content-Type: application/json "category": "1" } -### +### POST post comment POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments Content-Type: application/json - { "author": "MFEE", "content": "Good content" } -### - +### PATCH post PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 Content-Type: application/json @@ -49,7 +45,5 @@ Content-Type: application/json "description": "Description from Postman" } -### -DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 - -### \ No newline at end of file +### DELETE post +DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 \ No newline at end of file diff --git a/apps/api/src/controllers/category.ts b/apps/api/src/controllers/category.ts index 59294b96..3f1efe3a 100644 --- a/apps/api/src/controllers/category.ts +++ b/apps/api/src/controllers/category.ts @@ -1,18 +1,21 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Request, Response } from 'express'; + // Initialize categories array to save data in memory -const categories = []; +const categories: any[] = []; export const getCategory = (id: string) => { return categories.find((p) => p.id === id); }; // Get all categories -const getCategories = (req, res) => { +const getCategories = (req: Request, res: Response) => { // Return all the categories with a 200 status code res.status(200).json(categories); }; // Get category by id -const getCategoryById = (req, res) => { +const getCategoryById = (req: Request, res: Response) => { // Retrieve the id from the route params const { id } = req.params; // Check if we have a category with that id @@ -30,7 +33,7 @@ const getCategoryById = (req, res) => { }; // Create category -const createCategory = (req, res) => { +const createCategory = (req: Request, res: Response) => { // Retrieve the name from the request body const { name } = req.body; @@ -52,7 +55,7 @@ const createCategory = (req, res) => { }; // Update category -const updateCategory = (req, res) => { +const updateCategory = (req: Request, res: Response) => { // Retrieve the id from the route params const { id } = req.params; // Retrieve the index of the category in the array @@ -82,7 +85,7 @@ const updateCategory = (req, res) => { }; // Delete category -const deleteCategory = (req, res) => { +const deleteCategory = (req: Request, res: Response) => { // Retrieve the id from the route params const { id } = req.params; // Retrieve the index of the category in the array diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts new file mode 100644 index 00000000..2a2f27ec --- /dev/null +++ b/apps/api/src/controllers/post.ts @@ -0,0 +1,135 @@ +import { Request, Response } from 'express'; +import { Post } from '../types/types'; +import Crypto from 'crypto'; +import { validateComment, validatePartialPost, validatePost } from '../helpers/validators'; + +const POSTS: Post[] = [ + //Example post + { + id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a9', + title: 'Post Test Postman', + image: + 'https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800', + description: 'Description from Postman', + category: '1', + comments: [ + { + id: Crypto.randomUUID(), + author: 'MFEE', + content: 'Good content' + } + ] + } +]; + +// 1. Get all posts +const getPosts = (req: Request, res: Response) => { + res.status(200).json(POSTS); +}; + +// 2. Get posts by category +const getPostsByCategory = (req: Request, res: Response) => { + const { category } = req.params; + const posts = POSTS.filter((p) => p.category === category); + if (!posts.length) { + return res.status(404).json({ message: `Posts not found for category ${category}` }); + } + res.status(200).json(posts); +}; + +// 3. Get post by id +const getPostById = (req: Request, res: Response) => { + const { id } = req.params; + const post = POSTS.find((p) => p.id === id); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + res.status(200).json(post); +}; + +// 4. Create post +const createPost = (req: Request, res: Response) => { + const validationResult = validatePost(req.body); + if (validationResult.error) { + return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); + } + + // https://stackoverflow.com/questions/71185664/why-does-zod-make-all-my-schema-fields-optional + // Zod make all my schema fields optional + const newPost = { + id: Crypto.randomUUID(), + comments: [], + ...validationResult.data + }; + + POSTS.push(newPost); + + res.status(201).json(newPost); +}; + +// 5. Create post comment +const createPostComment = (req: Request, res: Response) => { + const validationResult = validateComment(req.body); + if (validationResult.error) { + return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); + } + + const { id } = req.params; + const postIndex = POSTS.findIndex((p) => p.id === id); + if (postIndex === -1) { + return res.status(404).json({ message: 'Cannot find post to add comment' }); + } + + const newComment = { + id: Crypto.randomUUID(), + ...validationResult.data + }; + + POSTS[postIndex].comments.push(newComment); + + res.status(201).json(newComment); +}; + +// 6. Update post +const updatePost = (req: Request, res: Response) => { + const validationResult = validatePartialPost(req.body); + if (validationResult.error) { + return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); + } + + const { id } = req.params; + const postIndex = POSTS.findIndex((p) => p.id === id); + if (postIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + const updatedPost = { ...POSTS[postIndex], ...validationResult.data }; + POSTS[postIndex] = updatedPost; + + res.status(200).json(updatedPost); +}; + +// 7. Delete post +const deletePost = (req: Request, res: Response) => { + const { id } = req.params; + const postIndex = POSTS.findIndex((p) => p.id === id); + if (postIndex === -1) { + return res.status(404).json({ message: 'Post not found' }); + } + + POSTS.splice(postIndex, 1); + + res.status(204).send(); +}; + +export default { + getPosts, + getPostsByCategory, + getPostById, + createPost, + createPostComment, + updatePost, + deletePost +}; diff --git a/apps/api/src/routes/categories.ts b/apps/api/src/routes/categories.ts index e3bc01ed..d245a489 100644 --- a/apps/api/src/routes/categories.ts +++ b/apps/api/src/routes/categories.ts @@ -4,8 +4,6 @@ import express from 'express'; import categoryController from '../controllers/category'; const router = express.Router(); -// Initialize categories array to save data in memory -const categories: any[] = []; // Get all categories router.get('/', categoryController.getCategories); diff --git a/apps/api/src/routes/posts.ts b/apps/api/src/routes/posts.ts index 12b14cf6..dd480da3 100644 --- a/apps/api/src/routes/posts.ts +++ b/apps/api/src/routes/posts.ts @@ -1,128 +1,27 @@ import express from 'express'; -import Crypto from 'crypto'; -import { validateComment, validatePartialPost, validatePost } from '../helpers/validators'; -import { Post } from '../types/types'; +import postController from '../controllers/post'; const router = express.Router(); -const POSTS: Post[] = [ - { - id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a9', - title: 'Post Test Postman', - image: - 'https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800', - description: 'Description from Postman', - category: '1', - comments: [ - { - id: Crypto.randomUUID(), - author: 'MFEE', - content: 'Good content' - } - ] - } -]; - // 1. Get all posts -router.get('/', (req, res) => { - res.status(200).json(POSTS); -}); +router.get('/', postController.getPosts); // 2. Get posts by category -router.get('/category/:category', (req, res) => { - const { category } = req.params; - const posts = POSTS.filter((p) => p.category === category); - if (!posts.length) { - return res.status(404).json({ message: `Posts not found for category ${category}` }); - } - res.status(200).json(posts); -}); +router.get('/category/:category', postController.getPostsByCategory); // 3. Get post by id -router.get('/:id', (req, res) => { - const { id } = req.params; - const post = POSTS.find((p) => p.id === id); - - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - - res.status(200).json(post); -}); +router.get('/:id', postController.getPostById); // 4. Create post -router.post('/', (req, res) => { - const validationResult = validatePost(req.body); - if (validationResult.error) { - return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); - } - - // https://stackoverflow.com/questions/71185664/why-does-zod-make-all-my-schema-fields-optional - // Zod make all my schema fields optional - const newPost = { - id: Crypto.randomUUID(), - comments: [], - ...validationResult.data - }; - - POSTS.push(newPost); - - res.status(201).json(newPost); -}); +router.post('/', postController.createPost); // 5. Create post comment -router.post('/:id/comments', (req, res) => { - const validationResult = validateComment(req.body); - if (validationResult.error) { - return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); - } - - const { id } = req.params; - const postIndex = POSTS.findIndex((p) => p.id === id); - if (postIndex === -1) { - return res.status(404).json({ message: 'Cannot find post to add comment' }); - } - - const newComment = { - id: Crypto.randomUUID(), - ...validationResult.data - }; - - POSTS[postIndex].comments.push(newComment); - - res.status(201).json(newComment); -}); +router.post('/:id/comments', postController.createPostComment); // 6. Update post +router.patch('/:id', postController.updatePost); -router.patch('/:id', (req, res) => { - const validationResult = validatePartialPost(req.body); - if (validationResult.error) { - return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); - } - - const { id } = req.params; - const postIndex = POSTS.findIndex((p) => p.id === id); - if (postIndex === -1) { - return res.status(404).json({ message: 'Post not found' }); - } - - const updatedPost = { ...POSTS[postIndex], ...validationResult.data }; - POSTS[postIndex] = updatedPost; - - res.status(200).json(updatedPost); -}); - -router.delete('/:id', (req, res) => { - const { id } = req.params; - const postIndex = POSTS.findIndex((p) => p.id === id); - if (postIndex === -1) { - return res.status(404).json({ message: 'Post not found' }); - } - - POSTS.splice(postIndex, 1); - - res.status(204).send(); -}); +// 7. Delete post +router.delete('/:id', postController.deletePost); export default router; From 2bfdee9dd7aae0332df661d2efa49632fcd940b1 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Mon, 16 Sep 2024 23:04:12 -0700 Subject: [PATCH 05/14] fixed auth types --- .gitignore | 2 ++ apps/api/src/api.http | 38 +++++++++++++++++++++++++++++++- apps/api/src/controllers/auth.ts | 24 +++++++++++--------- apps/api/src/main.ts | 3 +++ apps/api/src/middleware/auth.ts | 10 +++++---- package-lock.json | 38 ++++++++++++++++++++++++++++++++ package.json | 2 ++ 7 files changed, 101 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index f9418f1b..50bc8c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ Thumbs.db .nx/cache .angular + +.env.local diff --git a/apps/api/src/api.http b/apps/api/src/api.http index d8bbb9ba..da553990 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -46,4 +46,40 @@ Content-Type: application/json } ### DELETE post -DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 \ No newline at end of file +DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 + + +// AUTH + +### REGISTER +POST http://localhost:3000/api/auth/register +Content-Type: application/json + +{ + "username": "mfee-test", + "password": "Aa$123" +} + +### LOGIN +POST http://localhost:3000/api/auth/login +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU1Mjc2MiwiZXhwIjoxNzI2NTUzNjYyfQ.8mALljoCdTaU0JHZhXolfUc4fLkbjAyBYpfP7Wt2HfE + + +{ + "username": "mfee-test", + "password": "Aa$123" +} + +### REFRESH +POST http://localhost:3000/api/auth/refresh +Content-Type: application/json + +{ + "username": "mfee-test", + "password": "Aa$123" +} + +### LOGOUT +POST http://localhost:3000/api/auth/logout +Content-Type: application/json \ No newline at end of file diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index 96bb74b5..5087dfbf 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -1,11 +1,12 @@ import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; +import jwt, { Secret } from 'jsonwebtoken'; +import { Request, Response } from 'express'; import { User } from '../models/user'; const users: User[] = []; -const register = async (req, res) => { +const register = async (req: Request, res: Response) => { const { username, password } = req.body; // Check that we have the correct payload @@ -29,12 +30,12 @@ const register = async (req, res) => { users.push({ username, password: hashedPassword }); res.status(201).json({ message: 'User registered successfully' }); - } catch (e) { - res.status(500).json({ message: e.message }); + } catch (error) { + res.status(500).json({ message: (error as Error).message }); } }; -const login = async (req, res) => { +const login = async (req: Request, res: Response) => { const { username, password } = req.body; // Check that we have the correct payload @@ -53,8 +54,8 @@ const login = async (req, res) => { } // Generate access token and refresh token - const accessToken = jwt.sign({ username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); - const refreshToken = jwt.sign({ username }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' }); + const accessToken = jwt.sign({ username }, process.env.ACCESS_TOKEN_SECRET as Secret, { expiresIn: '15m' }); + const refreshToken = jwt.sign({ username }, process.env.REFRESH_TOKEN_SECRET as Secret, { expiresIn: '7d' }); // Save refresh token res.cookie('refreshToken', refreshToken, { @@ -65,7 +66,7 @@ const login = async (req, res) => { res.json({ accessToken }); }; -const refresh = (req, res) => { +const refresh = (req: Request, res: Response) => { // Get refresh token from cookies const refreshToken = req.cookies.refreshToken; @@ -73,18 +74,19 @@ const refresh = (req, res) => { return res.status(401).json({ message: 'Unauthorized' }); } - jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, { username }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET as Secret, (err: any, { username }: any) => { if (err) { // Invalid token return res.status(403).json({ message: 'Forbidden' }); } - const accessToken = jwt.sign({ username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); + const accessToken = jwt.sign({ username }, process.env.ACCESS_TOKEN_SECRET as Secret, { expiresIn: '15m' }); res.json({ accessToken }); }); }; -const logout = (req, res) => { +const logout = (req: Request, res: Response) => { res.clearCookie('refreshToken'); res.json({ message: 'Logged out successfully' }); }; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 23da43aa..75ff974c 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -7,6 +7,9 @@ import { verifyToken } from './middleware/auth'; import auth from './routes/auth'; import categories from './routes/categories'; import posts from './routes/posts'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '.env.local' }); // Load environment variables from .env.local file so that we don't have them in the repo const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index 193a5a9e..3596a97f 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -1,6 +1,7 @@ -import jwt from 'jsonwebtoken'; +import jwt, { Secret } from 'jsonwebtoken'; +import { NextFunction, Request, Response } from 'express'; -export const verifyToken = (req, res, next) => { +export const verifyToken = (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers['authorization']; if (!authHeader) { @@ -8,13 +9,14 @@ export const verifyToken = (req, res, next) => { } const token = authHeader.split(' ')[1]; - jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { + jwt.verify(token, process.env.ACCESS_TOKEN_SECRET as Secret, (err, user) => { if (err) { // Invalid token return res.status(403).json({ message: 'Forbidden' }); } - req.user = user; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (req as any).user = user; next(); }); }; diff --git a/package-lock.json b/package-lock.json index b4ac337f..771e51d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,8 +59,10 @@ "@swc/cli": "~0.1.62", "@swc/core": "~1.3.85", "@testing-library/react": "14.0.0", + "@types/bcrypt": "^5.0.2", "@types/express": "~4.17.13", "@types/jest": "^29.4.0", + "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.97", "@types/node": "18.16.9", "@types/react": "18.2.33", @@ -8003,6 +8005,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -8232,6 +8243,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -29318,6 +29338,15 @@ "@babel/types": "^7.20.7" } }, + "@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -29546,6 +29575,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", diff --git a/package.json b/package.json index ae51fac2..4caa6403 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,10 @@ "@swc/cli": "~0.1.62", "@swc/core": "~1.3.85", "@testing-library/react": "14.0.0", + "@types/bcrypt": "^5.0.2", "@types/express": "~4.17.13", "@types/jest": "^29.4.0", + "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.97", "@types/node": "18.16.9", "@types/react": "18.2.33", From d74b30f658e66be0f61743bbbfdb484612c3971c Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Mon, 16 Sep 2024 23:07:58 -0700 Subject: [PATCH 06/14] fixed error handler types --- apps/api/src/middleware/errorHandler.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/src/middleware/errorHandler.ts b/apps/api/src/middleware/errorHandler.ts index 90969d56..458d32d6 100644 --- a/apps/api/src/middleware/errorHandler.ts +++ b/apps/api/src/middleware/errorHandler.ts @@ -1,5 +1,7 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const errorHandler = (err, req, res, next) => { +import { Request, Response } from 'express'; + +export const errorHandler = (err: Error, req: Request, res: Response) => { // Log this for debug purposes console.error(err.stack); // Return custom error to user From b71914d8d6657b40891592abfd6d3552fcd170f6 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 03:52:19 -0700 Subject: [PATCH 07/14] added error handling --- apps/api/README.md | 22 ------ apps/api/src/api.http | 36 ++++----- apps/api/src/controllers/post.ts | 78 ++++++++++++++----- .../api/src/helpers/controllerErrorHandler.ts | 9 +++ apps/api/src/main.ts | 2 - apps/api/src/models/posts.ts | 13 ++-- 6 files changed, 93 insertions(+), 67 deletions(-) create mode 100644 apps/api/src/helpers/controllerErrorHandler.ts diff --git a/apps/api/README.md b/apps/api/README.md index 4d1e9684..308984a2 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -71,28 +71,6 @@ - **Extra** - Remove post comments from database when you delete the post -### Session 02 - -- Refactor the code from last session to add a post controller - -### Session 03 - -- N/A - -### Session 04 - -- N/A - -### Session 05 - -- Create MongoDB database -- Connect to MongoDB database using mongoose -- Create models for Post and Comment -- Refactor the controller to retrieve information from database - - *Tip: Use `populate` method to get data from reference id* -- **Extra** - - Remove post comments from database when you delete the post - ## How to ### Run postman collection diff --git a/apps/api/src/api.http b/apps/api/src/api.http index 28c28bb3..51719e70 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -1,42 +1,42 @@ -// ////////// // -// CATEGORIES // -// ////////// // +// //////////////////// // +// CATEGORIES // +// //////////////////// // ### GET CATEGORIES GET http://localhost:3000/api/categories -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw ### GET NOT VALID ROUTE GET http://localhost:3000/api/not-valid-route ### GET CATEGORIES BY ID POST http://localhost:3000/api/categories/1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw Content-Type: application/json { "name": "Other" } -// ///// // -// POSTS // -// ///// // +// //////////////////// // +// POSTS // +// //////////////////// // ### 1. GET ALL POSTS GET http://localhost:3000/api/posts -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw ### 2. GET POSTS BY CATEGORY GET http://localhost:3000/api/posts/category/1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw ### 3. GET POST BY ID GET http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw ### 4. CREATE POST POST http://localhost:3000/api/posts -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw Content-Type: application/json { @@ -48,7 +48,7 @@ Content-Type: application/json ### 5. CREATE POST COMMENT POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw Content-Type: application/json { @@ -58,7 +58,7 @@ Content-Type: application/json ### 6. UPDATE POST PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw Content-Type: application/json { @@ -68,11 +68,11 @@ Content-Type: application/json ### 7. DELETE POST DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU2NjgzOCwiZXhwIjoxNzI2NTY3NzM4fQ.UFDSTtgW0Xfb08SC4We2jihcCfq7eKTgzgcOsg4vVvs +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw -// //// // -// AUTH // -// //// // +// //////////////////// // +// AUTH // +// //////////////////// // ### REGISTER POST http://localhost:3000/api/auth/register diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts index 8db7c25a..9c413a8d 100644 --- a/apps/api/src/controllers/post.ts +++ b/apps/api/src/controllers/post.ts @@ -4,31 +4,45 @@ import postModel from '../models/posts'; // 1. Get all posts const getPosts = async (req: Request, res: Response) => { - const posts = await postModel.getAllPosts(); - console.log(posts); - res.status(200).json(posts); + try { + const posts = await postModel.getAllPosts(); + res.status(200).json(posts); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); + } }; // 2. Get posts by category const getPostsByCategory = async (req: Request, res: Response) => { const { category } = req.params; - const posts = await postModel.getPostsByCategory(category); - if (!posts.length) { - return res.status(404).json({ message: `Posts not found for category ${category}` }); + try { + const posts = await postModel.getPostsByCategory(category); + if (!posts.length) { + return res.status(404).json({ message: `Posts not found for category ${category}` }); + } + res.status(200).json(posts); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); } - res.status(200).json(posts); }; // 3. Get post by id const getPostById = async (req: Request, res: Response) => { const { id } = req.params; - const post = await postModel.getPostById(id); - if (!post) { - return res.status(404).json({ message: 'Post not found' }); + try { + const post = await postModel.getPostById(id); + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + res.status(200).json(post); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); } - res.status(200).json(post); }; // 4. Create post @@ -37,8 +51,14 @@ const createPost = async (req: Request, res: Response) => { if (validationResult.error) { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } - const newPost = await postModel.createPost(validationResult.data); - res.status(201).json(newPost); + + try { + const newPost = await postModel.createPost(validationResult.data); + res.status(201).json(newPost); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); + } }; // 5. Create post comment @@ -48,8 +68,14 @@ const createPostComment = async (req: Request, res: Response) => { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } const { id } = req.params; - const newComment = await postModel.createPostComment(id, validationResult.data); - res.status(201).json(newComment); + + try { + const newComment = await postModel.createPostComment(id, validationResult.data); + res.status(201).json(newComment); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); + } }; // 6. Update post @@ -59,16 +85,28 @@ const updatePost = async (req: Request, res: Response) => { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } const { id } = req.params; - const updatedPost = await postModel.updatePost(id, validationResult.data); - res.status(200).json(updatedPost); + + try { + const updatedPost = await postModel.updatePost(id, validationResult.data); + res.status(200).json(updatedPost); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); + } }; // 7. Delete post const deletePost = async (req: Request, res: Response) => { const { id } = req.params; - const deletedPost = postModel.deletePost(id); - console.log(deletedPost); - res.status(204).send({ message: `Post deleted successfully` }); + + try { + const deletedPost = await postModel.deletePost(id); + console.log(deletedPost); + res.status(204).send({ message: `Post deleted successfully` }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + res.status(500).json({ message: errorMessage }); + } }; export default { diff --git a/apps/api/src/helpers/controllerErrorHandler.ts b/apps/api/src/helpers/controllerErrorHandler.ts new file mode 100644 index 00000000..56481285 --- /dev/null +++ b/apps/api/src/helpers/controllerErrorHandler.ts @@ -0,0 +1,9 @@ +import { Request, Response, NextFunction } from 'express'; + +// Error handler wrapper for async functions +const asyncHandler = + (fn: (req: Request, res: Response, next: NextFunction) => Promise) => (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); // Catch errors and pass them to next() + }; + +export default asyncHandler; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 0d8d2202..174f0ac2 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -2,7 +2,6 @@ import cors from 'cors'; import express from 'express'; import helmet from 'helmet'; import mongoose from 'mongoose'; - import { corsOptions } from './config/corsConfig'; import { verifyToken } from './middleware/auth'; import { errorHandler } from './middleware/errorHandler'; @@ -27,7 +26,6 @@ app.use('/api/auth', auth); app.use(verifyToken); app.use('/api/categories', categories); // EXAMPLE app.use('/api/posts', posts); - // catch all non-existing routes app.use((req, res) => { res.status(404).json({ message: 'route not found' }); diff --git a/apps/api/src/models/posts.ts b/apps/api/src/models/posts.ts index ba3b21be..58d8c6aa 100644 --- a/apps/api/src/models/posts.ts +++ b/apps/api/src/models/posts.ts @@ -5,6 +5,7 @@ import Category from './category'; import CommentSchema from '../schemas/comment'; import { CATEGORIES, COMMENTS, POSTS } from '../data/initialData'; +//Initialize the database records so we can test const initMongoRecords = async () => { await PostSchema.deleteMany({}); await Category.deleteMany({}); @@ -53,6 +54,11 @@ const createPostComment = async (postId: string, comment: { author: string; cont ...comment }; + const foundPost = await PostSchema.findOne({ id: postId }); + if (!foundPost) { + throw new Error('Post not found'); + } + await CommentSchema.create(newComment); await PostSchema.updateOne({ id: postId }, { $push: { comments: newComment.id } }); return newComment; @@ -66,11 +72,8 @@ const updatePost = async (postId: string, data: Partial) => { // 7. Delete post const deletePost = async (postId: string) => { - const deletedPost = await PostSchema.findOneAndUpdate({ id: postId }, { deleted: true }); - const deletedComment = await CommentSchema.findOneAndUpdate({ id: postId }, { deleted: true }); - console.log(deletedComment); - console.log(deletedPost); - + const deletedPost = await PostSchema.findOneAndDelete({ id: postId }); + const deletedComment = await CommentSchema.findOneAndDelete({ id: postId }); return { deletedComments: deletedComment, deletedPost: deletedPost }; }; From b374715557879a172e501eaca46cc3e97513547f Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 05:01:34 -0700 Subject: [PATCH 08/14] code cleanup --- apps/api/src/api.http | 18 +++++++++--------- apps/api/src/data/initialData.ts | 9 ++++----- apps/api/src/models/category.ts | 5 +++++ apps/api/src/models/posts.ts | 21 ++++++++++----------- apps/api/src/schemas/comment.ts | 18 +++++++++--------- apps/api/src/schemas/post.ts | 22 ++++++++++++---------- apps/api/src/types/types.ts | 4 ++-- 7 files changed, 51 insertions(+), 46 deletions(-) diff --git a/apps/api/src/api.http b/apps/api/src/api.http index 51719e70..de1985bf 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -4,14 +4,14 @@ ### GET CATEGORIES GET http://localhost:3000/api/categories -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] ### GET NOT VALID ROUTE GET http://localhost:3000/api/not-valid-route ### GET CATEGORIES BY ID POST http://localhost:3000/api/categories/1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] Content-Type: application/json { @@ -24,19 +24,19 @@ Content-Type: application/json ### 1. GET ALL POSTS GET http://localhost:3000/api/posts -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] ### 2. GET POSTS BY CATEGORY GET http://localhost:3000/api/posts/category/1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] ### 3. GET POST BY ID GET http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] ### 4. CREATE POST POST http://localhost:3000/api/posts -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] Content-Type: application/json { @@ -48,7 +48,7 @@ Content-Type: application/json ### 5. CREATE POST COMMENT POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] Content-Type: application/json { @@ -58,7 +58,7 @@ Content-Type: application/json ### 6. UPDATE POST PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] Content-Type: application/json { @@ -68,7 +68,7 @@ Content-Type: application/json ### 7. DELETE POST DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU3MDAxOSwiZXhwIjoxNzI2NTcwOTE5fQ.sErMcseUw21l8cSZybbhz2CANe22vf3a11U7tjeSwrw +Authorization: Bearer [access_token] // //////////////////// // // AUTH // diff --git a/apps/api/src/data/initialData.ts b/apps/api/src/data/initialData.ts index c18bf1b6..90e5853c 100644 --- a/apps/api/src/data/initialData.ts +++ b/apps/api/src/data/initialData.ts @@ -1,9 +1,8 @@ import { Comment, Post } from '../types/types'; const POSTS: Post[] = [ - //Example post { - id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a9', + _id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a9', title: 'Post Test Postman', image: 'https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800', @@ -15,7 +14,7 @@ const POSTS: Post[] = [ const COMMENTS: Comment[] = [ { - id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a1', + _id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a1', author: 'MFEE', content: 'Good content' } @@ -23,11 +22,11 @@ const COMMENTS: Comment[] = [ const CATEGORIES = [ { - id: '1', + _id: '1', name: 'Other' }, { - id: '2', + _id: '2', name: 'React' } ]; diff --git a/apps/api/src/models/category.ts b/apps/api/src/models/category.ts index c69bbc87..eb552bd6 100644 --- a/apps/api/src/models/category.ts +++ b/apps/api/src/models/category.ts @@ -2,10 +2,15 @@ import mongoose, { Document, Schema } from 'mongoose'; interface ICategory extends Document { name: string; + _id: string; } export const categorySchema = new Schema( { + _id: { + type: String, + required: [true, 'id is required'] + }, name: { type: String, required: [true, 'Property is required'] diff --git a/apps/api/src/models/posts.ts b/apps/api/src/models/posts.ts index 58d8c6aa..3981c71b 100644 --- a/apps/api/src/models/posts.ts +++ b/apps/api/src/models/posts.ts @@ -19,18 +19,18 @@ initMongoRecords(); // 1. Get all posts const getAllPosts = async () => { - return await PostSchema.find({}); + return await PostSchema.find({}).populate(['comments', 'category']).sort({ createdAt: -1 }); }; // 2. Get posts by category const getPostsByCategory = async (category: string) => { - const postsByCategory = await PostSchema.find({ category: category }).populate(['comments', 'category']); + const postsByCategory = await PostSchema.find({ category: category }).populate(['comments', 'category']).sort({ createdAt: -1 }); return postsByCategory; }; // 3. Get post by id const getPostById = async (id: string) => { - const postById = await PostSchema.find({ id: id }).populate(['comments', 'category']); + const postById = await PostSchema.find({ _id: id }).populate(['comments', 'category']); return postById; }; @@ -39,7 +39,7 @@ const createPost = async (data: CuratedPost) => { // https://stackoverflow.com/questions/71185664/why-does-zod-make-all-my-schema-fields-optional // Zod make all my schema fields optional const newPost = { - id: Crypto.randomUUID(), + _id: Crypto.randomUUID(), comments: [], ...data }; @@ -50,30 +50,29 @@ const createPost = async (data: CuratedPost) => { // 5. Create post comment const createPostComment = async (postId: string, comment: { author: string; content: string }) => { const newComment = { - id: Crypto.randomUUID(), + _id: Crypto.randomUUID(), ...comment }; - const foundPost = await PostSchema.findOne({ id: postId }); + const foundPost = await PostSchema.findOne({ _id: postId }); if (!foundPost) { throw new Error('Post not found'); } - await CommentSchema.create(newComment); - await PostSchema.updateOne({ id: postId }, { $push: { comments: newComment.id } }); + await PostSchema.updateOne({ _id: postId }, { $push: { comments: newComment._id } }); return newComment; }; // 6. Update post const updatePost = async (postId: string, data: Partial) => { - const updatedPost = await PostSchema.findOneAndUpdate({ id: postId }, data, { new: true }); + const updatedPost = await PostSchema.findOneAndUpdate({ _id: postId }, data, { new: true }); return updatedPost; }; // 7. Delete post const deletePost = async (postId: string) => { - const deletedPost = await PostSchema.findOneAndDelete({ id: postId }); - const deletedComment = await CommentSchema.findOneAndDelete({ id: postId }); + const deletedPost = await PostSchema.findOneAndDelete({ _id: postId }); + const deletedComment = await CommentSchema.findOneAndDelete({ _id: postId }); return { deletedComments: deletedComment, deletedPost: deletedPost }; }; diff --git a/apps/api/src/schemas/comment.ts b/apps/api/src/schemas/comment.ts index c3eeefbc..761f360f 100644 --- a/apps/api/src/schemas/comment.ts +++ b/apps/api/src/schemas/comment.ts @@ -1,24 +1,24 @@ -import mongoose from 'mongoose'; +import mongoose, { Document } from 'mongoose'; -interface IComment extends mongoose.Document { - id: string; +interface IComment extends Document { + _id: string; author: string; content: string; } const commentSchema = new mongoose.Schema( { - id: { + _id: { type: String, - required: [true, 'Property is required'] + required: [true, 'id is required'] }, author: { type: String, - required: [true, 'Property is required'] + required: [true, 'Author is required'] }, content: { type: String, - required: [true, 'Property is required'] + required: [true, 'Content is required'] } }, { @@ -26,6 +26,6 @@ const commentSchema = new mongoose.Schema( } ); -const CommentSchema = mongoose.model('Comment', commentSchema); +const Comment = mongoose.model('Comment', commentSchema); -export default CommentSchema; +export default Comment; diff --git a/apps/api/src/schemas/post.ts b/apps/api/src/schemas/post.ts index 0cc07bee..cdf78398 100644 --- a/apps/api/src/schemas/post.ts +++ b/apps/api/src/schemas/post.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; interface IPost extends mongoose.Document { - id: string; + _id: string; title: string; image: string; description: string; @@ -11,29 +11,31 @@ interface IPost extends mongoose.Document { const postSchema = new mongoose.Schema( { - id: { + _id: { type: String, - required: [true, 'Property is required'] + required: [true, 'id is required'] }, title: { type: String, - required: [true, 'Property is required'] + required: [true, 'title is required'] }, image: { type: String, - required: [true, 'Property is required'] + required: [true, 'image is required'] }, description: { type: String, - required: [true, 'Property is required'] + required: [true, 'description is required'] }, category: { - type: String, - required: [true, 'Property is required'] + type: mongoose.Schema.Types.String, // Foreign key + ref: 'Category', + required: [true, 'category is required'] }, comments: { - type: [String], - required: [true, 'Property is required'] + type: [mongoose.Schema.Types.String], // Foreign key + ref: 'Comment', + required: [true, 'comments is required'] } }, { diff --git a/apps/api/src/types/types.ts b/apps/api/src/types/types.ts index 851b4a49..cc81942e 100644 --- a/apps/api/src/types/types.ts +++ b/apps/api/src/types/types.ts @@ -1,5 +1,5 @@ export interface Post { - id: string; + _id: string; title: string; image: string; description: string; @@ -15,7 +15,7 @@ export interface CuratedPost { } export interface Comment { - id: string; + _id: string; author: string; content: string; } From a0edd2920280b37f8a5816d64fdb96887c0367c6 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 05:14:36 -0700 Subject: [PATCH 09/14] .env path checker --- apps/api/src/helpers/dotenvChecker.ts | 18 ++++++++++++++++++ apps/api/src/main.ts | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/helpers/dotenvChecker.ts diff --git a/apps/api/src/helpers/dotenvChecker.ts b/apps/api/src/helpers/dotenvChecker.ts new file mode 100644 index 00000000..0ebbd9cb --- /dev/null +++ b/apps/api/src/helpers/dotenvChecker.ts @@ -0,0 +1,18 @@ +import dotenv from 'dotenv'; +import fs from 'fs'; +import path from 'path'; + +export const envChecker = () => { + const local = path.resolve('apps/api/.env.local'); + const fallbackEnvPath = path.resolve('apps/api/.env'); + + if (fs.existsSync(local)) { + dotenv.config({ path: local }); + console.log(`Loaded environment variables from ${local}`); + } else if (fs.existsSync(fallbackEnvPath)) { + dotenv.config({ path: fallbackEnvPath }); + console.log(`Loaded environment variables from ${fallbackEnvPath}`); + } else { + console.log('No environment files found.'); + } +}; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 174f0ac2..342722b3 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -8,9 +8,10 @@ import { errorHandler } from './middleware/errorHandler'; import auth from './routes/auth'; import categories from './routes/categories'; import posts from './routes/posts'; -import dotenv from 'dotenv'; +import { envChecker } from './helpers/dotenvChecker'; -dotenv.config({ path: '.env.local' }); // Load environment variables from .env.local file so that we don't have them in the repo +// Load environment variables from .env.local file so that we don't have them in the repo +envChecker(); const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 3000; From 9337ef39d6852538be25880c5f46c1e9ce68f06f Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 05:19:11 -0700 Subject: [PATCH 10/14] fix --- apps/api/src/api.http | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/api/src/api.http b/apps/api/src/api.http index de1985bf..148f6b68 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -4,14 +4,14 @@ ### GET CATEGORIES GET http://localhost:3000/api/categories -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] ### GET NOT VALID ROUTE GET http://localhost:3000/api/not-valid-route ### GET CATEGORIES BY ID -POST http://localhost:3000/api/categories/1 -Authorization: Bearer [access_token] +PATCH http://localhost:3000/api/categories/1 +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -24,19 +24,19 @@ Content-Type: application/json ### 1. GET ALL POSTS GET http://localhost:3000/api/posts -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] ### 2. GET POSTS BY CATEGORY GET http://localhost:3000/api/posts/category/1 -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] ### 3. GET POST BY ID GET http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] ### 4. CREATE POST POST http://localhost:3000/api/posts -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -48,7 +48,7 @@ Content-Type: application/json ### 5. CREATE POST COMMENT POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -58,7 +58,7 @@ Content-Type: application/json ### 6. UPDATE POST PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -68,7 +68,7 @@ Content-Type: application/json ### 7. DELETE POST DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer [access_token] +Authorization: Bearer [ACCESS_TOKEN] // //////////////////// // // AUTH // From 5f801b1dd1ffa2bfef684870dfa9cbcb9e32ae08 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 05:25:32 -0700 Subject: [PATCH 11/14] removed not used token in api.http --- apps/api/src/api.http | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/api/src/api.http b/apps/api/src/api.http index 148f6b68..a3757c68 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -86,8 +86,6 @@ Content-Type: application/json ### LOGIN POST http://localhost:3000/api/auth/login Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjU1Mjc2MiwiZXhwIjoxNzI2NTUzNjYyfQ.8mALljoCdTaU0JHZhXolfUc4fLkbjAyBYpfP7Wt2HfE - { "username": "mfee-test", From 7a150d074c3255e0b2031dd489754b6c1e159fdc Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 22:54:59 -0700 Subject: [PATCH 12/14] fixed initialized mongo records --- apps/api/src/api.http | 22 ++-- apps/api/src/controllers/post.ts | 106 ++++++------------ apps/api/src/data/initialData.ts | 10 +- .../api/src/helpers/controllerErrorHandler.ts | 16 ++- apps/api/src/main.ts | 3 +- apps/api/src/models/category.ts | 2 +- apps/api/src/models/posts.ts | 26 +++-- apps/api/src/schemas/post.ts | 5 +- 8 files changed, 82 insertions(+), 108 deletions(-) diff --git a/apps/api/src/api.http b/apps/api/src/api.http index a3757c68..5ae63087 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -4,14 +4,14 @@ ### GET CATEGORIES GET http://localhost:3000/api/categories -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 ### GET NOT VALID ROUTE GET http://localhost:3000/api/not-valid-route ### GET CATEGORIES BY ID -PATCH http://localhost:3000/api/categories/1 -Authorization: Bearer [ACCESS_TOKEN] +PATCH http://localhost:3000/api/categories/cat_1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 Content-Type: application/json { @@ -24,19 +24,19 @@ Content-Type: application/json ### 1. GET ALL POSTS GET http://localhost:3000/api/posts -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 ### 2. GET POSTS BY CATEGORY -GET http://localhost:3000/api/posts/category/1 -Authorization: Bearer [ACCESS_TOKEN] +GET http://localhost:3000/api/posts/category/cat_1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 ### 3. GET POST BY ID GET http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 ### 4. CREATE POST POST http://localhost:3000/api/posts -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 Content-Type: application/json { @@ -48,7 +48,7 @@ Content-Type: application/json ### 5. CREATE POST COMMENT POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 Content-Type: application/json { @@ -58,7 +58,7 @@ Content-Type: application/json ### 6. UPDATE POST PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 Content-Type: application/json { @@ -68,7 +68,7 @@ Content-Type: application/json ### 7. DELETE POST DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer [ACCESS_TOKEN] +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 // //////////////////// // // AUTH // diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts index 9c413a8d..c2b873d6 100644 --- a/apps/api/src/controllers/post.ts +++ b/apps/api/src/controllers/post.ts @@ -1,113 +1,77 @@ import { Request, Response } from 'express'; import { validateComment, validatePartialPost, validatePost } from '../helpers/validators'; import postModel from '../models/posts'; +import { asyncErrorHandler } from '../helpers/controllerErrorHandler'; // 1. Get all posts -const getPosts = async (req: Request, res: Response) => { - try { - const posts = await postModel.getAllPosts(); - res.status(200).json(posts); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); - } -}; +const getPosts = asyncErrorHandler(async (req: Request, res: Response) => { + const posts = await postModel.getAllPosts(); + res.status(200).json(posts); +}); // 2. Get posts by category -const getPostsByCategory = async (req: Request, res: Response) => { +const getPostsByCategory = asyncErrorHandler(async (req: Request, res: Response) => { const { category } = req.params; - try { - const posts = await postModel.getPostsByCategory(category); - if (!posts.length) { - return res.status(404).json({ message: `Posts not found for category ${category}` }); - } - res.status(200).json(posts); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); + const posts = await postModel.getPostsByCategory(category); + if (!posts.length) { + return res.status(404).json({ message: `Posts not found for category ${category}` }); } -}; + res.status(200).json(posts); +}); // 3. Get post by id -const getPostById = async (req: Request, res: Response) => { +const getPostById = asyncErrorHandler(async (req: Request, res: Response) => { const { id } = req.params; - try { - const post = await postModel.getPostById(id); - if (!post) { - return res.status(404).json({ message: 'Post not found' }); - } - res.status(200).json(post); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); + const post = await postModel.getPostById(id); + if (!post) { + return res.status(404).json({ message: 'Post not found' }); } -}; + res.status(200).json(post); +}); // 4. Create post -const createPost = async (req: Request, res: Response) => { +const createPost = asyncErrorHandler(async (req: Request, res: Response) => { const validationResult = validatePost(req.body); if (validationResult.error) { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } - - try { - const newPost = await postModel.createPost(validationResult.data); - res.status(201).json(newPost); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); - } -}; + const newPost = await postModel.createPost(validationResult.data); + res.status(201).json(newPost); +}); // 5. Create post comment -const createPostComment = async (req: Request, res: Response) => { +const createPostComment = asyncErrorHandler(async (req: Request, res: Response) => { const validationResult = validateComment(req.body); + console.log('validationResult', validationResult); if (validationResult.error) { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } const { id } = req.params; - - try { - const newComment = await postModel.createPostComment(id, validationResult.data); - res.status(201).json(newComment); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); - } -}; + const newComment = await postModel.createPostComment(id, validationResult.data); + res.status(201).json(newComment); +}); // 6. Update post -const updatePost = async (req: Request, res: Response) => { +const updatePost = asyncErrorHandler(async (req: Request, res: Response) => { const validationResult = validatePartialPost(req.body); if (validationResult.error) { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } const { id } = req.params; - - try { - const updatedPost = await postModel.updatePost(id, validationResult.data); - res.status(200).json(updatedPost); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); - } -}; + const updatedPost = await postModel.updatePost(id, validationResult.data); + res.status(200).json(updatedPost); +}); // 7. Delete post -const deletePost = async (req: Request, res: Response) => { +const deletePost = asyncErrorHandler(async (req: Request, res: Response) => { const { id } = req.params; - try { - const deletedPost = await postModel.deletePost(id); - console.log(deletedPost); - res.status(204).send({ message: `Post deleted successfully` }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; - res.status(500).json({ message: errorMessage }); - } -}; + const deletedPost = await postModel.deletePost(id); + console.log(deletedPost); + res.status(204).send({ message: `Post deleted successfully` }); +}); export default { getPosts, diff --git a/apps/api/src/data/initialData.ts b/apps/api/src/data/initialData.ts index 90e5853c..1434e67d 100644 --- a/apps/api/src/data/initialData.ts +++ b/apps/api/src/data/initialData.ts @@ -7,7 +7,7 @@ const POSTS: Post[] = [ image: 'https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800', description: 'Description from Postman', - category: '1', + category: 'cat_1', comments: ['a47f5337-16f2-49a0-bc14-e97bb24b56a1'] } ]; @@ -15,18 +15,18 @@ const POSTS: Post[] = [ const COMMENTS: Comment[] = [ { _id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a1', - author: 'MFEE', - content: 'Good content' + author: 'John Doe', + content: 'Great post!' } ]; const CATEGORIES = [ { - _id: '1', + _id: 'cat_1', name: 'Other' }, { - _id: '2', + _id: 'cat_2', name: 'React' } ]; diff --git a/apps/api/src/helpers/controllerErrorHandler.ts b/apps/api/src/helpers/controllerErrorHandler.ts index 56481285..8962ce01 100644 --- a/apps/api/src/helpers/controllerErrorHandler.ts +++ b/apps/api/src/helpers/controllerErrorHandler.ts @@ -1,9 +1,13 @@ import { Request, Response, NextFunction } from 'express'; -// Error handler wrapper for async functions -const asyncHandler = - (fn: (req: Request, res: Response, next: NextFunction) => Promise) => (req: Request, res: Response, next: NextFunction) => { - Promise.resolve(fn(req, res, next)).catch(next); // Catch errors and pass them to next() - }; +const UNKNOWN_ERROR_MESSAGE = 'An unknown error occurred'; -export default asyncHandler; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const asyncErrorHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise | Promise) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch((error) => { + const errorMessage = error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE; + res.status(500).json({ message: errorMessage }); + }); + }; +}; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 342722b3..6cd8be8f 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -9,6 +9,7 @@ import auth from './routes/auth'; import categories from './routes/categories'; import posts from './routes/posts'; import { envChecker } from './helpers/dotenvChecker'; +import { initMongoRecords } from './models/posts'; // Load environment variables from .env.local file so that we don't have them in the repo envChecker(); @@ -38,7 +39,7 @@ mongoose .connect(`${process.env.MONGO_URL}`) .then(() => { console.log('Connected to MongoDB'); - + initMongoRecords(); // Initialize a few records for testing app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); }); diff --git a/apps/api/src/models/category.ts b/apps/api/src/models/category.ts index eb552bd6..a6a8b4e1 100644 --- a/apps/api/src/models/category.ts +++ b/apps/api/src/models/category.ts @@ -2,7 +2,7 @@ import mongoose, { Document, Schema } from 'mongoose'; interface ICategory extends Document { name: string; - _id: string; + _id: mongoose.Schema.Types.String; } export const categorySchema = new Schema( diff --git a/apps/api/src/models/posts.ts b/apps/api/src/models/posts.ts index 3981c71b..86912a64 100644 --- a/apps/api/src/models/posts.ts +++ b/apps/api/src/models/posts.ts @@ -4,19 +4,18 @@ import PostSchema from '../schemas/post'; import Category from './category'; import CommentSchema from '../schemas/comment'; import { CATEGORIES, COMMENTS, POSTS } from '../data/initialData'; +import mongoose from 'mongoose'; //Initialize the database records so we can test -const initMongoRecords = async () => { - await PostSchema.deleteMany({}); - await Category.deleteMany({}); - await CommentSchema.deleteMany({}); - await PostSchema.create(POSTS); - await Category.create(CATEGORIES); - await CommentSchema.create(COMMENTS); +export const initMongoRecords = async () => { + await mongoose.connection.db.dropCollection('posts'); + await mongoose.connection.db.dropCollection('categories'); + await mongoose.connection.db.dropCollection('comments'); + await PostSchema.insertMany(POSTS); + await Category.insertMany(CATEGORIES); + await CommentSchema.insertMany(COMMENTS); }; -initMongoRecords(); - // 1. Get all posts const getAllPosts = async () => { return await PostSchema.find({}).populate(['comments', 'category']).sort({ createdAt: -1 }); @@ -54,12 +53,17 @@ const createPostComment = async (postId: string, comment: { author: string; cont ...comment }; - const foundPost = await PostSchema.findOne({ _id: postId }); + const foundPost = await PostSchema.findById(postId); if (!foundPost) { throw new Error('Post not found'); } + const createdComment = await CommentSchema.create(newComment); + + if (!createdComment) { + throw new Error('Comment not created'); + } - await PostSchema.updateOne({ _id: postId }, { $push: { comments: newComment._id } }); + await PostSchema.updateOne({ _id: postId }, { $push: { comments: newComment._id } }, { new: true }); return newComment; }; diff --git a/apps/api/src/schemas/post.ts b/apps/api/src/schemas/post.ts index cdf78398..89752c42 100644 --- a/apps/api/src/schemas/post.ts +++ b/apps/api/src/schemas/post.ts @@ -12,6 +12,7 @@ interface IPost extends mongoose.Document { const postSchema = new mongoose.Schema( { _id: { + // explicitly set _id to string type: String, required: [true, 'id is required'] }, @@ -28,12 +29,12 @@ const postSchema = new mongoose.Schema( required: [true, 'description is required'] }, category: { - type: mongoose.Schema.Types.String, // Foreign key + type: String, ref: 'Category', required: [true, 'category is required'] }, comments: { - type: [mongoose.Schema.Types.String], // Foreign key + type: [String], // Id values are being handled by Crypto.randomUUID(), not MongoDB ObjectId ref: 'Comment', required: [true, 'comments is required'] } From 4a96ce8d3564b0f066adc932f868c8a46d292ac1 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Tue, 17 Sep 2024 22:57:18 -0700 Subject: [PATCH 13/14] removed token --- apps/api/src/api.http | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/api/src/api.http b/apps/api/src/api.http index 5ae63087..dfad8a4c 100644 --- a/apps/api/src/api.http +++ b/apps/api/src/api.http @@ -4,14 +4,14 @@ ### GET CATEGORIES GET http://localhost:3000/api/categories -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] ### GET NOT VALID ROUTE GET http://localhost:3000/api/not-valid-route ### GET CATEGORIES BY ID PATCH http://localhost:3000/api/categories/cat_1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -24,19 +24,19 @@ Content-Type: application/json ### 1. GET ALL POSTS GET http://localhost:3000/api/posts -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] ### 2. GET POSTS BY CATEGORY GET http://localhost:3000/api/posts/category/cat_1 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] ### 3. GET POST BY ID GET http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] ### 4. CREATE POST POST http://localhost:3000/api/posts -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -48,7 +48,7 @@ Content-Type: application/json ### 5. CREATE POST COMMENT POST http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9/comments -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -58,7 +58,7 @@ Content-Type: application/json ### 6. UPDATE POST PATCH http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] Content-Type: application/json { @@ -68,7 +68,7 @@ Content-Type: application/json ### 7. DELETE POST DELETE http://localhost:3000/api/posts/a47f5337-16f2-49a0-bc14-e97bb24b56a9 -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1mZWUtdGVzdCIsImlhdCI6MTcyNjYzODY5MSwiZXhwIjoxNzI2NjM5NTkxfQ.7meGhYMF29DRMiY5gudHweje7yMRQhqskptenV81io8 +Authorization: Bearer [ACCESS_TOKEN] // //////////////////// // // AUTH // From 7db0e6381de1f0e1fbbd2632ddfa2ef8562ecb01 Mon Sep 17 00:00:00 2001 From: Coreandrum1 Date: Wed, 18 Sep 2024 00:14:04 -0700 Subject: [PATCH 14/14] postman test fixes --- apps/api/src/controllers/post.ts | 8 ++----- apps/api/src/data/initialData.ts | 4 ---- apps/api/src/models/category.ts | 5 ---- apps/api/src/models/posts.ts | 39 +++++++++++++------------------- apps/api/src/schemas/comment.ts | 9 ++------ apps/api/src/schemas/post.ts | 18 +++++---------- apps/api/src/types/types.ts | 2 -- 7 files changed, 26 insertions(+), 59 deletions(-) diff --git a/apps/api/src/controllers/post.ts b/apps/api/src/controllers/post.ts index c2b873d6..b5736c39 100644 --- a/apps/api/src/controllers/post.ts +++ b/apps/api/src/controllers/post.ts @@ -13,10 +13,8 @@ const getPosts = asyncErrorHandler(async (req: Request, res: Response) => { const getPostsByCategory = asyncErrorHandler(async (req: Request, res: Response) => { const { category } = req.params; + console.log('category', category); const posts = await postModel.getPostsByCategory(category); - if (!posts.length) { - return res.status(404).json({ message: `Posts not found for category ${category}` }); - } res.status(200).json(posts); }); @@ -44,7 +42,6 @@ const createPost = asyncErrorHandler(async (req: Request, res: Response) => { // 5. Create post comment const createPostComment = asyncErrorHandler(async (req: Request, res: Response) => { const validationResult = validateComment(req.body); - console.log('validationResult', validationResult); if (validationResult.error) { return res.status(400).json({ message: JSON.parse(validationResult.error.message) }); } @@ -68,8 +65,7 @@ const updatePost = asyncErrorHandler(async (req: Request, res: Response) => { const deletePost = asyncErrorHandler(async (req: Request, res: Response) => { const { id } = req.params; - const deletedPost = await postModel.deletePost(id); - console.log(deletedPost); + await postModel.deletePost(id); res.status(204).send({ message: `Post deleted successfully` }); }); diff --git a/apps/api/src/data/initialData.ts b/apps/api/src/data/initialData.ts index 1434e67d..c2588033 100644 --- a/apps/api/src/data/initialData.ts +++ b/apps/api/src/data/initialData.ts @@ -2,7 +2,6 @@ import { Comment, Post } from '../types/types'; const POSTS: Post[] = [ { - _id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a9', title: 'Post Test Postman', image: 'https://images.unsplash.com/photo-1556276797-5086e6b45ff9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=800', @@ -14,7 +13,6 @@ const POSTS: Post[] = [ const COMMENTS: Comment[] = [ { - _id: 'a47f5337-16f2-49a0-bc14-e97bb24b56a1', author: 'John Doe', content: 'Great post!' } @@ -22,11 +20,9 @@ const COMMENTS: Comment[] = [ const CATEGORIES = [ { - _id: 'cat_1', name: 'Other' }, { - _id: 'cat_2', name: 'React' } ]; diff --git a/apps/api/src/models/category.ts b/apps/api/src/models/category.ts index a6a8b4e1..c69bbc87 100644 --- a/apps/api/src/models/category.ts +++ b/apps/api/src/models/category.ts @@ -2,15 +2,10 @@ import mongoose, { Document, Schema } from 'mongoose'; interface ICategory extends Document { name: string; - _id: mongoose.Schema.Types.String; } export const categorySchema = new Schema( { - _id: { - type: String, - required: [true, 'id is required'] - }, name: { type: String, required: [true, 'Property is required'] diff --git a/apps/api/src/models/posts.ts b/apps/api/src/models/posts.ts index 86912a64..89c9275f 100644 --- a/apps/api/src/models/posts.ts +++ b/apps/api/src/models/posts.ts @@ -1,9 +1,6 @@ -import Crypto from 'crypto'; import { CuratedPost, Post } from '../types/types'; -import PostSchema from '../schemas/post'; -import Category from './category'; -import CommentSchema from '../schemas/comment'; -import { CATEGORIES, COMMENTS, POSTS } from '../data/initialData'; +import PostDocument from '../schemas/post'; +import CommentDocument from '../schemas/comment'; import mongoose from 'mongoose'; //Initialize the database records so we can test @@ -11,26 +8,24 @@ export const initMongoRecords = async () => { await mongoose.connection.db.dropCollection('posts'); await mongoose.connection.db.dropCollection('categories'); await mongoose.connection.db.dropCollection('comments'); - await PostSchema.insertMany(POSTS); - await Category.insertMany(CATEGORIES); - await CommentSchema.insertMany(COMMENTS); }; // 1. Get all posts const getAllPosts = async () => { - return await PostSchema.find({}).populate(['comments', 'category']).sort({ createdAt: -1 }); + const posts = await PostDocument.find({}).populate(['comments', 'category']).sort({ createdAt: -1 }); + return posts; }; // 2. Get posts by category -const getPostsByCategory = async (category: string) => { - const postsByCategory = await PostSchema.find({ category: category }).populate(['comments', 'category']).sort({ createdAt: -1 }); +const getPostsByCategory = async (categoryId: string) => { + const postsByCategory = await PostDocument.find({ category: categoryId }).populate(['comments', 'category']).sort({ createdAt: -1 }); return postsByCategory; }; // 3. Get post by id const getPostById = async (id: string) => { - const postById = await PostSchema.find({ _id: id }).populate(['comments', 'category']); - return postById; + const [post] = await PostDocument.find({ _id: id }).populate(['comments', 'category']); + return post; }; // 4. Create post @@ -38,45 +33,43 @@ const createPost = async (data: CuratedPost) => { // https://stackoverflow.com/questions/71185664/why-does-zod-make-all-my-schema-fields-optional // Zod make all my schema fields optional const newPost = { - _id: Crypto.randomUUID(), comments: [], ...data }; - await PostSchema.create(newPost); - return newPost; + const createdPost = await PostDocument.create(newPost); + return createdPost; }; // 5. Create post comment const createPostComment = async (postId: string, comment: { author: string; content: string }) => { const newComment = { - _id: Crypto.randomUUID(), ...comment }; - const foundPost = await PostSchema.findById(postId); + const foundPost = await PostDocument.findById(postId); if (!foundPost) { throw new Error('Post not found'); } - const createdComment = await CommentSchema.create(newComment); + const createdComment = await CommentDocument.create(newComment); if (!createdComment) { throw new Error('Comment not created'); } - await PostSchema.updateOne({ _id: postId }, { $push: { comments: newComment._id } }, { new: true }); + await PostDocument.updateOne({ _id: postId }, { $push: { comments: createdComment._id } }, { new: true }); return newComment; }; // 6. Update post const updatePost = async (postId: string, data: Partial) => { - const updatedPost = await PostSchema.findOneAndUpdate({ _id: postId }, data, { new: true }); + const updatedPost = await PostDocument.findOneAndUpdate({ _id: postId }, data, { new: true }); return updatedPost; }; // 7. Delete post const deletePost = async (postId: string) => { - const deletedPost = await PostSchema.findOneAndDelete({ _id: postId }); - const deletedComment = await CommentSchema.findOneAndDelete({ _id: postId }); + const deletedPost = await PostDocument.findOneAndDelete({ _id: postId }); + const deletedComment = await CommentDocument.findOneAndDelete({ _id: postId }); return { deletedComments: deletedComment, deletedPost: deletedPost }; }; diff --git a/apps/api/src/schemas/comment.ts b/apps/api/src/schemas/comment.ts index 761f360f..56e08435 100644 --- a/apps/api/src/schemas/comment.ts +++ b/apps/api/src/schemas/comment.ts @@ -1,17 +1,12 @@ import mongoose, { Document } from 'mongoose'; interface IComment extends Document { - _id: string; author: string; content: string; } const commentSchema = new mongoose.Schema( { - _id: { - type: String, - required: [true, 'id is required'] - }, author: { type: String, required: [true, 'Author is required'] @@ -26,6 +21,6 @@ const commentSchema = new mongoose.Schema( } ); -const Comment = mongoose.model('Comment', commentSchema); +const CommentDocument = mongoose.model('Comment', commentSchema); -export default Comment; +export default CommentDocument; diff --git a/apps/api/src/schemas/post.ts b/apps/api/src/schemas/post.ts index 89752c42..b03ceef3 100644 --- a/apps/api/src/schemas/post.ts +++ b/apps/api/src/schemas/post.ts @@ -1,21 +1,15 @@ import mongoose from 'mongoose'; interface IPost extends mongoose.Document { - _id: string; title: string; image: string; description: string; - category: string; - comments: string[]; + category: mongoose.Schema.Types.ObjectId; + comments: mongoose.Schema.Types.ObjectId[]; } const postSchema = new mongoose.Schema( { - _id: { - // explicitly set _id to string - type: String, - required: [true, 'id is required'] - }, title: { type: String, required: [true, 'title is required'] @@ -29,12 +23,12 @@ const postSchema = new mongoose.Schema( required: [true, 'description is required'] }, category: { - type: String, + type: mongoose.Schema.Types.ObjectId, ref: 'Category', required: [true, 'category is required'] }, comments: { - type: [String], // Id values are being handled by Crypto.randomUUID(), not MongoDB ObjectId + type: [mongoose.Schema.Types.ObjectId], ref: 'Comment', required: [true, 'comments is required'] } @@ -44,6 +38,6 @@ const postSchema = new mongoose.Schema( } ); -const PostSchema = mongoose.model('Post', postSchema); +const PostDocument = mongoose.model('Post', postSchema); -export default PostSchema; +export default PostDocument; diff --git a/apps/api/src/types/types.ts b/apps/api/src/types/types.ts index cc81942e..d6944389 100644 --- a/apps/api/src/types/types.ts +++ b/apps/api/src/types/types.ts @@ -1,5 +1,4 @@ export interface Post { - _id: string; title: string; image: string; description: string; @@ -15,7 +14,6 @@ export interface CuratedPost { } export interface Comment { - _id: string; author: string; content: string; }