From 77f8b4cdf09b368a712b40f413b20b7fc983abb9 Mon Sep 17 00:00:00 2001 From: yolanda Date: Sun, 18 May 2025 11:46:51 +0200 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 11c19505c8cde7ec0aecf28dec5c8a2c5b3b9c01 Mon Sep 17 00:00:00 2001 From: kike454 Date: Sun, 18 May 2025 18:06:58 +0200 Subject: [PATCH 05/26] feat: add providers --- src/controllers/provider.ts | 132 ++++++++++++++++++++++++++++++++++++ src/router/index.ts | 2 + src/router/provider.ts | 25 +++++++ src/schemas/provider.ts | 8 +++ 4 files changed, 167 insertions(+) create mode 100644 src/controllers/provider.ts create mode 100644 src/router/provider.ts create mode 100644 src/schemas/provider.ts diff --git a/src/controllers/provider.ts b/src/controllers/provider.ts new file mode 100644 index 0000000..c544428 --- /dev/null +++ b/src/controllers/provider.ts @@ -0,0 +1,132 @@ +import { Provider } from "../schemas/provider"; + +export const getProviders = async (req, res) => { + try { + const providers = await Provider.find(); + + if (!providers || providers.length === 0) { + return res.status(404).json({ + message: 'No providers found', + status: 'failure' + }); + } + + return res.status(200).json({ + items: providers, + count: providers.length, + message: 'Providers data fetched successfully', + status: 'success' + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error fetching providers', + status: 'failure' + }); + } +}; + +export const getProviderById = async (req, res) => { + const { id } = req.params; + + try { + const provider = await Provider.findById(id); + + if (!provider) { + return res.status(404).json({ + message: 'Provider not found', + status: 'failure' + }); + } + + return res.status(200).json({ + items: provider, + message: 'Provider data fetched successfully', + status: 'success' + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error fetching provider', + status: 'failure' + }); + } +}; + +export const createProvider = async (req, res) => { + try { + const providerData = req.body; + + if (!providerData.name || !providerData.contact_email) { + return res.status(400).json({ + message: 'Invalid input data', + status: 'failure' + }); + } + + const provider = await Provider.insertOne(providerData); + + return res.status(201).json({ + items: provider, + message: 'Provider created successfully', + status: 'success' + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error creating provider', + status: 'failure' + }); + } +}; + +export const updateProviderById = async (req, res) => { + const { id } = req.params; + const updateData = req.body; + + try { + const provider = await Provider.findByIdAndUpdate(id, updateData, { new: true }); + + if (!provider) { + return res.status(404).json({ + message: 'Provider not found', + status: 'failure' + }); + } + + return res.status(200).json({ + items: provider, + message: 'Provider updated successfully', + status: 'success' + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error updating provider', + status: 'failure' + }); + } +}; + +export const deleteProviderById = async (req, res) => { + const { id } = req.params; + + try { + const provider = await Provider.findByIdAndDelete(id); + + if (!provider) { + return res.status(404).json({ + message: 'Provider not found', + status: 'failure' + }); + } + + return res.status(204).send(); + + } catch (error) { + return res.status(500).json({ + message: 'Error deleting provider', + status: 'failure' + }); + } +}; diff --git a/src/router/index.ts b/src/router/index.ts index cb013e4..a14eccd 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,11 +1,13 @@ import express from 'express'; import sneakersRouter from './sneakers'; import storesRouter from './stores'; +import providersRouter from './provider'; const router = express.Router() router.use('/sneakers', sneakersRouter); router.use('/stores', storesRouter); +router.use('/providers', providersRouter); export default router; \ No newline at end of file diff --git a/src/router/provider.ts b/src/router/provider.ts new file mode 100644 index 0000000..4fd1f75 --- /dev/null +++ b/src/router/provider.ts @@ -0,0 +1,25 @@ +import express from 'express'; +import { + getProviders, + getProviderById, + createProvider, + updateProviderById, + deleteProviderById +} from '../controllers/provider'; + +const providersRouter = express.Router(); + +// GET +providersRouter.get('/', getProviders); +providersRouter.get('/:id', getProviderById); + +// POST +providersRouter.post('/', createProvider); + +// PUT +providersRouter.put('/:id', updateProviderById); + +// DELETE +providersRouter.delete('/:id', deleteProviderById); + +export default providersRouter; diff --git a/src/schemas/provider.ts b/src/schemas/provider.ts new file mode 100644 index 0000000..562c558 --- /dev/null +++ b/src/schemas/provider.ts @@ -0,0 +1,8 @@ +import mongoose from "mongoose"; + +const providerSchema = new mongoose.Schema({ + name: { type: String, required: true }, + contact_email: { type: String, required: true } +}); + +export const Provider = mongoose.model('Provider', providerSchema); From 02b0dff0bd45851e085f9ef1876de05f74b0b727 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 02:24:23 +0200 Subject: [PATCH 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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(); From 9cbcdf7baaf29d26440c8e2526aab326a8b2a591 Mon Sep 17 00:00:00 2001 From: pablogaraay Date: Mon, 19 May 2025 13:24:28 +0200 Subject: [PATCH 11/26] Added Providers JSON --- seeds/providers/providers.json | 402 +++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 seeds/providers/providers.json diff --git a/seeds/providers/providers.json b/seeds/providers/providers.json new file mode 100644 index 0000000..835ed7e --- /dev/null +++ b/seeds/providers/providers.json @@ -0,0 +1,402 @@ +[ + { + "name": "Nike", + "contact_email": "support@nike.com" + }, + { + "name": "Adidas", + "contact_email": "support@adidas.com" + }, + { + "name": "Puma", + "contact_email": "support@puma.com" + }, + { + "name": "Reebok", + "contact_email": "support@reebok.com" + }, + { + "name": "New Balance", + "contact_email": "support@newbalance.com" + }, + { + "name": "Under Armour", + "contact_email": "support@underarmour.com" + }, + { + "name": "Asics", + "contact_email": "support@asics.com" + }, + { + "name": "Converse", + "contact_email": "support@converse.com" + }, + { + "name": "Vans", + "contact_email": "support@vans.com" + }, + { + "name": "Fila", + "contact_email": "support@fila.com" + }, + { + "name": "Brooks", + "contact_email": "support@brooks.com" + }, + { + "name": "Saucony", + "contact_email": "support@saucony.com" + }, + { + "name": "Skechers", + "contact_email": "support@skechers.com" + }, + { + "name": "Mizuno", + "contact_email": "support@mizuno.com" + }, + { + "name": "Jordan", + "contact_email": "support@jordan.com" + }, + { + "name": "New Era", + "contact_email": "support@newera.com" + }, + { + "name": "DC Shoes", + "contact_email": "support@dcshoes.com" + }, + { + "name": "Burton", + "contact_email": "support@burton.com" + }, + { + "name": "The North Face", + "contact_email": "support@thenorthface.com" + }, + { + "name": "Columbia", + "contact_email": "support@columbia.com" + }, + { + "name": "Salomon", + "contact_email": "support@salomon.com" + }, + { + "name": "Merrell", + "contact_email": "support@merrell.com" + }, + { + "name": "Patagonia", + "contact_email": "support@patagonia.com" + }, + { + "name": "Arc'teryx", + "contact_email": "support@arcteryx.com" + }, + { + "name": "Hoka One One", + "contact_email": "support@hokaoneone.com" + }, + { + "name": "Altra", + "contact_email": "support@altra.com" + }, + { + "name": "On Running", + "contact_email": "support@onrunning.com" + }, + { + "name": "Vibram", + "contact_email": "support@vibram.com" + }, + { + "name": "K-Swiss", + "contact_email": "support@k-swiss.com" + }, + { + "name": "Lululemon", + "contact_email": "support@lululemon.com" + }, + { + "name": "Karhu", + "contact_email": "support@karhu.com" + }, + { + "name": "Diadora", + "contact_email": "support@diadora.com" + }, + { + "name": "Ecco", + "contact_email": "support@ecco.com" + }, + { + "name": "Gola", + "contact_email": "support@gola.com" + }, + { + "name": "KangaROOS", + "contact_email": "support@kangaroos.com" + }, + { + "name": "Le Coq Sportif", + "contact_email": "support@lecoqsportif.com" + }, + { + "name": "Veja", + "contact_email": "support@veja.com" + }, + { + "name": "Allbirds", + "contact_email": "support@allbirds.com" + }, + { + "name": "Koio", + "contact_email": "support@koio.com" + }, + { + "name": "Cariuma", + "contact_email": "support@cariuma.com" + }, + { + "name": "Novesta", + "contact_email": "support@novesta.com" + }, + { + "name": "Danner", + "contact_email": "support@danner.com" + }, + { + "name": "Clarks", + "contact_email": "support@clarks.com" + }, + { + "name": "TOMS", + "contact_email": "support@toms.com" + }, + { + "name": "Supra", + "contact_email": "support@supra.com" + }, + { + "name": "Golden Goose", + "contact_email": "support@goldengoose.com" + }, + { + "name": "Common Projects", + "contact_email": "support@commonprojects.com" + }, + { + "name": "BAPE", + "contact_email": "support@bape.com" + }, + { + "name": "Yeezy", + "contact_email": "support@yeezy.com" + }, + { + "name": "Li-Ning", + "contact_email": "support@li-ning.com" + }, + { + "name": "Peak", + "contact_email": "support@peak.com" + }, + { + "name": "Anta", + "contact_email": "support@anta.com" + }, + { + "name": "361 Degrees", + "contact_email": "support@361degrees.com" + }, + { + "name": "Air Jordan", + "contact_email": "support@airjordan.com" + }, + { + "name": "Y-3", + "contact_email": "support@y-3.com" + }, + { + "name": "Off-White", + "contact_email": "support@off-white.com" + }, + { + "name": "Balenciaga", + "contact_email": "support@balenciaga.com" + }, + { + "name": "Gucci", + "contact_email": "support@gucci.com" + }, + { + "name": "Dior", + "contact_email": "support@dior.com" + }, + { + "name": "Dolce & Gabbana", + "contact_email": "support@dolcegabbana.com" + }, + { + "name": "Moschino", + "contact_email": "support@moschino.com" + }, + { + "name": "Alexander McQueen", + "contact_email": "support@alexandermcqueen.com" + }, + { + "name": "Prada", + "contact_email": "support@prada.com" + }, + { + "name": "Versace", + "contact_email": "support@versace.com" + }, + { + "name": "Saint Laurent", + "contact_email": "support@saintlaurent.com" + }, + { + "name": "Givenchy", + "contact_email": "support@givenchy.com" + }, + { + "name": "Fendi", + "contact_email": "support@fendi.com" + }, + { + "name": "Valentino", + "contact_email": "support@valentino.com" + }, + { + "name": "Chanel", + "contact_email": "support@chanel.com" + }, + { + "name": "Balmain", + "contact_email": "support@balmain.com" + }, + { + "name": "Undercover", + "contact_email": "support@undercover.com" + }, + { + "name": "Maison Margiela", + "contact_email": "support@maisonmargiela.com" + }, + { + "name": "Rick Owens", + "contact_email": "support@rickowens.com" + }, + { + "name": "Veja x Rick Owens", + "contact_email": "support@vejaxrickowens.com" + }, + { + "name": "Nike SB", + "contact_email": "support@nikesb.com" + }, + { + "name": "Adidas Originals", + "contact_email": "support@adidasoriginals.com" + }, + { + "name": "Puma Select", + "contact_email": "support@pumaselect.com" + }, + { + "name": "New Balance Numeric", + "contact_email": "support@newbalancenumeric.com" + }, + { + "name": "Saucony Originals", + "contact_email": "support@sauconyoriginals.com" + }, + { + "name": "Converse Chuck", + "contact_email": "support@conversechuck.com" + }, + { + "name": "Vans Vault", + "contact_email": "support@vansvault.com" + }, + { + "name": "Reebok Classics", + "contact_email": "support@reebokclassics.com" + }, + { + "name": "Asics Tiger", + "contact_email": "support@asicstiger.com" + }, + { + "name": "Onitsuka Tiger", + "contact_email": "support@onitsukatiger.com" + }, + { + "name": "Under Armour HOVR", + "contact_email": "support@underarmourhovr.com" + }, + { + "name": "Skechers Sport", + "contact_email": "support@skecherssport.com" + }, + { + "name": "Merrell Moab", + "contact_email": "support@merrellmoab.com" + }, + { + "name": "Salomon Speedcross", + "contact_email": "support@salomonspeedcross.com" + }, + { + "name": "Patagonia Footwear", + "contact_email": "support@patagoniafootwear.com" + }, + { + "name": "Arc'teryx Veilance", + "contact_email": "support@arcteryxveilance.com" + }, + { + "name": "Hoka EVO", + "contact_email": "support@hokaevo.com" + }, + { + "name": "Altra Escalante", + "contact_email": "support@altraescalante.com" + }, + { + "name": "On Cloud", + "contact_email": "support@oncloud.com" + }, + { + "name": "Vibram FiveFingers", + "contact_email": "support@vibramfivefingers.com" + }, + { + "name": "K-Swiss Classic", + "contact_email": "support@k-swissclassic.com" + }, + { + "name": "Lululemon Surge", + "contact_email": "support@lululemonsurge.com" + }, + { + "name": "Karhu Fusion", + "contact_email": "support@karhufusion.com" + }, + { + "name": "Diadora Heritage", + "contact_email": "support@diadoraheritage.com" + }, + { + "name": "Ecco Biom", + "contact_email": "support@eccobiom.com" + }, + { + "name": "Gola Classics", + "contact_email": "support@golaclassics.com" + } + ] \ No newline at end of file From 2ee7adc5360c16c5216abc95300465fc9858b721 Mon Sep 17 00:00:00 2001 From: pablogaraay Date: Mon, 19 May 2025 13:31:37 +0200 Subject: [PATCH 12/26] provider schema fixed --- src/schemas/provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schemas/provider.ts b/src/schemas/provider.ts index 562c558..f8664e3 100644 --- a/src/schemas/provider.ts +++ b/src/schemas/provider.ts @@ -1,6 +1,7 @@ import mongoose from "mongoose"; const providerSchema = new mongoose.Schema({ + _id: { type: String }, name: { type: String, required: true }, contact_email: { type: String, required: true } }); From 478ff04e6be653fb3ffa8450e528e6d522a23ab0 Mon Sep 17 00:00:00 2001 From: pablogaraay Date: Mon, 19 May 2025 13:40:12 +0200 Subject: [PATCH 13/26] provider schema email validation fixed --- package.json | 3 ++- src/schemas/provider.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2d120c6..f1ada60 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dotenv": "^16.5.0", "express": "^4.21.2", "mongoose": "^8.15.0", - "ts-node": "^10.9.2" + "ts-node": "^10.9.2", + "validator": "^13.15.0" } } diff --git a/src/schemas/provider.ts b/src/schemas/provider.ts index f8664e3..4b3cf17 100644 --- a/src/schemas/provider.ts +++ b/src/schemas/provider.ts @@ -1,9 +1,10 @@ import mongoose from "mongoose"; +import isEmail from 'validator/lib/isEmail'; const providerSchema = new mongoose.Schema({ _id: { type: String }, name: { type: String, required: true }, - contact_email: { type: String, required: true } + contact_email: { type: String, required: true, validate: {validator: (v: string) => isEmail(v)}} }); export const Provider = mongoose.model('Provider', providerSchema); From 0a5b65d765d194b2dd43451bb15be1470e125990 Mon Sep 17 00:00:00 2001 From: pablogaraay Date: Mon, 19 May 2025 13:46:24 +0200 Subject: [PATCH 14/26] providers metadata added --- seeds/providers/providers.metadata.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 seeds/providers/providers.metadata.json diff --git a/seeds/providers/providers.metadata.json b/seeds/providers/providers.metadata.json new file mode 100644 index 0000000..cad1d5f --- /dev/null +++ b/seeds/providers/providers.metadata.json @@ -0,0 +1,12 @@ +{ + "indexes": [ + { + "v": {"numberInt": "2" }, + "key": { "_id": { "$numberInt": "1" } }, + "name": "_id_" + } + ], + "uuid": "0123456789abcdef0123456789abcdef", + "collectionName": "providers", + "type": "collection" +} \ No newline at end of file From 82b8b5a74ba532b4d044e5346390bda119fe493c Mon Sep 17 00:00:00 2001 From: pablogaraay Date: Mon, 19 May 2025 14:31:05 +0200 Subject: [PATCH 15/26] errors fixed --- package.json | 3 +-- seeds/providers/providers.metadata.json | 12 ------------ src/schemas/provider.ts | 3 +-- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 seeds/providers/providers.metadata.json diff --git a/package.json b/package.json index f1ada60..2d120c6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "dotenv": "^16.5.0", "express": "^4.21.2", "mongoose": "^8.15.0", - "ts-node": "^10.9.2", - "validator": "^13.15.0" + "ts-node": "^10.9.2" } } diff --git a/seeds/providers/providers.metadata.json b/seeds/providers/providers.metadata.json deleted file mode 100644 index cad1d5f..0000000 --- a/seeds/providers/providers.metadata.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "indexes": [ - { - "v": {"numberInt": "2" }, - "key": { "_id": { "$numberInt": "1" } }, - "name": "_id_" - } - ], - "uuid": "0123456789abcdef0123456789abcdef", - "collectionName": "providers", - "type": "collection" -} \ No newline at end of file diff --git a/src/schemas/provider.ts b/src/schemas/provider.ts index 4b3cf17..abc416a 100644 --- a/src/schemas/provider.ts +++ b/src/schemas/provider.ts @@ -1,10 +1,9 @@ import mongoose from "mongoose"; -import isEmail from 'validator/lib/isEmail'; const providerSchema = new mongoose.Schema({ _id: { type: String }, name: { type: String, required: true }, - contact_email: { type: String, required: true, validate: {validator: (v: string) => isEmail(v)}} + contact_email: { type: String, required: true} }); export const Provider = mongoose.model('Provider', providerSchema); From 0a006d3b933fad86469d4a48c72dc24f73f638ff Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Mon, 19 May 2025 21:50:04 +0200 Subject: [PATCH 16/26] feat: add first reviews endpoints and controllers This two endpoints are the only ones related to reviews that start with /sneakers --- docs/openapi.yaml | 41 +++++++++++++--- src/controllers/sneakers.ts | 97 ++++++++++++++++++++++++++++++++++++- src/router/sneakers.ts | 7 ++- src/schemas/review.ts | 11 +++++ 4 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 src/schemas/review.ts diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 58aa363..1fbcd47 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,11 +1,11 @@ -openapi: 3.0.0 +openapi: 3.0.3 info: title: Snapi (Sneakers API) version: 1.0.0 description: API for managing sneakers, reviews, users, providers and stores servers: - - url: http://localhost:8080/api - description: V1 + - url: http://localhost:8080/api/ + description: Production server tags: - name: sneakers @@ -216,7 +216,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Review' + $ref: '#/components/schemas/ReviewsResponse' + '400': + description: Invalid input data + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } + '409': + description: User has already reviewed this sneaker + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } '404': description: Sneaker not found content: @@ -792,15 +802,21 @@ components: _id: type: string readOnly: true + sneakerId: + type: string + readOnly: true # The saved sneakerId is the one in the param + userId: + type: string rating: type: integer minimum: 1 maximum: 5 comment: type: string - userId: + date: type: string - required: [rating] + format: date-time + required: [sneakerId, rating, userId] UserInput: type: object @@ -855,6 +871,19 @@ components: enum: [success] required: [items, message, status] + ReviewsResponse: + type: object + properties: + items: + $ref: '#/components/schemas/Review' + message: + type: string + status: + type: string + enum: [success] + required: [items, message, status] + + Provider: type: object properties: diff --git a/src/controllers/sneakers.ts b/src/controllers/sneakers.ts index a7d25c1..511f7a4 100644 --- a/src/controllers/sneakers.ts +++ b/src/controllers/sneakers.ts @@ -1,3 +1,4 @@ +import { Review } from "../schemas/review"; import { Sneaker } from "../schemas/sneaker"; import { getCurrencyRate } from "./utils/currency"; @@ -188,4 +189,98 @@ export const deleteSneakerById = async (req, res) => { status: 'failure' }); } -}; \ No newline at end of file +}; + + +export const getSneakerReviews = async (req, res) => { + const { sneakerId } = req.params; + + try { + + const existingSneaker = await Sneaker.findOne({ _id: sneakerId }); + + if (!existingSneaker) { + return res.status(404).json({ + message: 'Sneaker not found', + status: 'failure' + }); + } + + const reviews = await Review.find({ sneakerId }).sort({ date: -1 }); + + if (!reviews || reviews.length === 0) { + return res.status(404).json({ + message: 'No reviews available for this sneaker', + status: 'failure' + }); + } + + return res.status(200).json(reviews); + + } catch (error) { + + return res.status(500).json({ + message: 'Error fetching reviews', + status: 'failure' + }); + } +}; + + + +export const createSneakerReview = async (req, res) => { + const { sneakerId } = req.params; + const { rating, comment, userId } = req.body; + + try { + const existingSneaker = await Sneaker.findOne({ _id: sneakerId }); + + if (!existingSneaker) { + return res.status(404).json({ + message: 'Sneaker not found', + status: 'failure' + }); + } + + if (!rating || rating < 1 || rating > 5) { + return res.status(400).json({ + message: 'Rating must be between 1 and 5', + status: 'failure' + }); + } + + const existingReview = await Review.findOne({ sneakerId, userId }); + + if (existingReview) { + return res.status(409).json({ + message: 'You have already reviewed this sneaker', + status: 'failure' + }); + } + + const newReview = new Review({ + sneakerId, + rating, + userId, + comment, + date: new Date() + }) + + await newReview.save(); + + + return res.status(201).json({ + items: newReview, + message: 'Review created successfully', + status: 'success' + }); + + } catch (error) { + + return res.status(500).json({ + message: 'Error creating review', + status: 'failure' + }); + } +} + diff --git a/src/router/sneakers.ts b/src/router/sneakers.ts index 3a1967b..cbc5022 100644 --- a/src/router/sneakers.ts +++ b/src/router/sneakers.ts @@ -4,7 +4,9 @@ import { getSneakerById, createSneaker, updateSneakerById, - deleteSneakerById + deleteSneakerById, + getSneakerReviews, + createSneakerReview } from '../controllers/sneakers'; const sneakersRouter = express.Router(); @@ -13,8 +15,11 @@ const sneakersRouter = express.Router(); sneakersRouter.get('/', getSneakers); sneakersRouter.get('/:sneakerId', getSneakerById); +sneakersRouter.get('/:sneakerId/reviews', getSneakerReviews); + // POST sneakersRouter.post('/', createSneaker); +sneakersRouter.post('/:sneakerId/reviews', createSneakerReview); // PUT sneakersRouter.put('/:sneakerId', updateSneakerById); diff --git a/src/schemas/review.ts b/src/schemas/review.ts new file mode 100644 index 0000000..5c93218 --- /dev/null +++ b/src/schemas/review.ts @@ -0,0 +1,11 @@ +import mongoose from "mongoose" + +const reviewSchema = new mongoose.Schema({ + sneakerId: { type: mongoose.Schema.Types.ObjectId, ref: 'Sneaker', required: true }, + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + rating: { type: Number, min: 0, max: 5, required: true}, + comment: { type: String, minLength: 0, maxLength: 80 }, + date: { type: Date, default: Date.now, required: true } +}) + +export const Review = mongoose.model('Review', reviewSchema); \ No newline at end of file From ab96bc2d79500573008199e8a944bd01e3780c1f Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 17:05:16 +0200 Subject: [PATCH 17/26] feat: add reviews router and controllers --- docs/openapi.yaml | 14 +------ src/controllers/reviews.ts | 75 ++++++++++++++++++++++++++++++++++++++ src/router/index.ts | 3 ++ src/router/reviews.ts | 16 ++++++++ 4 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 src/controllers/reviews.ts create mode 100644 src/router/reviews.ts diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 1fbcd47..59ddbd7 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -250,7 +250,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Review' + $ref: '#/components/schemas/ReviewsResponse' '404': description: Review not found content: @@ -279,12 +279,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Review' - '403': - description: Not authorized to update this review - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } + $ref: '#/components/schemas/ReviewsResponse' '404': description: Review not found content: @@ -304,11 +299,6 @@ paths: responses: '204': description: Review deleted - '403': - description: Not authorized to delete this review - content: - application/json: - schema: { $ref: '#/components/schemas/Error' } '404': description: Review not found content: diff --git a/src/controllers/reviews.ts b/src/controllers/reviews.ts new file mode 100644 index 0000000..708eeeb --- /dev/null +++ b/src/controllers/reviews.ts @@ -0,0 +1,75 @@ +import { Review } from "../schemas/review"; + +export const getReviewById = async (req, res) => { + try { + const { reviewId } = req.params; + const review = await Review.findById(reviewId); + + if (!review) { + return res.status(404).json({ + message: 'Review not found', status: "failure" + }); + } + + return res.status(200).json({ + items: review, + message: "Review successfully updated", + status: "success" + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error getting review', + status: 'failure' + }); + } +} + +export const updateReviewById = async (req, res) => { + try { + const { reviewId } = req.params; + const updatedReview = await Review.findByIdAndUpdate( + reviewId, + req.body, + { new: true } + ); + + if (!updatedReview) { + return res.status(404).json({ message: 'Review not found' }); + } + + return res.status(200).json({ + items: updatedReview, + message: "Store successfully updated", + status: "success" + }); + + } catch (error) { + return res.status(500).json({ + message: 'Error updating review', + status: 'failure' + }); + } +} + +export const deleteReviewById = async (req, res) => { + try { + const { reviewId } = req.params; + const deletedReview = await Review.findByIdAndDelete(reviewId); + + if (!deletedReview) { + return res.status(404).json({ + message: 'Review not found', + status: 'failure' + }); + } + + return res.status(204).send(); + + } catch (error) { + return res.status(500).json({ + message: 'Error deleting review', + status: 'failure' + }); + } +} diff --git a/src/router/index.ts b/src/router/index.ts index 1123910..9400b15 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -2,10 +2,13 @@ import express from 'express'; import sneakersRouter from './sneakers'; import storesRouter from './stores'; import usersRouter from './users'; +import reviewsRouter from './reviews'; + const router = express.Router() router.use('/sneakers', sneakersRouter); router.use('/stores', storesRouter); router.use('/users', usersRouter); +router.use('/reviews', reviewsRouter); export default router; \ No newline at end of file diff --git a/src/router/reviews.ts b/src/router/reviews.ts new file mode 100644 index 0000000..8482509 --- /dev/null +++ b/src/router/reviews.ts @@ -0,0 +1,16 @@ +import express from 'express'; +import { + getReviewById, + updateReviewById, + deleteReviewById +} from '../controllers/reviews'; + +const reviewsRouter = express.Router(); + +reviewsRouter.get('/:reviewId', getReviewById); + +reviewsRouter.put('/:reviewId', updateReviewById); + +reviewsRouter.delete('/:reviewId', deleteReviewById); + +export default reviewsRouter; \ No newline at end of file From 0500fd0dd579137a307bb9ddb92d9efacd98c42d Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 17:07:55 +0200 Subject: [PATCH 18/26] fix(sneakers): add missing try-catch block --- src/controllers/sneakers.ts | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/controllers/sneakers.ts b/src/controllers/sneakers.ts index 511f7a4..211926d 100644 --- a/src/controllers/sneakers.ts +++ b/src/controllers/sneakers.ts @@ -75,26 +75,36 @@ export const getSneakers = async (req, res) => { export const getSneakerById = async (req, res) => { - const { sneakerId } = req.params; + try { + const { sneakerId } = req.params; + + const sneaker = await Sneaker.findOne({ _id: sneakerId }); + + if (!sneaker) { + return res + .status(404) + .json({ + message: 'Sneaker not found', + status: 'failure' + }); + } - const sneaker = await Sneaker.findOne({ _id: sneakerId }); + return res + .status(200) + .json({ + items: sneaker, + message: 'Sneaker data fetched succesfully.', + status: 'success' + }) - if (!sneaker) { + } catch (error) { return res - .status(404) + .status(500) .json({ - message: 'Sneaker not found', + message: 'Error getting sneaker', status: 'failure' }); } - - return res - .status(200) - .json({ - items: sneaker, - message: 'Sneaker data fetched succesfully.', - status: 'success' - }) }; export const createSneaker = async (req, res) => { From 3f3fa8cd1b61d56d3eb1ce10d96fcd851e395f02 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 17:09:00 +0200 Subject: [PATCH 19/26] refactor(stores): change response message for consistency --- src/controllers/stores.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/stores.ts b/src/controllers/stores.ts index 5d2e25a..1732bfe 100644 --- a/src/controllers/stores.ts +++ b/src/controllers/stores.ts @@ -42,7 +42,7 @@ export const getStoreById = async (req, res) => { } catch (error) { return res.status(500).json({ - message: 'Error fetching stores', + message: 'Error getting store', status: 'failure' }); } From ac4135e1c0ef95540b2a7253586c9460f3b96d59 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 17:09:49 +0200 Subject: [PATCH 20/26] refactor(users): change deleteUser controller name, and response message for consistency --- src/controllers/users.ts | 4 ++-- src/router/users.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/users.ts b/src/controllers/users.ts index 8588754..aa7a696 100644 --- a/src/controllers/users.ts +++ b/src/controllers/users.ts @@ -23,7 +23,7 @@ export const getUserById = async (req, res) => { } catch (error) { return res.status(500).json({ - message: 'Error fetching user', + message: 'Error getting user', status: 'failure' }); } @@ -102,7 +102,7 @@ export const updateUser = async (req, res) => { }; -export const deleteUser = async (req, res) => { +export const deleteUserById = async (req, res) => { try { const { id } = req.params; diff --git a/src/router/users.ts b/src/router/users.ts index 937ab1c..99c4f37 100644 --- a/src/router/users.ts +++ b/src/router/users.ts @@ -3,7 +3,7 @@ import { createUser, getUserById, updateUser, - deleteUser, + deleteUserById, getFavorites, addFavorite, removeFavorite @@ -23,7 +23,7 @@ usersRouter.post('/:id/favorites', addFavorite); usersRouter.put('/:id', updateUser); // DELETE -usersRouter.delete('/:id', deleteUser); +usersRouter.delete('/:id', deleteUserById); usersRouter.delete('/:id/favorites/:sneakerId', removeFavorite); export default usersRouter; From 1112b4ebb00566782c26d3da2d15a45f7d6a3825 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 17:17:25 +0200 Subject: [PATCH 21/26] fix(openapi): remove false YAML schema errors in VSCode This is a workaroung for resolving the false 'Value is not accepted. Valid values: true.yaml-schema: OpenAPI 3.0.X(1)' validation errors --- docs/openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 59ddbd7..9a6b1c0 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://json-schema.org/draft/2020-12/schema openapi: 3.0.3 info: title: Snapi (Sneakers API) From 13bb7db3cd01b435be07911ec37cc913f95c5f55 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 17:27:47 +0200 Subject: [PATCH 22/26] docs: update roadmap --- docs/roadmap.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index c327f69..3b6c85b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,16 +5,16 @@ - [x] The API offers a REST interface and allows CRUD operations on the DB - [x] The database is a MongoDB database. - [x] The database is **automatically seeded** on launch if it's empty. - - [ ] At least one message is in XML format and has an associated schema. + - [ ] At least one message is in XML format and has an associated schema. (**optional**) - [x] At least one response is in JSON format - - [ ] There are at least 3 resources and they are related to each other. + - [x] There are at least 3 resources and they are related to each other (sneakers, users, reviews). - [x] One of the collections has at least **1000 documents** - [x] There is a **dataset** to seed this collection in the repository ( -> this dataset is in **JSON format**) - [ ] At least one route allows pagination - [x] At least one route allows filtering data to search inside this collection - [x] Uses an external API - - [ ] At least one consumed message is in XML format + - [ ] At least one consumed message is in XML format (**optional**) - [x] At least one consumed message is in JSON format - [x] The consumed information is integrated with our API and data is saved in our DB. - [x] The API keeps working even if the external API is down. From c274b1d75620449e02597fe9990cafe89901f48e Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 18:07:54 +0200 Subject: [PATCH 23/26] feat(providers): seed data on launch --- src/config/database.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/config/database.ts b/src/config/database.ts index dd11428..f1a5e34 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -4,11 +4,12 @@ import dotenv from 'dotenv'; import { Sneaker } from "../schemas/sneaker"; import { Currency } from "../schemas/currency"; import { Store } from "../schemas/store"; - +import { Provider } from "../schemas/provider"; import sneakerSeedData from "../../seeds/sneakers/sneakers.json"; import currencySeedData from "../../seeds/currencies.json"; import storeSeedData from "../../seeds/stores/stores.json"; +import providerSeedData from "../../seeds/providers/providers.json"; dotenv.config({ path: '../../.env' }); @@ -29,7 +30,8 @@ export async function seedDatabase() { await Promise.all([ seedSneakers(), seedCurrencies(), - seedStores() + seedStores(), + seedProviders() ]); } catch (error) { console.error("Seeding failed:", error); @@ -72,3 +74,15 @@ async function seedStores() { await Store.insertMany(storeSeedData); console.log(`Seeded ${storeSeedData.length} documents in 'stores'`); } + +async function seedProviders() { + const providersCount = await Provider.countDocuments(); + + if (providersCount > 0) { + console.log(`Found ${providersCount} documents in 'providers' - skipping seed`); + return; + } + + await Provider.insertMany(providerSeedData); + console.log(`Seeded ${providerSeedData.length} documents in 'providers'`); +} From f5361738ad8bd9862b08673e2f1380d7f15860c0 Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 18:09:29 +0200 Subject: [PATCH 24/26] fix(providers): remove _id from mongoose schema fields --- src/schemas/provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/schemas/provider.ts b/src/schemas/provider.ts index abc416a..162f80a 100644 --- a/src/schemas/provider.ts +++ b/src/schemas/provider.ts @@ -1,7 +1,6 @@ import mongoose from "mongoose"; const providerSchema = new mongoose.Schema({ - _id: { type: String }, name: { type: String, required: true }, contact_email: { type: String, required: true} }); From 31c1da933531f3e9d3707ca949edf4eee67a428f Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 18:09:57 +0200 Subject: [PATCH 25/26] feat(providers): add missing responsse on updateProviderById controller --- src/controllers/provider.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/provider.ts b/src/controllers/provider.ts index c544428..bbd3283 100644 --- a/src/controllers/provider.ts +++ b/src/controllers/provider.ts @@ -94,6 +94,13 @@ export const updateProviderById = async (req, res) => { }); } + if (!updateData.name || !updateData.contact_email) { + return res.status(400).json({ + message: 'Invalid input data', + status: 'failure' + }); + } + return res.status(200).json({ items: provider, message: 'Provider updated successfully', From 737462ed2f9379ef0c927c05672d9d4673e63e5a Mon Sep 17 00:00:00 2001 From: khalidbelk Date: Tue, 20 May 2025 18:13:18 +0200 Subject: [PATCH 26/26] fix(openapi): update openapi to match providers responses --- docs/openapi.yaml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 58aa363..86f2b6a 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -318,7 +318,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UserInput' + $ref: '#/components/schemas/User' responses: '201': description: User created @@ -511,9 +511,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Provider' + $ref: '#/components/schemas/Provider' '404': description: No providers found content: @@ -536,7 +534,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Provider' + $ref: '#/components/schemas/ProvidersResponse' '400': description: Invalid input data content: @@ -560,7 +558,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Provider' + $ref: '#/components/schemas/ProvidersResponse' '404': description: Provider not found content: @@ -589,7 +587,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Provider' + $ref: '#/components/schemas/ProvidersResponse' '400': description: Invalid input data content: @@ -868,6 +866,18 @@ components: format: email required: [name, contact_email] + ProvidersResponse: + type: object + properties: + items: + $ref: '#/components/schemas/Provider' + message: + type: string + status: + type: string + enum: [success] + required: [items, message, status] + Store: type: object properties: