Skip to content

Commit 569dc92

Browse files
committed
Musterlösung Aufgabe Product API
1 parent be09b39 commit 569dc92

File tree

6 files changed

+258
-3
lines changed

6 files changed

+258
-3
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
11
import express from "express";
2+
import buildProductRouter from "./router/product";
3+
import { InMemoryProductRepository } from "./repository/in-memory-product-repository";
4+
import type { Product } from "./types/product";
5+
6+
const sampleData = [
7+
{
8+
id: crypto.randomUUID(),
9+
name: "Laptop Pro X",
10+
description: "Leistungsstarker Laptop für professionelle Anwendungen.",
11+
price: 1200.0,
12+
currency: "EUR",
13+
},
14+
{
15+
id: crypto.randomUUID(),
16+
name: "Gaming Maus XYZ",
17+
description: "Ergonomische Maus für Gamer mit anpassbaren Tasten.",
18+
price: 65.99,
19+
currency: "EUR",
20+
},
21+
{
22+
id: crypto.randomUUID(),
23+
name: "UHD Monitor 27 Zoll",
24+
description: "4K-Monitor mit hoher Farbtreue.",
25+
price: 350.0,
26+
currency: "USD",
27+
},
28+
{
29+
id: crypto.randomUUID(),
30+
name: "Smartphone Z10",
31+
description: "Neuestes Smartphone mit fortschrittlicher Kamera.",
32+
price: 899.99,
33+
currency: "GBP",
34+
},
35+
] satisfies Product[];
236

337
const app = express();
438

