From 77f8b4cdf09b368a712b40f413b20b7fc983abb9 Mon Sep 17 00:00:00 2001 From: yolanda Date: Sun, 18 May 2025 11:46:51 +0200 Subject: [PATCH 1/9] users --- package-lock.json | 43 ++++++++++++++++++++-------------------- package.json | 11 +++++----- src/config/database.ts | 1 + src/controllers/users.ts | 0 src/router/index.ts | 4 ++-- src/router/users.ts | 0 src/schemas/users.ts | 9 +++++++++ 7 files changed, 40 insertions(+), 28 deletions(-) create mode 100644 src/controllers/users.ts create mode 100644 src/router/users.ts create mode 100644 src/schemas/users.ts diff --git a/package-lock.json b/package-lock.json index e5d295b..17633f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,16 +12,17 @@ "axios": "^1.9.0", "body-parser": "^1.20.3", "cors": "^2.8.5", - "dotenv": "^16.4.7", - "express": "^4.17.1", - "mongoose": "^8.14.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "mongoose": "^8.15.0", "ts-node": "^10.9.2" }, "devDependencies": { - "@types/express": "5.0.0", + "@types/express": "^5.0.0", "@types/mongoose": "^5.11.96", + "@types/node": "^22.15.18", "ts-node-dev": "^2.0.0", - "typescript": "^5.7.3" + "typescript": "^5.8.3" } }, "node_modules/@cspotcode/source-map-support": { @@ -166,12 +167,12 @@ } }, "node_modules/@types/node": { - "version": "22.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", - "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", + "version": "22.15.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", + "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/qs": { @@ -588,9 +589,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1282,9 +1283,9 @@ } }, "node_modules/mongoose": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.14.1.tgz", - "integrity": "sha512-ijd12vjqUBr5Btqqflu0c/o8Oed5JpdaE0AKO9TjGxCgywYwnzt6ynR1ySjhgxGxrYVeXC0t1P11f1zlRiE93Q==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.0.tgz", + "integrity": "sha512-WFKsY1q12ScGabnZWUB9c/QzZmz/ESorrV27OembB7Gz6rrh9m3GA4Srsv1uvW1s9AHO5DeZ6DdUTyF9zyNERQ==", "license": "MIT", "dependencies": { "bson": "^6.10.3", @@ -1966,9 +1967,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -1979,9 +1980,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index 62e8118..2d120c6 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,19 @@ "license": "ISC", "description": "", "devDependencies": { - "@types/express": "5.0.0", + "@types/express": "^5.0.0", "@types/mongoose": "^5.11.96", + "@types/node": "^22.15.18", "ts-node-dev": "^2.0.0", - "typescript": "^5.7.3" + "typescript": "^5.8.3" }, "dependencies": { "axios": "^1.9.0", "body-parser": "^1.20.3", "cors": "^2.8.5", - "dotenv": "^16.4.7", - "express": "^4.17.1", - "mongoose": "^8.14.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "mongoose": "^8.15.0", "ts-node": "^10.9.2" } } diff --git a/src/config/database.ts b/src/config/database.ts index 884bf83..dd11428 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -5,6 +5,7 @@ import { Sneaker } from "../schemas/sneaker"; import { Currency } from "../schemas/currency"; import { Store } from "../schemas/store"; + import sneakerSeedData from "../../seeds/sneakers/sneakers.json"; import currencySeedData from "../../seeds/currencies.json"; import storeSeedData from "../../seeds/stores/stores.json"; diff --git a/src/controllers/users.ts b/src/controllers/users.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/router/index.ts b/src/router/index.ts index cb013e4..1123910 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,11 +1,11 @@ import express from 'express'; import sneakersRouter from './sneakers'; import storesRouter from './stores'; - +import usersRouter from './users'; const router = express.Router() router.use('/sneakers', sneakersRouter); router.use('/stores', storesRouter); - +router.use('/users', usersRouter); export default router; \ No newline at end of file diff --git a/src/router/users.ts b/src/router/users.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/schemas/users.ts b/src/schemas/users.ts new file mode 100644 index 0000000..e900b2c --- /dev/null +++ b/src/schemas/users.ts @@ -0,0 +1,9 @@ +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + favorites: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Sneaker' }] +}); + +export const User = mongoose.model("User", userSchema); From 9d18fb87e66cf0a3599090f1cc4fa1e24fe37b6a Mon Sep 17 00:00:00 2001 From: yolanda Date: Sun, 18 May 2025 14:01:05 +0200 Subject: [PATCH 2/9] add controllers --- src/controllers/users.ts | 207 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/src/controllers/users.ts b/src/controllers/users.ts index e69de29..17283d8 100644 --- a/src/controllers/users.ts +++ b/src/controllers/users.ts @@ -0,0 +1,207 @@ +import { User } from "../schemas/users"; +import { Sneaker } from "../schemas/sneaker"; + + +export const getUserById = async (req, res) => { + try { + const { id } = req.params; + + const user = await User.findById(id); + + if (!user) { + return res.status(404).json({ + message: 'User not found', + status: 'failure' + }); + } + + return res.status(200).json({ + items: user, + status: 'success' + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error fetching user', + status: 'failure' + }); + } +}; + + +export const createUser = async (req, res) => { + try { + const { name, email } = req.body; + + if (!name || !email) { + return res.status(400).json({ + message: 'Invalid input data', + status: 'failure' + }); + } + + const newUser = new User({ name, email }); + await newUser.save(); + + return res.status(201).json({ + items: newUser, + message: 'User created successfully', + status: 'success' + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error creating user', + status: 'failure' + }); + } +}; + +export const updateUser = async (req, res) => { + try { + const { id } = req.params; + const updateData = req.body; + + if (!updateData.name && !updateData.email) { + return res.status(400).json({ + message: 'Invalid input data', + status: 'failure' + }); + } + + const updatedUser = await User.findByIdAndUpdate(id, updateData, { new: true }); + + if (!updatedUser) { + return res.status(404).json({ + message: "User not found", + status: "failure" + }); + } + + return res.status(200).json({ + items: updatedUser, + message: "User successfully updated", + status: "success" + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error updating user', + status: 'failure' + }); + } +}; + + +export const deleteUser = async (req, res) => { + try { + const { id } = req.params; + + const user = await User.findByIdAndDelete(id); + + if (!user) { + return res.status(404).json({ + message: "User not found", + status: "failure" + }); + } + + return res.status(204).send(); + + } catch (error) { + return res.status(500).json({ + message: 'Error deleting user', + status: 'failure' + }); + } +}; + +export const getFavorites = async (req, res) => { + try { + const { id } = req.params; + const user = await User.findById(id).populate("favorites"); + + if (!user) { + return res.status(404).json({ + message: "User not found", + status: "failure" + }); + } + + return res.status(200).json({ + items: user.favorites || [], + status: "success" + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error fetching favorites', + status: 'failure' + }); + } +}; + + +export const addFavorite = async (req, res) => { + try { + const { id } = req.params; + const { sneakerId } = req.body; + + const user = await User.findById(id); + const sneaker = await Sneaker.findById(sneakerId); + + if (!user || !sneaker) { + return res.status(404).json({ + message: "User or sneaker not found", + status: "failure" + }); + } + + if (!user.favorites.includes(sneakerId)) { + user.favorites.push(sneakerId); + await user.save(); + } + + return res.status(201).json({ + items: user.favorites, + message: "Sneaker added to favorites", + status: "success" + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error adding favorite', + status: 'failure' + }); + } +}; + + +export const removeFavorite = async (req, res) => { + try { + const { id, sneakerId } = req.params; + + const user = await User.findById(id); + + if (!user) { + return res.status(404).json({ + message: "User not found", + status: "failure" + }); + } + + user.favorites = user.favorites.filter( + (fav) => fav.toString() !== sneakerId + ); + + await user.save(); + + return res.status(204).send(); + + } catch (error) { + return res.status(500).json({ + message: 'Error removing favorite', + status: 'failure' + }); + } +}; From ca8f981e02f7253b4468ab65a0a5e95fe3242ee9 Mon Sep 17 00:00:00 2001 From: yolanda Date: Sun, 18 May 2025 14:11:14 +0200 Subject: [PATCH 3/9] update router --- src/router/users.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/router/users.ts b/src/router/users.ts index e69de29..63ce362 100644 --- a/src/router/users.ts +++ b/src/router/users.ts @@ -0,0 +1,35 @@ +import express from 'express'; +import { + createUser, + getUserById, + updateUser, + deleteUser, + getFavorites, + addFavorite, + removeFavorite +} from '../controllers/users'; + +const usersRouter = express.Router(); + +// POST /users +usersRouter.post('/', createUser); + +// GET /users/:id +usersRouter.get('/:id', getUserById); + +// PUT /users/:id +usersRouter.put('/:id', updateUser); + +// DELETE /users/:id +usersRouter.delete('/:id', deleteUser); + +// GET /users/:id/favorites +usersRouter.get('/:id/favorites', getFavorites); + +// POST /users/:id/favorites +usersRouter.post('/:id/favorites', addFavorite); + +// DELETE /users/:id/favorites/:sneakerId +usersRouter.delete('/:id/favorites/:sneakerId', removeFavorite); + +export default usersRouter; From 9ff2d6a72ac18948e718d76244109f5f886cece9 Mon Sep 17 00:00:00 2001 From: Khalid Date: Sun, 18 May 2025 15:08:28 +0200 Subject: [PATCH 4/9] refactor: cleanup comments --- src/router/users.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/router/users.ts b/src/router/users.ts index 63ce362..937ab1c 100644 --- a/src/router/users.ts +++ b/src/router/users.ts @@ -11,25 +11,19 @@ import { const usersRouter = express.Router(); -// POST /users -usersRouter.post('/', createUser); - -// GET /users/:id +// GET usersRouter.get('/:id', getUserById); - -// PUT /users/:id -usersRouter.put('/:id', updateUser); - -// DELETE /users/:id -usersRouter.delete('/:id', deleteUser); - -// GET /users/:id/favorites usersRouter.get('/:id/favorites', getFavorites); -// POST /users/:id/favorites +// POST +usersRouter.post('/', createUser); usersRouter.post('/:id/favorites', addFavorite); -// DELETE /users/:id/favorites/:sneakerId +// PUT +usersRouter.put('/:id', updateUser); + +// DELETE +usersRouter.delete('/:id', deleteUser); usersRouter.delete('/:id/favorites/:sneakerId', removeFavorite); export default usersRouter; From 02b0dff0bd45851e085f9ef1876de05f74b0b727 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 02:24:23 +0200 Subject: [PATCH 5/9] feat: add 409 status for createUser --- docs/openapi.yaml | 6 ++++++ src/controllers/users.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index d9af5fb..71da526 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -331,6 +331,12 @@ paths: content: application/json: schema: { $ref: '#/components/schemas/Error' } + '409': + description: Email already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /users/{id}: get: diff --git a/src/controllers/users.ts b/src/controllers/users.ts index 17283d8..f389023 100644 --- a/src/controllers/users.ts +++ b/src/controllers/users.ts @@ -40,7 +40,16 @@ export const createUser = async (req, res) => { }); } - const newUser = new User({ name, email }); + const existingUser = await User.findOne({ email }); + + if (existingUser) { + return res.status(409).json({ + message: 'Email already exists', + status: 'failure' + }); + } + + const newUser = await User.create({ name, email }); await newUser.save(); return res.status(201).json({ From 00cb6b8f062d2285030a25be70692f1d8b649c05 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 02:25:08 +0200 Subject: [PATCH 6/9] fix: require both name and email in updateUser validation --- src/controllers/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/users.ts b/src/controllers/users.ts index f389023..4cf57b5 100644 --- a/src/controllers/users.ts +++ b/src/controllers/users.ts @@ -71,7 +71,7 @@ export const updateUser = async (req, res) => { const { id } = req.params; const updateData = req.body; - if (!updateData.name && !updateData.email) { + if (!updateData.name || !updateData.email) { return res.status(400).json({ message: 'Invalid input data', status: 'failure' From f4b5a92a5a0f65b8a641eccb5a8275dbe9dc1971 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 02:33:24 +0200 Subject: [PATCH 7/9] fix(openapi): readapt error schema --- docs/openapi.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 71da526..083cc7e 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -846,6 +846,9 @@ components: Error: type: object properties: - code: { type: string } - message: { type: string } - required: [error] \ No newline at end of file + message: + type: string + status: + type: string + enum: [failure] + required: [message, status] \ No newline at end of file From 0d2936cad29ddfbcc9a6959c93062b64e62efbd1 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 03:45:07 +0200 Subject: [PATCH 8/9] fix(users): make the openAPI and the current responses/bodies consistent --- docs/openapi.yaml | 52 ++++++++++++++++++++++++++++++++++------ src/controllers/users.ts | 4 ++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 083cc7e..58aa363 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -318,7 +318,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserInput' responses: '201': description: User created @@ -355,7 +355,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserResponse' '404': description: User not found content: @@ -377,14 +377,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserInput' responses: '200': description: User updated content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserResponse' '400': description: Invalid input data content: @@ -466,9 +466,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Sneaker' + $ref: '#/components/schemas/FavoritesResponse' '404': description: User or sneaker not found content: @@ -804,6 +802,16 @@ components: type: string required: [rating] + UserInput: + type: object + properties: + name: + type: string + email: + type: string + format: email + required: [name, email] + User: type: object properties: @@ -815,8 +823,38 @@ components: email: type: string format: email + favorites: + type: array + items: + type: string required: [name, email] + UserResponse: + type: object + properties: + items: + $ref: '#/components/schemas/User' + message: + type: string + status: + type: string + enum: [success] + required: [items, message, status] + + FavoritesResponse: + type: object + properties: + items: + type: array + items: + type: string + message: + type: string + status: + type: string + enum: [success] + required: [items, message, status] + Provider: type: object properties: diff --git a/src/controllers/users.ts b/src/controllers/users.ts index 4cf57b5..74e1043 100644 --- a/src/controllers/users.ts +++ b/src/controllers/users.ts @@ -17,6 +17,7 @@ export const getUserById = async (req, res) => { return res.status(200).json({ items: user, + message: 'User data fetched succesfully.', status: 'success' }); @@ -49,8 +50,7 @@ export const createUser = async (req, res) => { }); } - const newUser = await User.create({ name, email }); - await newUser.save(); + const newUser = await User.insertOne({ name, email }); return res.status(201).json({ items: newUser, From 777398b320ee11a9a8b294f2d3a4646a0690f0ea Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 03:49:17 +0200 Subject: [PATCH 9/9] fix(users): handle the case where the favorite is not found --- src/controllers/users.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/controllers/users.ts b/src/controllers/users.ts index 74e1043..8588754 100644 --- a/src/controllers/users.ts +++ b/src/controllers/users.ts @@ -199,10 +199,18 @@ export const removeFavorite = async (req, res) => { }); } - user.favorites = user.favorites.filter( - (fav) => fav.toString() !== sneakerId + const index = user.favorites.findIndex( + (fav) => fav.toString() === sneakerId ); + if (index === -1) { + return res.status(404).json({ + message: "Sneaker not found in favorites", + status: "failure" + }); + } + + user.favorites.splice(index, 1); await user.save(); return res.status(204).send();