diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2687d48 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +## Documentation + +- **OpenAPI / Swagger document**: [here](/docs/openapi.yaml). +- **API Architecture & Data Models**: [here](/docs/api-overview.md) +- Overview of our **REST-compliant API routes**: [here](/docs/routes.md) +- **Currencies** (external API) data: [here](/docs/currencies.md) + +Non-technical (project management) + +- **Roadmap** of requirements: [here](/docs/roadmap.md) \ No newline at end of file diff --git a/docs/api-overview.md b/docs/api-overview.md new file mode 100644 index 0000000..1f1ffb6 --- /dev/null +++ b/docs/api-overview.md @@ -0,0 +1,60 @@ +## API Overview + +### API Architecture + +
+ API Diagram +
+ +> Diagram made with [eraser.io](https://eraser.io) + +Our API architecture consists of three main components: + +1. **Node.js Server**: Handles REST endpoints and processes requests: + - Performs CRUD operations on the MongoDB database + - Fetches and stores data from external API + - Exposes RESTful endpoints for client applications + +2. **MongoDB Database**: Stores and manages application data: + - Maintains collections for core entities + - Persists data retrieved from external API + +3. **External API**: Third-party service that provides additional data (**currencies exchange rates**) + +This architecture ensures efficient data management and separation of concerns while maintaining data persistence. + +### Data Models + +Our **MongoDB** database is structured with 6 collections: + +- 5 collections for core API entities +- 1 collection for storing external API data + +Below you can find a detailed overview of our data models and their relationships: + +**Providers Collection:** + +Providers Model + +**Sneakers Collection:** + +Sneakers Model + +**Reviews Collection:** + +Reviews Model + +(references Users and Sneakers documents) + +**Currencies Collection:** + +Currencies Model + +**Stores Collection:** + +Stores Model + +**Users Collection:** + +Users Model + diff --git a/docs/assets/api-diagram.png b/docs/assets/api-diagram.png new file mode 100644 index 0000000..d4b227c Binary files /dev/null and b/docs/assets/api-diagram.png differ diff --git a/docs/assets/data-models/currencies.png b/docs/assets/data-models/currencies.png new file mode 100644 index 0000000..ad446d5 Binary files /dev/null and b/docs/assets/data-models/currencies.png differ diff --git a/docs/assets/data-models/providers.png b/docs/assets/data-models/providers.png new file mode 100644 index 0000000..8e61bab Binary files /dev/null and b/docs/assets/data-models/providers.png differ diff --git a/docs/assets/data-models/reviews.png b/docs/assets/data-models/reviews.png new file mode 100644 index 0000000..803ae8d Binary files /dev/null and b/docs/assets/data-models/reviews.png differ diff --git a/docs/assets/data-models/sneakers.png b/docs/assets/data-models/sneakers.png new file mode 100644 index 0000000..8bc67ec Binary files /dev/null and b/docs/assets/data-models/sneakers.png differ diff --git a/docs/assets/data-models/stores.png b/docs/assets/data-models/stores.png new file mode 100644 index 0000000..fa8970b Binary files /dev/null and b/docs/assets/data-models/stores.png differ diff --git a/docs/assets/data-models/users.png b/docs/assets/data-models/users.png new file mode 100644 index 0000000..68a2a01 Binary files /dev/null and b/docs/assets/data-models/users.png differ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index fabb6aa..db57d80 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -199,6 +199,13 @@ paths: required: true schema: type: string + - name: format + in: query + required: false + schema: + type: string + enum: [json, xml] + description: Set to 'xml' to receive the response in XML format. Defaults to JSON. responses: '200': description: Successful response diff --git a/docs/roadmap.md b/docs/roadmap.md index ac4bf51..d3f2460 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,7 +5,7 @@ - [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. (**optional**) + - [x] At least one message is in XML format and has an associated schema. (**optional**) - [x] At least one response is in JSON format - [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** diff --git a/docs/routes.md b/docs/routes.md index ab50666..40ae580 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -49,6 +49,21 @@ sneakers?currency=eur&limit=2&release_date_after=2021-10-22 **GET** /sneakers/{sneakerId}/reviews : Get all reviews for a specific sneaker +Query Parameters: +- `format`: Response format (optional, defaults to JSON) + - Type: string + Allowed values: **JSON, XML** + +Examples: + +``` +# Get all reviews for sneaker with ID 682b89e24d2ea61f1a8ab86f (JSON format) +/sneakers/682b89e24d2ea61f1a8ab86f/reviews + +# Get all reviews for sneaker with ID 682b89e24d2ea61f1a8ab86f (XML format) +/sneakers/682b89e24d2ea61f1a8ab86f/reviews?format=xml +``` + **POST** /sneakers/{sneakerId}/reviews : Create review for a sneaker **GET** /reviews/{reviewId} : Get a specific review diff --git a/package-lock.json b/package-lock.json index 17633f3..a53bd7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^4.21.2", + "js2xmlparser": "^5.0.0", "mongoose": "^8.15.0", "ts-node": "^10.9.2" }, @@ -1100,6 +1101,15 @@ "node": ">=0.12.0" } }, + "node_modules/js2xmlparser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-5.0.0.tgz", + "integrity": "sha512-ckXs0Fzd6icWurbeAXuqo+3Mhq2m8pOPygsQjTPh8K5UWgKaUgDSHrdDxAfexmT11xvBKOQ6sgYwPkYc5RW/bg==", + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2047,6 +2057,12 @@ "dev": true, "license": "ISC" }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "license": "Apache-2.0" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2d120c6..1c38fe0 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^4.21.2", + "js2xmlparser": "^5.0.0", "mongoose": "^8.15.0", "ts-node": "^10.9.2" } diff --git a/src/controllers/sneakers.ts b/src/controllers/sneakers.ts index 24355d2..778a2ac 100644 --- a/src/controllers/sneakers.ts +++ b/src/controllers/sneakers.ts @@ -1,7 +1,11 @@ +import { parse } from "js2xmlparser"; + import { Review } from "../schemas/review"; import { Sneaker } from "../schemas/sneaker"; import { getCurrencyRate } from "./utils/currency"; +import { cleanReviewForXml } from "./utils/xml"; + export const getSneakers = async (req, res) => { try { const { release_date_after, limit = 20, offset = 0, currency } = req.query; @@ -219,39 +223,63 @@ export const deleteSneakerById = async (req, res) => { export const getSneakerReviews = async (req, res) => { const { sneakerId } = req.params; + const { format } = req.query; try { - const existingSneaker = await Sneaker.findOne({ _id: sneakerId }); if (!existingSneaker) { - return res.status(404).json({ + const errorObj = { message: 'Sneaker not found', status: 'failure' - }); + }; + + if (format === 'xml') { + res.set('Content-Type', 'application/xml'); + return res.status(404).send(parse("error", errorObj)); + } + + return res.status(404).json(errorObj); } - const reviews = await Review.find({ sneakerId }).select('-__v').sort({ date: -1 }); + const reviews = await Review.find({ sneakerId }).sort({ date: -1 }); if (!reviews || reviews.length === 0) { - return res.status(404).json({ + const errorObj = { message: 'No reviews available for this sneaker', status: 'failure' - }); + }; + + if (format === 'xml') { + res.set('Content-Type', 'application/xml'); + return res.status(404).send(parse("error", errorObj)); + } + + return res.status(404).json(errorObj); } - return res.status(200).json({ - items: reviews, - message: 'Reviews data fetched successfully', - status: 'success' - }); + if (format === 'xml') { + res.set('Content-Type', 'application/xml'); + const cleanedReviews = reviews.map(r => cleanReviewForXml(r.toObject())); - } catch (error) { + return res.status(200) + .send(parse("reviews", { review: cleanedReviews })); + } - return res.status(500).json({ + return res.status(200).json(reviews); + + } catch (error) { + const errorObj = { message: 'Error fetching reviews', status: 'failure' - }); + }; + + if (req.query.format === 'xml') { + res.set('Content-Type', 'application/xml'); + return res.status(500).send(parse("error", errorObj)); + } + + return res.status(500).json(errorObj); } }; diff --git a/src/controllers/utils/xml.ts b/src/controllers/utils/xml.ts new file mode 100644 index 0000000..1ee3f47 --- /dev/null +++ b/src/controllers/utils/xml.ts @@ -0,0 +1,12 @@ + +export const cleanReviewForXml = (review) => { + return { + _id: review._id?.toString(), + sneakerId: review.sneakerId?.toString(), + userId: review.userId?.toString(), + rating: review.rating, + comment: review.comment, + date: review.date instanceof Date ? review.date.toISOString() : review.date + }; +} +