Skip to content
Merged

Dev #15

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -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)
60 changes: 60 additions & 0 deletions docs/api-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## API Overview

### API Architecture

<div align="center">
<img src="assets/api-diagram.png" alt="API Diagram" width="50%">
</div>

> 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:**

<img src="assets/data-models/providers.png" alt="Providers Model" width="40%">

**Sneakers Collection:**

<img src="assets/data-models/sneakers.png" alt="Sneakers Model" width="40%">

**Reviews Collection:**

<img src="assets/data-models/reviews.png" alt="Reviews Model" width="40%">

(references Users and Sneakers documents)

**Currencies Collection:**

<img src="assets/data-models/currencies.png" alt="Currencies Model" width="40%">

**Stores Collection:**

<img src="assets/data-models/stores.png" alt="Stores Model" width="40%">

**Users Collection:**

<img src="assets/data-models/users.png" alt="Users Model" width="40%">

Binary file added docs/assets/api-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/data-models/currencies.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/data-models/providers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/data-models/reviews.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/data-models/sneakers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/data-models/stores.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/data-models/users.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
15 changes: 15 additions & 0 deletions docs/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
56 changes: 42 additions & 14 deletions src/controllers/sneakers.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
};

Expand Down
12 changes: 12 additions & 0 deletions src/controllers/utils/xml.ts
Original file line number Diff line number Diff line change
@@ -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
};
}