5-
app.get("/", (_, res) => {
39+
app.use(express.json()); // Middleware zum Parsen von JSON-Daten
40+
41+
const productRepository = new InMemoryProductRepository(sampleData);
42+
const productRouter = buildProductRouter(productRepository);
43+
app.use("/products", productRouter);
44+
45+
app.get("/", (_, res) => {
646
res.send("Hello express");
747
});
848

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Product, CreateProductDto, UpdateProductDto } from "../types/product";
2+
import { ProductRepository } from "./product-repository";
3+
4+
export class InMemoryProductRepository implements ProductRepository {
5+
constructor(private products: Product[] = []) {}
6+
7+
findAll(): Product[] {
8+
return this.products;
9+
}
10+
11+
findAllFiltered(filters: {
12+
name?: string;
13+
price?: number;
14+
currency?: string;
15+
}): Product[] {
16+
let filteredProducts = this.products;
17+
18+
if (filters.name) {
19+
const searchName = filters.name.toLowerCase();
20+
filteredProducts = filteredProducts.filter((product) =>
21+
product.name.toLowerCase().includes(searchName)
22+
);
23+
}
24+
25+
if (filters.price) {
26+
filteredProducts = filteredProducts.filter(
27+
(product) => product.price === filters.price
28+
);
29+
}
30+
31+
if (filters.currency) {
32+
filteredProducts = filteredProducts.filter(
33+
(product) => product.currency === filters.currency
34+
);
35+
}
36+
37+
return filteredProducts;
38+
}
39+
40+
findById(id: string): Product | undefined {
41+
return this.products.find((p) => p.id === id);
42+
}
43+
44+
create(productDto: CreateProductDto): Product {
45+
const newProduct: Product = {
46+
id: crypto.randomUUID(),
47+
...productDto,
48+
};
49+
this.products.push(newProduct);
50+
return newProduct;
51+
}
52+
53+
update(id: string, updateData: UpdateProductDto): Product | undefined {
54+
const productIndex = this.products.findIndex((p) => p.id === id);
55+
if (productIndex === -1) return undefined;
56+
57+
this.products[productIndex] = {
58+
...this.products[productIndex],
59+
...updateData,
60+
};
61+
return this.products[productIndex];
62+
}
63+
64+
delete(id: string): boolean {
65+
const initialLength = this.products.length;
66+
this.products = this.products.filter((p) => p.id !== id);
67+
return this.products.length !== initialLength;
68+
}
69+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Product, CreateProductDto, UpdateProductDto } from "../types/product";
2+
3+
export interface ProductRepository {
4+
findAll(): Product[];
5+
findAllFiltered(filters: {
6+
name?: string;
7+
price?: number;
8+
currency?: string;
9+
}): Product[];
10+
findById(id: string): Product | undefined;
11+
create(product: CreateProductDto): Product;
12+
update(id: string, product: UpdateProductDto): Product | undefined;
13+
delete(id: string): boolean;
14+
}

src/router/product.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import express, { Request, Response } from "express";
2+
import { CreateProductDto, UpdateProductDto } from "../types/product";
3+
import { ProductRepository } from "../repository/product-repository";
4+
5+
const buildProductRouter = (productRepository: ProductRepository) => {
6+
const router = express.Router();
7+
8+
/**
9+
* GET /
10+
* Gibt eine Liste aller Produkte zurück.
11+
* Optional können Filter nach Name, Preis und Währung angewendet werden.
12+
*/
13+
router.get("/", (req: Request, res: Response) => {
14+
const { name, price, currency } = req.query;
15+
16+
const parsedPrice =
17+
typeof price === "string" ? parseFloat(price) : undefined;
18+
const filters = {
19+
name: typeof name === "string" ? name : undefined,
20+
price:
21+
parsedPrice !== undefined && !isNaN(parsedPrice)
22+
? parsedPrice
23+
: undefined,
24+
currency: typeof currency === "string" ? currency : undefined,
25+
};
26+
27+
const products = productRepository.findAllFiltered(filters);
28+
res.status(200).json(products);
29+
});
30+
31+
/**
32+
* GET /:id
33+
* Gibt ein bestimmtes Produkt anhand seiner ID zurück.
34+
*/
35+
router.get("/:id", (req: Request, res: Response) => {
36+
const { id } = req.params;
37+
const product = productRepository.findById(id);
38+
39+
if (!product) {
40+
res.status(404).json({ message: "Product not found" });
41+
return;
42+
}
43+
44+
res.status(200).json(product);
45+
});
46+
47+
/**
48+
* POST /
49+
* Erstellt ein neues Produkt.
50+
* Erfordert name, description, price und currency im Request Body.
51+
*/
52+
router.post("/", (req: Request, res: Response) => {
53+
const productDto = req.body as CreateProductDto;
54+
55+
if (
56+
!productDto.name ||
57+
!productDto.description ||
58+
!productDto.price ||
59+
!productDto.currency
60+
) {
61+
res.status(400).json({
62+
message: "Missing required fields: name, description, price, currency",
63+
});
64+
return;
65+
}
66+
67+
if (typeof productDto.price !== "number" || productDto.price <= 0) {
68+
res.status(400).json({ message: "Price must be a positive number" });
69+
return;
70+
}
71+
72+
const newProduct = productRepository.create(productDto);
73+
res.status(201).json(newProduct);
74+
});
75+
76+
/**
77+
* PUT /:id
78+
* Aktualisiert ein bestehendes Produkt anhand seiner ID.
79+
* Erlaubt die Aktualisierung von name, description, price, currency.
80+
*/
81+
router.put("/:id", ({ params: { id }, body }: Request, res: Response) => {
82+
const updateData = body as UpdateProductDto;
83+
84+
if (
85+
updateData.price !== undefined &&
86+
(typeof updateData.price !== "number" || updateData.price <= 0)
87+
) {
88+
res.status(400).json({ message: "Price must be a positive number" });
89+
return;
90+
}
91+
92+
const updatedProduct = productRepository.update(id, updateData);
93+
if (!updatedProduct) {
94+
res.status(404).json({ message: "Product not found" });
95+
} else {
96+
res.status(200).json(updatedProduct);
97+
}
98+
});
99+
100+
/**
101+
* DELETE /:id
102+
* Löscht ein Produkt anhand seiner ID.
103+
*/
104+
router.delete("/:id", (req: Request, res: Response) => {
105+
const { id } = req.params;
106+
const deleted = productRepository.delete(id);
107+
108+
if (!deleted) {
109+
res.status(404).json({ message: "Product not found" });
110+
} else {
111+
res.status(204).send(); // 204 No Content für erfolgreiches Löschen
112+
}
113+
});
114+
115+
return router;
116+
};
117+
118+
export default buildProductRouter;

src/types/product.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type Currency = "EUR" | "USD" | "GBP"; // Unterstützte Währungen als Union Type
2+
3+
export type Product = {
4+
id: string; // Eindeutige ID des Produkts
5+
name: string; // Name des Produkts
6+
description: string; // Beschreibung des Produkts
7+
price: number; // Preis des Produkts
8+
currency: Currency; // Währung des Preises
9+
};
10+
11+
// Produkt-Typ ohne ID (wird bei Erstellung vergeben)
12+
export type CreateProductDto = Omit<Product, "id">;
13+
// Produkt-Typ für Aktualisierungen (alle Felder bis auf id optional)
14+
export type UpdateProductDto = Partial<CreateProductDto> & { id: string };

0 commit comments

Comments
 (0)