For Frontend practice
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
DATABASE_URL=postgresql://webstore_user:webstore_password@localhost:5432/webstore_db?schema=public
|
||||||
|
JWT_SECRET=replace-with-a-long-random-secret
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
COOKIE_NAME=webstore_token
|
||||||
|
COOKIE_SECURE=false
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
npm-debug.log*
|
||||||
|
public/images/
|
||||||
|
!public/images/.gitkeep
|
||||||
|
prisma/dev.db
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN apk add --no-cache openssl && npm install --ignore-scripts
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run prisma:generate
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Webstore Backend (Laxer + CQRS + Prisma)
|
||||||
|
|
||||||
|
Educational Node.js backend for frontend students.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
prisma/
|
||||||
|
schema.prisma
|
||||||
|
seed.js
|
||||||
|
public/
|
||||||
|
images/
|
||||||
|
scripts/
|
||||||
|
extract-images-from-build.bat
|
||||||
|
src/
|
||||||
|
Api/
|
||||||
|
Controller/
|
||||||
|
Router/
|
||||||
|
Middleware/
|
||||||
|
app.js
|
||||||
|
server.js
|
||||||
|
Application/
|
||||||
|
User/
|
||||||
|
Command/
|
||||||
|
command.js
|
||||||
|
commandhandler.js
|
||||||
|
Querry/
|
||||||
|
query.js
|
||||||
|
queryhandler.js
|
||||||
|
Services/
|
||||||
|
DTO/
|
||||||
|
Domain/
|
||||||
|
IRepository/
|
||||||
|
Models/
|
||||||
|
Infrastructure/
|
||||||
|
Repository/
|
||||||
|
config/
|
||||||
|
env.js
|
||||||
|
prisma.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts PostgreSQL and API, runs Prisma generate/db push/seed automatically.
|
||||||
|
|
||||||
|
## Auth (JWT in Cookie)
|
||||||
|
|
||||||
|
- `POST /api/users/register`
|
||||||
|
- `POST /api/users/login`
|
||||||
|
- `GET /api/users/me` (requires cookie)
|
||||||
|
- `POST /api/users/logout` (requires cookie)
|
||||||
|
|
||||||
|
Cookie is HTTP-only and set with `sameSite=lax`.
|
||||||
|
|
||||||
|
## Shop endpoints
|
||||||
|
|
||||||
|
- `GET /api/health`
|
||||||
|
- `GET /api/shop/categories`
|
||||||
|
- `GET /api/shop/products`
|
||||||
|
- `POST /api/shop/orders`
|
||||||
|
|
||||||
|
Order payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"customer_name": "Jane Doe",
|
||||||
|
"customer_email": "jane@example.com",
|
||||||
|
"items": [
|
||||||
|
{ "product_id": 1, "quantity": 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image extraction batch
|
||||||
|
|
||||||
|
```bat
|
||||||
|
scripts\extract-images-from-build.bat "..\build" ".\public\images"
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults (if arguments omitted):
|
||||||
|
|
||||||
|
- Source: `..\build`
|
||||||
|
- Target: `.\public\images`
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: webstore_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: webstore_db
|
||||||
|
POSTGRES_USER: webstore_user
|
||||||
|
POSTGRES_PASSWORD: webstore_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: webstore_api
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
NODE_ENV: development
|
||||||
|
DATABASE_URL: postgresql://webstore_user:webstore_password@db:5432/webstore_db?schema=public
|
||||||
|
JWT_SECRET: dev-secret-change-me
|
||||||
|
JWT_EXPIRES_IN: 7d
|
||||||
|
COOKIE_NAME: webstore_token
|
||||||
|
COOKIE_SECURE: "false"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
command: sh -c "npm run prisma:generate && npm run prisma:push && npm run prisma:seed && npm run dev"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Copy and edit values for your production/student environment.
|
||||||
|
|
||||||
|
# API
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=production
|
||||||
|
DATABASE_URL=postgresql://webstore_user:webstore_password@db:5432/webstore_db?schema=public
|
||||||
|
JWT_SECRET=change-me-in-real-use
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
COOKIE_NAME=webstore_token
|
||||||
|
COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Database container bootstrap
|
||||||
|
POSTGRES_DB=webstore_db
|
||||||
|
POSTGRES_USER=webstore_user
|
||||||
|
POSTGRES_PASSWORD=webstore_password
|
||||||
|
|
||||||
|
# Optional: host port overrides
|
||||||
|
API_PORT=3000
|
||||||
|
DB_PORT=5432
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: webstore_db_prod
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data_prod:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: webstore-api:prod
|
||||||
|
container_name: webstore_api_prod
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
env_file:
|
||||||
|
- .production.env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
PORT: ${PORT}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN}
|
||||||
|
COOKIE_NAME: ${COOKIE_NAME}
|
||||||
|
COOKIE_SECURE: ${COOKIE_SECURE}
|
||||||
|
command: sh -c "npm run prisma:generate && npm run prisma:push && npm run prisma:seed && npm start"
|
||||||
|
ports:
|
||||||
|
- "${API_PORT:-3000}:3000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data_prod:
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions EnableDelayedExpansion
|
||||||
|
|
||||||
|
set "ROOT_DIR=%~dp0"
|
||||||
|
set "ENV_FILE=%ROOT_DIR%\.production.env"
|
||||||
|
set "COMPOSE_FILE=%ROOT_DIR%\production.compose.yml"
|
||||||
|
set "ARCHIVE_FILE=%ROOT_DIR%\images\webstore-production-images.tar"
|
||||||
|
|
||||||
|
set "API_IMAGE=webstore-api:prod"
|
||||||
|
|
||||||
|
echo [1/7] Checking Docker + Compose...
|
||||||
|
docker --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Docker CLI is not available.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
docker compose version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Docker Compose plugin is not available.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/7] Validating required files...
|
||||||
|
if not exist "%ENV_FILE%" (
|
||||||
|
echo ERROR: Missing env file: "%ENV_FILE%"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
if not exist "%COMPOSE_FILE%" (
|
||||||
|
echo ERROR: Missing compose file: "%COMPOSE_FILE%"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [3/7] Loading prebuilt image archive if present...
|
||||||
|
if exist "%ARCHIVE_FILE%" (
|
||||||
|
docker load -i "%ARCHIVE_FILE%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to load image archive.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo WARN: "%ARCHIVE_FILE%" not found. Assuming images already exist locally.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [4/7] Ensuring API image exists locally...
|
||||||
|
docker image inspect "%API_IMAGE%" >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: API image "%API_IMAGE%" not found.
|
||||||
|
echo Run scripts\build-production-images.bat first or provide the archive in images\.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [5/7] Starting production stack for students...
|
||||||
|
pushd "%ROOT_DIR%" >nul
|
||||||
|
docker compose --env-file .production.env -f production.compose.yml up -d
|
||||||
|
if errorlevel 1 (
|
||||||
|
popd >nul
|
||||||
|
echo ERROR: Failed to start compose stack.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/7] Showing running services...
|
||||||
|
docker compose --env-file .production.env -f production.compose.yml ps
|
||||||
|
|
||||||
|
echo [7/7] Done.
|
||||||
|
echo API should be available at http://localhost:3000 (or API_PORT in .production.env)
|
||||||
|
echo Stop stack with:
|
||||||
|
echo docker compose --env-file .production.env -f production.compose.yml down
|
||||||
|
popd >nul
|
||||||
|
exit /b 0
|
||||||
Binary file not shown.
Generated
+1534
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "webstore-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Teaching backend for a webstore using Node.js, Express, and PostgreSQL",
|
||||||
|
"main": "src/Api/server.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon src/Api/server.js",
|
||||||
|
"start": "node src/Api/server.js",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:push": "prisma db push",
|
||||||
|
"prisma:seed": "node prisma/seed.js",
|
||||||
|
"postinstall": "prisma generate"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"express",
|
||||||
|
"postgres",
|
||||||
|
"webstore",
|
||||||
|
"backend"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"@prisma/client": "^5.20.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.4",
|
||||||
|
"prisma": "^5.20.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
email String @unique
|
||||||
|
passwordHash String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Category {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String @unique
|
||||||
|
slug String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
products Product[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Product {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
categoryId Int
|
||||||
|
name String @unique
|
||||||
|
description String?
|
||||||
|
price Decimal @db.Decimal(10, 2)
|
||||||
|
imageUrl String?
|
||||||
|
stock Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
|
||||||
|
orderItems OrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Order {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
customerName String
|
||||||
|
customerEmail String
|
||||||
|
totalPrice Decimal @db.Decimal(10, 2)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
items OrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model OrderItem {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
orderId Int
|
||||||
|
productId Int
|
||||||
|
quantity Int
|
||||||
|
unitPrice Decimal @db.Decimal(10, 2)
|
||||||
|
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const { PrismaClient } = require("@prisma/client");
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const categories = [
|
||||||
|
{ name: "Shoes", slug: "shoes" },
|
||||||
|
{ name: "Bags", slug: "bags" },
|
||||||
|
{ name: "Accessories", slug: "accessories" }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
await prisma.category.upsert({
|
||||||
|
where: { slug: category.slug },
|
||||||
|
update: {},
|
||||||
|
create: category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shoes = await prisma.category.findUnique({ where: { slug: "shoes" } });
|
||||||
|
const bags = await prisma.category.findUnique({ where: { slug: "bags" } });
|
||||||
|
const accessories = await prisma.category.findUnique({ where: { slug: "accessories" } });
|
||||||
|
|
||||||
|
const products = [
|
||||||
|
{
|
||||||
|
categoryId: shoes.id,
|
||||||
|
name: "Street Runner",
|
||||||
|
description: "Lightweight city sneaker.",
|
||||||
|
price: "18990.00",
|
||||||
|
imageUrl: "/images/street-runner.jpg",
|
||||||
|
stock: 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryId: shoes.id,
|
||||||
|
name: "Trail Edge",
|
||||||
|
description: "Stable shoe for outdoor tracks.",
|
||||||
|
price: "24990.00",
|
||||||
|
imageUrl: "/images/trail-edge.jpg",
|
||||||
|
stock: 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryId: bags.id,
|
||||||
|
name: "Urban Tote",
|
||||||
|
description: "Everyday tote with zipper top.",
|
||||||
|
price: "14990.00",
|
||||||
|
imageUrl: "/images/urban-tote.jpg",
|
||||||
|
stock: 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryId: accessories.id,
|
||||||
|
name: "Classic Cap",
|
||||||
|
description: "Adjustable cotton cap.",
|
||||||
|
price: "6990.00",
|
||||||
|
imageUrl: "/images/classic-cap.jpg",
|
||||||
|
stock: 42
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
await prisma.product.upsert({
|
||||||
|
where: { name: product.name },
|
||||||
|
update: {},
|
||||||
|
create: product
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
console.log("Prisma seed completed.");
|
||||||
|
})
|
||||||
|
.catch(async (error) => {
|
||||||
|
console.error(error);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions EnableDelayedExpansion
|
||||||
|
|
||||||
|
set "ROOT_DIR=%~dp0.."
|
||||||
|
set "IMAGE_OUT_DIR=%ROOT_DIR%\images"
|
||||||
|
set "ARCHIVE_FILE=%IMAGE_OUT_DIR%\webstore-production-images.tar"
|
||||||
|
|
||||||
|
set "API_IMAGE=webstore-api:prod"
|
||||||
|
set "DB_IMAGE=postgres:16-alpine"
|
||||||
|
|
||||||
|
echo [1/6] Checking Docker availability...
|
||||||
|
docker --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Docker CLI is not available. Install Docker Desktop first.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/6] Building production API image: %API_IMAGE%
|
||||||
|
pushd "%ROOT_DIR%" >nul
|
||||||
|
docker build -t "%API_IMAGE%" -f Dockerfile .
|
||||||
|
if errorlevel 1 (
|
||||||
|
popd >nul
|
||||||
|
echo ERROR: Failed to build API image.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
popd >nul
|
||||||
|
|
||||||
|
echo [3/6] Pulling database image: %DB_IMAGE%
|
||||||
|
docker pull "%DB_IMAGE%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to pull database image.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [4/6] Preparing image output directory: %IMAGE_OUT_DIR%
|
||||||
|
if not exist "%IMAGE_OUT_DIR%" mkdir "%IMAGE_OUT_DIR%"
|
||||||
|
|
||||||
|
echo [5/6] Exporting images to archive...
|
||||||
|
if exist "%ARCHIVE_FILE%" del /f /q "%ARCHIVE_FILE%"
|
||||||
|
docker save -o "%ARCHIVE_FILE%" "%API_IMAGE%" "%DB_IMAGE%"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to export Docker images.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/6] Done.
|
||||||
|
echo Created archive: "%ARCHIVE_FILE%"
|
||||||
|
echo Share this file with students together with production.compose.yml and .production.env.
|
||||||
|
exit /b 0
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const GetCategoriesQuery = require("../../Application/Shop/Query/GetCategoriesQuery");
|
||||||
|
const GetProductsQuery = require("../../Application/Shop/Query/GetProductsQuery");
|
||||||
|
const CreateOrderCommand = require("../../Application/Shop/Command/CreateOrderCommand");
|
||||||
|
const CreateProductCommand = require("../../Application/Shop/Command/CreateProductCommand");
|
||||||
|
const container = require("../../Infrastructure/DI/container");
|
||||||
|
|
||||||
|
const getCategoriesHandler = container.resolve("GetCategoriesQueryHandler");
|
||||||
|
const getProductsHandler = container.resolve("GetProductsQueryHandler");
|
||||||
|
const createOrderHandler = container.resolve("CreateOrderCommandHandler");
|
||||||
|
const createProductHandler = container.resolve("CreateProductCommandHandler");
|
||||||
|
|
||||||
|
const getCategories = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const query = new GetCategoriesQuery();
|
||||||
|
const data = await getCategoriesHandler.handle(query);
|
||||||
|
return res.json({ data });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProducts = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const query = new GetProductsQuery();
|
||||||
|
const data = await getProductsHandler.handle(query);
|
||||||
|
return res.json({ data });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createProduct = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const command = new CreateProductCommand(req.body);
|
||||||
|
const data = await createProductHandler.handle(command);
|
||||||
|
return res.status(201).json({ data });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrder = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const command = new CreateOrderCommand(req.body);
|
||||||
|
const data = await createOrderHandler.handle(command);
|
||||||
|
return res.status(201).json({ data });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCategories,
|
||||||
|
getProducts,
|
||||||
|
createProduct,
|
||||||
|
createOrder
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
const RegisterUserCommand = require("../../Application/User/Command/RegisterUserCommand");
|
||||||
|
const LoginUserCommand = require("../../Application/User/Command/LoginUserCommand");
|
||||||
|
const LogoutUserCommand = require("../../Application/User/Command/LogoutUserCommand");
|
||||||
|
const GetCurrentUserQuery = require("../../Application/User/Query/GetCurrentUserQuery");
|
||||||
|
const container = require("../../Infrastructure/DI/container");
|
||||||
|
|
||||||
|
const authService = container.resolve("AuthService");
|
||||||
|
const registerHandler = container.resolve("RegisterUserCommandHandler");
|
||||||
|
const loginHandler = container.resolve("LoginUserCommandHandler");
|
||||||
|
const logoutHandler = container.resolve("LogoutUserCommandHandler");
|
||||||
|
const currentUserQueryHandler = container.resolve("GetCurrentUserQueryHandler");
|
||||||
|
|
||||||
|
const register = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const command = new RegisterUserCommand(req.body);
|
||||||
|
const result = await registerHandler.handle(command);
|
||||||
|
|
||||||
|
res.cookie(authService.cookieName, result.token, authService.cookieOptions());
|
||||||
|
return res.status(201).json({ data: result.user });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const command = new LoginUserCommand(req.body);
|
||||||
|
const result = await loginHandler.handle(command);
|
||||||
|
|
||||||
|
res.cookie(authService.cookieName, result.token, authService.cookieOptions());
|
||||||
|
return res.json({ data: result.user });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const me = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const query = new GetCurrentUserQuery({ userId: req.auth.userId });
|
||||||
|
const result = await currentUserQueryHandler.handle(query);
|
||||||
|
return res.json({ data: result });
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const command = new LogoutUserCommand({ userId: req.auth.userId });
|
||||||
|
await logoutHandler.handle(command);
|
||||||
|
|
||||||
|
res.clearCookie(authService.cookieName, authService.clearCookieOptions());
|
||||||
|
return res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
me,
|
||||||
|
logout
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const AuthService = require("../../Application/Services/AuthService");
|
||||||
|
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
|
const authMiddleware = (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const token = req.cookies[authService.cookieName];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ message: "Authentication required." });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.auth = authService.verifyToken(token);
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ message: "Invalid or expired token." });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = authMiddleware;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const errorHandler = (err, req, res, next) => {
|
||||||
|
if (res.headersSent) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = err.statusCode || 500;
|
||||||
|
return res.status(status).json({
|
||||||
|
message: err.message || "Internal server error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = errorHandler;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const notFound = (req, res) => {
|
||||||
|
res.status(404).json({ message: "Route not found" });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = notFound;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const controller = require("../Controller/shop.controller");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/categories", controller.getCategories);
|
||||||
|
router.get("/products", controller.getProducts);
|
||||||
|
router.post("/products", controller.createProduct);
|
||||||
|
router.post("/orders", controller.createOrder);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const controller = require("../Controller/user.controller");
|
||||||
|
const authMiddleware = require("../Middleware/auth.middleware");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/register", controller.register);
|
||||||
|
router.post("/login", controller.login);
|
||||||
|
router.get("/me", authMiddleware, controller.me);
|
||||||
|
router.post("/logout", authMiddleware, controller.logout);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const helmet = require("helmet");
|
||||||
|
const cors = require("cors");
|
||||||
|
const morgan = require("morgan");
|
||||||
|
const cookieParser = require("cookie-parser");
|
||||||
|
|
||||||
|
const userRouter = require("./Router/user.router");
|
||||||
|
const shopRouter = require("./Router/shop.router");
|
||||||
|
const notFound = require("./Middleware/notFound");
|
||||||
|
const errorHandler = require("./Middleware/errorHandler");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(cors({ origin: true, credentials: true }));
|
||||||
|
app.use(morgan("dev"));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
app.get("/api/health", (req, res) => {
|
||||||
|
res.json({ status: "ok", message: "CQRS backend running" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/api/users", userRouter);
|
||||||
|
app.use("/api/shop", shopRouter);
|
||||||
|
|
||||||
|
app.use(notFound);
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
const app = require("./app");
|
||||||
|
const env = require("../config/env");
|
||||||
|
|
||||||
|
app.listen(env.port, () => {
|
||||||
|
console.log(`Server listening on port ${env.port}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class UserDTO {
|
||||||
|
constructor(user) {
|
||||||
|
this.id = user.id;
|
||||||
|
this.name = user.name;
|
||||||
|
this.email = user.email;
|
||||||
|
this.createdAt = user.createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserDTO;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const env = require("../../config/env");
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
constructor() {
|
||||||
|
this.cookieName = env.cookieName;
|
||||||
|
}
|
||||||
|
|
||||||
|
signToken(payload) {
|
||||||
|
return jwt.sign(payload, env.jwtSecret, { expiresIn: env.jwtExpiresIn });
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyToken(token) {
|
||||||
|
return jwt.verify(token, env.jwtSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieOptions() {
|
||||||
|
return {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: env.cookieSecure,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCookieOptions() {
|
||||||
|
return {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: env.cookieSecure,
|
||||||
|
path: "/"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AuthService;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
const bcrypt = require("bcryptjs");
|
||||||
|
|
||||||
|
class PasswordService {
|
||||||
|
async hash(rawPassword) {
|
||||||
|
return bcrypt.hash(rawPassword, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
async compare(rawPassword, hash) {
|
||||||
|
return bcrypt.compare(rawPassword, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PasswordService;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class CreateOrderCommand {
|
||||||
|
constructor(payload) {
|
||||||
|
this.customer_name = payload.customer_name;
|
||||||
|
this.customer_email = payload.customer_email;
|
||||||
|
this.items = payload.items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CreateOrderCommand;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class CreateOrderCommandHandler {
|
||||||
|
constructor(shopRepository) {
|
||||||
|
this.shopRepository = shopRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(command) {
|
||||||
|
return this.shopRepository.createOrder(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CreateOrderCommandHandler;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
class CreateProductCommand {
|
||||||
|
constructor(payload) {
|
||||||
|
this.category_id = payload.category_id;
|
||||||
|
this.name = payload.name;
|
||||||
|
this.description = payload.description;
|
||||||
|
this.price = payload.price;
|
||||||
|
this.image_url = payload.image_url;
|
||||||
|
this.stock = payload.stock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CreateProductCommand;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class CreateProductCommandHandler {
|
||||||
|
constructor(shopRepository) {
|
||||||
|
this.shopRepository = shopRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(command) {
|
||||||
|
return this.shopRepository.createProduct(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CreateProductCommandHandler;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class GetCategoriesQuery {}
|
||||||
|
|
||||||
|
module.exports = GetCategoriesQuery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class GetCategoriesQueryHandler {
|
||||||
|
constructor(shopRepository) {
|
||||||
|
this.shopRepository = shopRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle() {
|
||||||
|
return this.shopRepository.findCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GetCategoriesQueryHandler;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class GetProductsQuery {}
|
||||||
|
|
||||||
|
module.exports = GetProductsQuery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class GetProductsQueryHandler {
|
||||||
|
constructor(shopRepository) {
|
||||||
|
this.shopRepository = shopRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle() {
|
||||||
|
return this.shopRepository.findProducts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GetProductsQueryHandler;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
class LoginUserCommand {
|
||||||
|
constructor(payload) {
|
||||||
|
this.email = payload.email;
|
||||||
|
this.password = payload.password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LoginUserCommand;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const PasswordService = require("../../Services/PasswordService");
|
||||||
|
const UserDTO = require("../../DTO/UserDTO");
|
||||||
|
|
||||||
|
class LoginUserCommandHandler {
|
||||||
|
constructor(userRepository, authService) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.authService = authService;
|
||||||
|
this.passwordService = new PasswordService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(command) {
|
||||||
|
if (!command.email || !command.password) {
|
||||||
|
const error = new Error("email and password are required");
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userRepository.findByEmail(command.email);
|
||||||
|
if (!user) {
|
||||||
|
const error = new Error("Invalid credentials");
|
||||||
|
error.statusCode = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await this.passwordService.compare(command.password, user.passwordHash);
|
||||||
|
if (!isValid) {
|
||||||
|
const error = new Error("Invalid credentials");
|
||||||
|
error.statusCode = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.authService.signToken({ userId: user.id, email: user.email });
|
||||||
|
return { user: new UserDTO(user), token };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LoginUserCommandHandler;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class LogoutUserCommand {
|
||||||
|
constructor(payload) {
|
||||||
|
this.userId = payload.userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LogoutUserCommand;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class LogoutUserCommandHandler {
|
||||||
|
async handle(command) {
|
||||||
|
if (!command.userId) {
|
||||||
|
const error = new Error("Unauthorized");
|
||||||
|
error.statusCode = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LogoutUserCommandHandler;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class RegisterUserCommand {
|
||||||
|
constructor(payload) {
|
||||||
|
this.name = payload.name;
|
||||||
|
this.email = payload.email;
|
||||||
|
this.password = payload.password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RegisterUserCommand;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const PasswordService = require("../../Services/PasswordService");
|
||||||
|
const UserDTO = require("../../DTO/UserDTO");
|
||||||
|
|
||||||
|
class RegisterUserCommandHandler {
|
||||||
|
constructor(userRepository, authService) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.authService = authService;
|
||||||
|
this.passwordService = new PasswordService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(command) {
|
||||||
|
if (!command.name || !command.email || !command.password) {
|
||||||
|
const error = new Error("name, email and password are required");
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await this.userRepository.findByEmail(command.email);
|
||||||
|
if (existing) {
|
||||||
|
const error = new Error("Email already in use");
|
||||||
|
error.statusCode = 409;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await this.passwordService.hash(command.password);
|
||||||
|
const user = await this.userRepository.create({
|
||||||
|
name: command.name,
|
||||||
|
email: command.email,
|
||||||
|
passwordHash
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = this.authService.signToken({ userId: user.id, email: user.email });
|
||||||
|
return { user: new UserDTO(user), token };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RegisterUserCommandHandler;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class GetCurrentUserQuery {
|
||||||
|
constructor(payload) {
|
||||||
|
this.userId = payload.userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GetCurrentUserQuery;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const UserDTO = require("../../DTO/UserDTO");
|
||||||
|
|
||||||
|
class GetCurrentUserQueryHandler {
|
||||||
|
constructor(userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(query) {
|
||||||
|
if (!query.userId) {
|
||||||
|
const error = new Error("Unauthorized");
|
||||||
|
error.statusCode = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userRepository.findById(query.userId);
|
||||||
|
if (!user) {
|
||||||
|
const error = new Error("User not found");
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserDTO(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GetCurrentUserQueryHandler;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class IUserRepository {
|
||||||
|
async findById(_id) {
|
||||||
|
throw new Error("Method not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(_email) {
|
||||||
|
throw new Error("Method not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(_data) {
|
||||||
|
throw new Error("Method not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = IUserRepository;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class User {
|
||||||
|
constructor({ id, name, email, passwordHash, createdAt }) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
this.passwordHash = passwordHash;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = User;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
class DIContainer {
|
||||||
|
constructor() {
|
||||||
|
this.registry = new Map();
|
||||||
|
this.singletons = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSingleton(token, factory) {
|
||||||
|
this.registry.set(token, { factory, lifetime: "singleton" });
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTransient(token, factory) {
|
||||||
|
this.registry.set(token, { factory, lifetime: "transient" });
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(token) {
|
||||||
|
const entry = this.registry.get(token);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`Dependency not registered: ${token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.lifetime === "singleton") {
|
||||||
|
if (!this.singletons.has(token)) {
|
||||||
|
this.singletons.set(token, entry.factory(this));
|
||||||
|
}
|
||||||
|
return this.singletons.get(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.factory(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DIContainer;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const DIContainer = require("./DIContainer");
|
||||||
|
|
||||||
|
const AuthService = require("../../Application/Services/AuthService");
|
||||||
|
|
||||||
|
const UserPrismaRepository = require("../Repository/UserPrismaRepository");
|
||||||
|
const ShopPrismaRepository = require("../Repository/ShopPrismaRepository");
|
||||||
|
|
||||||
|
const RegisterUserCommandHandler = require("../../Application/User/Command/RegisterUserCommandHandler");
|
||||||
|
const LoginUserCommandHandler = require("../../Application/User/Command/LoginUserCommandHandler");
|
||||||
|
const LogoutUserCommandHandler = require("../../Application/User/Command/LogoutUserCommandHandler");
|
||||||
|
const GetCurrentUserQueryHandler = require("../../Application/User/Query/GetCurrentUserQueryHandler");
|
||||||
|
|
||||||
|
const GetCategoriesQueryHandler = require("../../Application/Shop/Query/GetCategoriesQueryHandler");
|
||||||
|
const GetProductsQueryHandler = require("../../Application/Shop/Query/GetProductsQueryHandler");
|
||||||
|
const CreateProductCommandHandler = require("../../Application/Shop/Command/CreateProductCommandHandler");
|
||||||
|
const CreateOrderCommandHandler = require("../../Application/Shop/Command/CreateOrderCommandHandler");
|
||||||
|
|
||||||
|
const container = new DIContainer();
|
||||||
|
|
||||||
|
container.registerSingleton("AuthService", () => new AuthService());
|
||||||
|
|
||||||
|
container.registerSingleton("UserRepository", () => new UserPrismaRepository());
|
||||||
|
container.registerSingleton("ShopRepository", () => new ShopPrismaRepository());
|
||||||
|
|
||||||
|
container.registerSingleton("RegisterUserCommandHandler", (c) => {
|
||||||
|
return new RegisterUserCommandHandler(c.resolve("UserRepository"), c.resolve("AuthService"));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("LoginUserCommandHandler", (c) => {
|
||||||
|
return new LoginUserCommandHandler(c.resolve("UserRepository"), c.resolve("AuthService"));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("LogoutUserCommandHandler", () => {
|
||||||
|
return new LogoutUserCommandHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("GetCurrentUserQueryHandler", (c) => {
|
||||||
|
return new GetCurrentUserQueryHandler(c.resolve("UserRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("GetCategoriesQueryHandler", (c) => {
|
||||||
|
return new GetCategoriesQueryHandler(c.resolve("ShopRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("GetProductsQueryHandler", (c) => {
|
||||||
|
return new GetProductsQueryHandler(c.resolve("ShopRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("CreateProductCommandHandler", (c) => {
|
||||||
|
return new CreateProductCommandHandler(c.resolve("ShopRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("CreateOrderCommandHandler", (c) => {
|
||||||
|
return new CreateOrderCommandHandler(c.resolve("ShopRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = container;
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
const prisma = require("../../config/prisma");
|
||||||
|
|
||||||
|
class ShopPrismaRepository {
|
||||||
|
async findCategories() {
|
||||||
|
return prisma.category.findMany({ orderBy: { name: "asc" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findProducts() {
|
||||||
|
return prisma.product.findMany({
|
||||||
|
include: {
|
||||||
|
category: true
|
||||||
|
},
|
||||||
|
orderBy: { id: "asc" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProduct(payload) {
|
||||||
|
const categoryId = Number(payload.category_id);
|
||||||
|
const stock = payload.stock == null ? 0 : Number(payload.stock);
|
||||||
|
const price = Number(payload.price);
|
||||||
|
|
||||||
|
if (Number.isNaN(categoryId) || !payload.name || Number.isNaN(price) || Number.isNaN(stock)) {
|
||||||
|
const error = new Error("category_id, name, price and stock are required");
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price < 0 || stock < 0) {
|
||||||
|
const error = new Error("price and stock must be >= 0");
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await prisma.category.findUnique({ where: { id: categoryId } });
|
||||||
|
if (!category) {
|
||||||
|
const error = new Error("Category not found");
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.product.create({
|
||||||
|
data: {
|
||||||
|
categoryId,
|
||||||
|
name: payload.name,
|
||||||
|
description: payload.description || null,
|
||||||
|
price,
|
||||||
|
imageUrl: payload.image_url || null,
|
||||||
|
stock
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
category: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrder(payload) {
|
||||||
|
const { customer_name: customerName, customer_email: customerEmail, items } = payload;
|
||||||
|
|
||||||
|
if (!customerName || !customerEmail || !Array.isArray(items) || items.length === 0) {
|
||||||
|
const error = new Error("customer_name, customer_email and items are required");
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.$transaction(async (tx) => {
|
||||||
|
let totalPrice = 0;
|
||||||
|
const orderItems = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const productId = Number(item.product_id);
|
||||||
|
const quantity = Number(item.quantity);
|
||||||
|
|
||||||
|
if (Number.isNaN(productId) || Number.isNaN(quantity) || quantity <= 0) {
|
||||||
|
const error = new Error("Each item needs product_id and quantity > 0");
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = await tx.product.findUnique({ where: { id: productId } });
|
||||||
|
if (!product) {
|
||||||
|
const error = new Error(`Product ${productId} not found`);
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product.stock < quantity) {
|
||||||
|
const error = new Error(`Not enough stock for product ${product.name}`);
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPrice += Number(product.price) * quantity;
|
||||||
|
orderItems.push({ product, quantity });
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await tx.order.create({
|
||||||
|
data: {
|
||||||
|
customerName,
|
||||||
|
customerEmail,
|
||||||
|
totalPrice,
|
||||||
|
items: {
|
||||||
|
create: orderItems.map((entry) => ({
|
||||||
|
productId: entry.product.id,
|
||||||
|
quantity: entry.quantity,
|
||||||
|
unitPrice: entry.product.price
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
items: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of orderItems) {
|
||||||
|
await tx.product.update({
|
||||||
|
where: { id: entry.product.id },
|
||||||
|
data: { stock: entry.product.stock - entry.quantity }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ShopPrismaRepository;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const prisma = require("../../config/prisma");
|
||||||
|
const IUserRepository = require("../../Domain/IRepository/IUserRepository");
|
||||||
|
const User = require("../../Domain/Models/User");
|
||||||
|
|
||||||
|
class UserPrismaRepository extends IUserRepository {
|
||||||
|
async findById(id) {
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: Number(id) } });
|
||||||
|
return user ? new User(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email) {
|
||||||
|
const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
|
||||||
|
return user ? new User(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data) {
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
email: data.email.toLowerCase(),
|
||||||
|
passwordHash: data.passwordHash
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new User(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserPrismaRepository;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const dotenv = require("dotenv");
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
port: Number(process.env.PORT || 3000),
|
||||||
|
nodeEnv: process.env.NODE_ENV || "development",
|
||||||
|
databaseUrl: process.env.DATABASE_URL,
|
||||||
|
jwtSecret: process.env.JWT_SECRET || "dev-secret",
|
||||||
|
jwtExpiresIn: process.env.JWT_EXPIRES_IN || "7d",
|
||||||
|
cookieName: process.env.COOKIE_NAME || "webstore_token",
|
||||||
|
cookieSecure: process.env.COOKIE_SECURE === "true"
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const { PrismaClient } = require("@prisma/client");
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
module.exports = prisma;
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,230 @@
|
|||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[magyar]{babel}
|
||||||
|
\usepackage{indentfirst}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{tikz}
|
||||||
|
\usetikzlibrary{positioning}
|
||||||
|
\usepackage{fancyvrb}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{pifont}
|
||||||
|
\usepackage{newunicodechar}
|
||||||
|
|
||||||
|
% Unicode szimbólumok definiálása
|
||||||
|
\newcommand{\cmark}{\ding{51}} % ✓ check mark
|
||||||
|
\newcommand{\xmark}{\ding{55}} % ✗ ballot x
|
||||||
|
\newunicodechar{✓}{\cmark}
|
||||||
|
\newunicodechar{✗}{\xmark}
|
||||||
|
\newunicodechar{❌}{\textcolor{red}{\xmark}}
|
||||||
|
|
||||||
|
% Globális verbatim beállítás - tiny betűméret minden verbatim blokkhoz
|
||||||
|
\fvset{fontsize=\tiny}
|
||||||
|
\usepackage{listingsutf8}
|
||||||
|
\usepackage{textcomp}
|
||||||
|
\usepackage{eurosym}
|
||||||
|
\usepackage{mathtools}
|
||||||
|
\lstset{literate=
|
||||||
|
{á}{{\'a}}1 {é}{{\'e}}1 {í}{{\'i}}1 {ó}{{\'o}}1 {ú}{{\'u}}1
|
||||||
|
{Á}{{\'A}}1 {É}{{\'E}}1 {Í}{{\'I}}1 {Ó}{{\'O}}1 {Ú}{{\'U}}1
|
||||||
|
{à}{{\`a}}1 {è}{{\`e}}1 {ì}{{\`i}}1 {ò}{{\`o}}1 {ù}{{\`u}}1
|
||||||
|
{À}{{\`A}}1 {È}{{\'E}}1 {Ì}{{\`I}}1 {Ò}{{\`O}}1 {Ù}{{\`U}}1
|
||||||
|
{ä}{{\"a}}1 {ë}{{\"e}}1 {ï}{{\"i}}1 {ö}{{\"o}}1 {ü}{{\"u}}1
|
||||||
|
{Ä}{{\"A}}1 {Ë}{{\"E}}1 {Ï}{{\"I}}1 {Ö}{{\"O}}1 {Ü}{{\"U}}1
|
||||||
|
{â}{{\^a}}1 {ê}{{\^e}}1 {î}{{\^i}}1 {ô}{{\^o}}1 {û}{{\^u}}1
|
||||||
|
{Â}{{\^A}}1 {Ê}{{\^E}}1 {Î}{{\^I}}1 {Ô}{{\^O}}1 {Û}{{\^U}}1
|
||||||
|
{œ}{{\oe}}1 {Œ}{{\OE}}1 {æ}{{\ae}}1 {Æ}{{\AE}}1 {ß}{{\ss}}1
|
||||||
|
{ç}{{\c c}}1 {Ç}{{\c C}}1 {ø}{{\o}}1 {å}{{\r a}}1 {Å}{{\r A}}1
|
||||||
|
{€}{{\EUR}}1 {£}{{\pounds}}1 {ő}{{\H{o}}}1 {ű}{{\H{u}}}1
|
||||||
|
}
|
||||||
|
% Docker nyelvdefiníció
|
||||||
|
\lstdefinelanguage{Docker}{
|
||||||
|
keywords={FROM, RUN, CMD, LABEL, MAINTAINER, EXPOSE, ENV, ADD, COPY,
|
||||||
|
ENTRYPOINT, VOLUME, USER, WORKDIR, ARG, ONBUILD, STOPSIGNAL,
|
||||||
|
HEALTHCHECK, SHELL, AS},
|
||||||
|
keywordstyle=\color{blue}\bfseries,
|
||||||
|
identifierstyle=\color{black},
|
||||||
|
sensitive=false,
|
||||||
|
comment=[l]{\#},
|
||||||
|
commentstyle=\color{purple}\ttfamily,
|
||||||
|
stringstyle=\color{red}\ttfamily,
|
||||||
|
morestring=[b]',
|
||||||
|
morestring=[b]"
|
||||||
|
}
|
||||||
|
|
||||||
|
% JavaScript nyelvdefiníció
|
||||||
|
\lstdefinelanguage{JavaScript}{
|
||||||
|
keywords={typeof, new, true, false, catch, function, return, null, catch,
|
||||||
|
switch, var, const, let, if, in, while, do, else, case, break, async,
|
||||||
|
await, class, export, import, extends, super, this, throw, try, default},
|
||||||
|
keywordstyle=\color{blue}\bfseries,
|
||||||
|
ndkeywords={class, export, boolean, throw, implements, import, this},
|
||||||
|
ndkeywordstyle=\color{darkgray}\bfseries,
|
||||||
|
identifierstyle=\color{black},
|
||||||
|
sensitive=false,
|
||||||
|
comment=[l]{//},
|
||||||
|
morecomment=[s]{/*}{*/},
|
||||||
|
commentstyle=\color{purple}\ttfamily,
|
||||||
|
stringstyle=\color{red}\ttfamily,
|
||||||
|
morestring=[b]',
|
||||||
|
morestring=[b]"
|
||||||
|
}
|
||||||
|
|
||||||
|
% YAML nyelvdefiníció
|
||||||
|
\lstdefinelanguage{yaml}{
|
||||||
|
keywords={true,false,null,y,n},
|
||||||
|
keywordstyle=\color{darkgray}\bfseries,
|
||||||
|
sensitive=false,
|
||||||
|
comment=[l]{\#},
|
||||||
|
commentstyle=\color{purple}\ttfamily,
|
||||||
|
stringstyle=\color{red}\ttfamily,
|
||||||
|
morestring=[b]',
|
||||||
|
morestring=[b]",
|
||||||
|
basicstyle=\ttfamily\scriptsize,
|
||||||
|
breaklines=true,
|
||||||
|
columns=fullflexible,
|
||||||
|
keepspaces=true,
|
||||||
|
showstringspaces=false
|
||||||
|
}
|
||||||
|
|
||||||
|
\lstdefinestyle{HTML}{
|
||||||
|
language=HTML,
|
||||||
|
breaklines=true,
|
||||||
|
postbreak=\mbox{\textcolor{red}{$\hookrightarrow$}\space},
|
||||||
|
stringstyle=\ttfamily,
|
||||||
|
inputencoding=utf8,
|
||||||
|
morekeywords={header, time, nav, main, article, section, aside, role,
|
||||||
|
footer, details, open, summary, srcdoc, list, datalist, placeholder,
|
||||||
|
pattern, required, min, max, step, enctype, formaction, formmethod,
|
||||||
|
formnovalidate, formtarget, output}
|
||||||
|
}
|
||||||
|
|
||||||
|
\lstdefinestyle{JavaScript}{
|
||||||
|
basicstyle=\ttfamily\scriptsize,
|
||||||
|
breaklines=true,
|
||||||
|
columns=fullflexible,
|
||||||
|
keepspaces=true,
|
||||||
|
showstringspaces=false,
|
||||||
|
literate={}
|
||||||
|
}
|
||||||
|
\lstdefinestyle{NodeJS}{
|
||||||
|
basicstyle=\ttfamily\scriptsize,
|
||||||
|
breaklines=true,
|
||||||
|
columns=fullflexible,
|
||||||
|
keepspaces=true,
|
||||||
|
showstringspaces=false,
|
||||||
|
literate={}
|
||||||
|
}
|
||||||
|
\lstdefinestyle{Express}{
|
||||||
|
basicstyle=\ttfamily\scriptsize,
|
||||||
|
breaklines=true,
|
||||||
|
columns=fullflexible,
|
||||||
|
keepspaces=true,
|
||||||
|
showstringspaces=false,
|
||||||
|
literate={}
|
||||||
|
}
|
||||||
|
\lstdefinestyle{Prisma}{
|
||||||
|
basicstyle=\ttfamily\scriptsize,
|
||||||
|
breaklines=true,
|
||||||
|
columns=fullflexible,
|
||||||
|
keepspaces=true,
|
||||||
|
showstringspaces=false,
|
||||||
|
literate={}
|
||||||
|
}
|
||||||
|
\usepackage{hyperref}
|
||||||
|
\usepackage{attachfile}
|
||||||
|
\usepackage{multirow}
|
||||||
|
% Navigációs pöttyök hozzáadása subsection nélküli fejezetekhez
|
||||||
|
\usepackage{remreset}
|
||||||
|
\makeatletter
|
||||||
|
\@removefromreset{subsection}{section}
|
||||||
|
\makeatother
|
||||||
|
\setcounter{subsection}{1}
|
||||||
|
%%%%%
|
||||||
|
\attachfilesetup{color={1.0 0.6 0.0},author={MD},description={Kattintson duplán a minta %
|
||||||
|
megtekintéséhez!},icon=Paperclip}
|
||||||
|
% Széchenyi Egyetem arculati színek
|
||||||
|
\definecolor{szenavy}{RGB}{44,62,80} % Sötét kék (fejléc)
|
||||||
|
\definecolor{szecyan}{RGB}{0,168,225} % Világos kék (kiemelés, logó)
|
||||||
|
\definecolor{szezold}{RGB}{139,195,74} % Élénk zöld (akcentus)
|
||||||
|
\definecolor{szeszurke}{RGB}{96,96,96} % Sötét szürke
|
||||||
|
% Kompatibilitás a régi parancsokkal
|
||||||
|
\definecolor{kiemelesszin}{RGB}{0,168,225} % Kék kiemelés (szecyan)
|
||||||
|
\definecolor{kiemelesszinZ}{RGB}{139,195,74} % Zöld kiemelés (szezold)
|
||||||
|
\definecolor{kiemelesszinN}{RGB}{44,62,80} % Navy kiemelés (szenavy)
|
||||||
|
\definecolor{hivatkozasszin}{RGB}{0,168,225} % Kék hivatkozás
|
||||||
|
\newcommand{\kiemel}[1]{{\color{kiemelesszin}#1}}
|
||||||
|
\newcommand{\kiemelZ}[1]{{\color{kiemelesszinZ}#1}}
|
||||||
|
\newcommand{\kiemelN}[1]{{\color{kiemelesszinN}#1}}
|
||||||
|
\newcommand{\hiv}[1]{{\color{hivatkozasszin}#1}}
|
||||||
|
\newcommand{\logoalul}{
|
||||||
|
\begin{picture}(0,0)
|
||||||
|
\put(120,-0){\hbox{\includegraphics[scale=.5]{./common/sze_logo.pdf}}}
|
||||||
|
\put(205,-6){\hbox{\includegraphics[scale=.4]{./common/it_logo.pdf}}}
|
||||||
|
\end{picture}
|
||||||
|
}
|
||||||
|
|
||||||
|
\frenchspacing
|
||||||
|
\usetheme[compress]{Berlin}
|
||||||
|
\useoutertheme[subsection=false]{miniframes}
|
||||||
|
\setbeamerfont{section in head/foot}{size=\tiny}
|
||||||
|
\setbeamerfont{subsection in head/foot}{size=\tiny}
|
||||||
|
|
||||||
|
% Adaptív, kattintható navigáció:
|
||||||
|
% sok section esetén az aktuális marad nagy és szöveges,
|
||||||
|
% a többi section lekicsinyített sorszámként jelenik meg.
|
||||||
|
\newcommand{\sectioncompactthreshold}{11}
|
||||||
|
\makeatletter
|
||||||
|
\providecommand{\totalsectionscount}{0}
|
||||||
|
\AtEndDocument{%
|
||||||
|
\immediate\write\@auxout{\string\gdef\string\totalsectionscount{\arabic{section}}}%
|
||||||
|
}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
\setbeamertemplate{section in head/foot}{%
|
||||||
|
{\fontsize{6}{7}\selectfont\bfseries\insertsectionhead}%
|
||||||
|
}
|
||||||
|
\setbeamertemplate{section in head/foot shaded}{%
|
||||||
|
\ifnum\totalsectionscount>\sectioncompactthreshold
|
||||||
|
{\fontsize{4.5}{5.5}\selectfont\insertsectionheadnumber}%
|
||||||
|
\else
|
||||||
|
{\fontsize{5}{6}\selectfont\insertsectionhead}%
|
||||||
|
\fi
|
||||||
|
}
|
||||||
|
\setbeamertemplate{headline}
|
||||||
|
{
|
||||||
|
\leavevmode%
|
||||||
|
\hbox{%
|
||||||
|
\begin{beamercolorbox}[wd=\paperwidth,ht=2.5ex,dp=1.125ex]{section in head/foot}%
|
||||||
|
\insertsectionnavigationhorizontal{\paperwidth}{}{\hskip0pt plus1filll}
|
||||||
|
\end{beamercolorbox}%
|
||||||
|
}
|
||||||
|
\vskip0pt%
|
||||||
|
}
|
||||||
|
|
||||||
|
% Kisebb betűméret a slide-okhoz
|
||||||
|
\setbeamerfont{frametitle}{size=\normalsize}
|
||||||
|
\setbeamerfont{framesubtitle}{size=\small}
|
||||||
|
\setbeamerfont{block title}{size=\small}
|
||||||
|
\setbeamerfont{block body}{size=\footnotesize}
|
||||||
|
\setbeamerfont{itemize/enumerate body}{size=\footnotesize}
|
||||||
|
\setbeamerfont{itemize/enumerate subbody}{size=\scriptsize}
|
||||||
|
|
||||||
|
% Beamer színséma testreszabása Széchenyi arculathoz
|
||||||
|
\setbeamercolor{structure}{fg=szecyan}
|
||||||
|
\setbeamercolor{palette primary}{bg=szenavy,fg=white}
|
||||||
|
\setbeamercolor{palette secondary}{bg=szecyan,fg=white}
|
||||||
|
\setbeamercolor{palette tertiary}{bg=szezold,fg=white}
|
||||||
|
\setbeamercolor{palette quaternary}{bg=szeszurke,fg=white}
|
||||||
|
\setbeamercolor{titlelike}{parent=palette primary}
|
||||||
|
\setbeamercolor{frametitle}{bg=szenavy,fg=white}
|
||||||
|
\setbeamercolor{frametitle right}{bg=szenavy}
|
||||||
|
\setbeamercolor{block title}{bg=szecyan,fg=white}
|
||||||
|
\setbeamercolor{block body}{bg=szecyan!10,fg=black}
|
||||||
|
\setbeamercolor{block title alerted}{bg=szezold,fg=white}
|
||||||
|
\setbeamercolor{block body alerted}{bg=szezold!10,fg=black}
|
||||||
|
\setbeamercolor{item}{fg=szecyan}
|
||||||
|
\setbeamercolor{subitem}{fg=szezold}
|
||||||
|
|
||||||
|
\author{Magda Donát}
|
||||||
|
\institute{Széchenyi István Egyetem, Győr}
|
||||||
|
\date{\hiv{\href{https://git.mdnd-it.cc/Donat/GKNB_MSTM071}{https://git.mdnd-it.cc/Donat/GKNB_MSTM071}}\\ \today}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
\section{Alapok}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Mi a TypeScript?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A TypeScript a JavaScript \kiemel{típusos kiterjesztése}.
|
||||||
|
\item Fejlesztés közben jelez hibákat, mielőtt futna a kód.
|
||||||
|
\item A fordítás után sima JavaScript fut a böngészőben vagy Node.js alatt.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Hogyan gondolkodj róla kezdőként?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A JavaScript megmondja, \kiemel{hogyan} fusson a program.
|
||||||
|
\item A TypeScript megmutatja, \kiemel{milyen adatokkal} futhat biztonságosan.
|
||||||
|
\item A típusok dokumentációként is működnek, így a következő fejlesztő gyorsabban megérti a kódot.
|
||||||
|
\item A cél nem a bonyolult típusok gyártása, hanem a hibák korai kiszűrése.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Miért jó kezdőknek?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Könnyebb megérteni, milyen adatot vár egy függvény.
|
||||||
|
\item Biztonságosabb a refaktorálás.
|
||||||
|
\item Jobb IDE-támogatást kapsz (autocomplete, hibajelzések).
|
||||||
|
\item Kisebb az esélye a futásidejű meglepetéseknek.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Mire figyelj a tanulás elején?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Először a JavaScript alapokat erősítsd meg: változók, függvények, objektumok, tömbök.
|
||||||
|
\item Utána vezesd be a TypeScriptet kis lépésekben: előbb paraméter- és visszatérési típusokkal.
|
||||||
|
\item Ne használd túl korán az any típust, mert ezzel elveszíted a TypeScript fő előnyét.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Telepítés és első projekt}
|
||||||
|
\begin{lstlisting}[style=NodeJS]
|
||||||
|
mkdir ts-course
|
||||||
|
cd ts-course
|
||||||
|
npm init -y
|
||||||
|
npm install -D typescript ts-node @types/node
|
||||||
|
npx tsc --init
|
||||||
|
\end{lstlisting}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Egyszerű tsconfig.json}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item strict: szigorúbb ellenőrzés, kevesebb rejtett hiba.
|
||||||
|
\item rootDir/outDir: tisztán elválasztja a forráskódot és a fordított kódot.
|
||||||
|
\item module NodeNext: modern ES-modulok Node környezetben.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Első TypeScript-kód}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
// src/index.ts
|
||||||
|
function greet(name: string): string {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(greet("TypeScript"));
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{lstlisting}[style=NodeJS]
|
||||||
|
npx tsc
|
||||||
|
node dist/index.js
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Először a tsc fordító fut le, ez ellenőrzi a típusokat.
|
||||||
|
\item Ezután már JavaScript fut, vagyis a végrehajtás mindig JS-ben történik.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
\section{Típusok}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Primitív típusok}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
let userName: string = "Anna";
|
||||||
|
let age: number = 20;
|
||||||
|
let isStudent: boolean = true;
|
||||||
|
let value: null = null;
|
||||||
|
let missing: undefined = undefined;
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Ezek a legalapvetőbb építőelemek, minden összetettebb típus ezekre épül.
|
||||||
|
\item A null és undefined kezelése kulcsfontosságú a stabil alkalmazásokhoz.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Típusinferencia}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
let city = "Gyor"; // string
|
||||||
|
let points = 10; // number
|
||||||
|
const role = "admin"; // "admin" literal
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Nem kell mindig kiírni a típust.
|
||||||
|
\item Akkor érdemes explicit típust adni, ha nő tőle az olvashatóság.
|
||||||
|
\item Jó egyensúly: ahol egyértelmű, hagyd inferálni; ahol kétértelmű, írj explicit típust.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Tömbök és tuple}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
const nums: number[] = [1, 2, 3];
|
||||||
|
const tags: Array<string> = ["ts", "js"];
|
||||||
|
|
||||||
|
const point: [number, number] = [10, 20];
|
||||||
|
const personRow: [id: number, name: string] = [1, "Bela"];
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Tömb: tetszőleges elemszám, azonos elemtípus.
|
||||||
|
\item Tuple: fix elemszám, pozíciónként meghatározott típus.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Objektumtípusok}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
const user: { id: number; name: string; active: boolean } = {
|
||||||
|
id: 1,
|
||||||
|
name: "Kata",
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Objektumoknál a típus gyorsan hosszúvá válhat, ezért a következő lépés a type/interface használata.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Tipikus kezdő hibák típusoknál}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Összekeverni a number és string értékeket (pl. "10" + 5).
|
||||||
|
\item Elfelejteni, hogy a const literál-típust is adhat.
|
||||||
|
\item Nem kezelni a null lehetőségét külső adatoknál.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
\section{Függvények}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Függvénytípusok}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function add(a: number, b: number): number {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiply = (a: number, b: number): number => a * b;
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Mindig típusold a bemenetet, és lehetőség szerint a visszatérési értéket is.
|
||||||
|
\item Ettől egyértelműbb lesz a függvény szerződése.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Opcionális és alapértelmezett paraméter}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function greet(name: string, title?: string): string {
|
||||||
|
return title ? `${title} ${name}` : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function power(base: number, exp = 2): number {
|
||||||
|
return base ** exp;
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Opcionális paraméter csak a paraméterlista végén legyen.
|
||||||
|
\item Az alapértelmezett paraméter csökkenti a hívó kód bonyolultságát.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Union típusok}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
let id: string | number;
|
||||||
|
id = "A12";
|
||||||
|
id = 12;
|
||||||
|
|
||||||
|
type Direction = "up" | "down" | "left" | "right";
|
||||||
|
let move: Direction = "left";
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A union azt jelenti, hogy az adat többféle típusból érkezhet.
|
||||||
|
\item Literál unionnal szűk, előre ismert értékkészletet adhatsz meg.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Típusszűkítés (narrowing)}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function printId(value: string | number) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
console.log(value.toUpperCase());
|
||||||
|
} else {
|
||||||
|
console.log(value.toFixed(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Narrowing nélkül a fordító nem engedné string- vagy number-specifikus metódusok hívását.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Gondolkodási minta union esetén}
|
||||||
|
\begin{itemize}
|
||||||
|
\item 1. lépés: sorold fel, milyen típusok jöhetnek.
|
||||||
|
\item 2. lépés: minden esetre írj megfelelő ellenőrzést.
|
||||||
|
\item 3. lépés: minden ágban csak az adott típushoz illő műveletet végezz.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
\section{Típusaliasok és interfészek}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Type alias}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const u: User = { id: 1, name: "Dora" };
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A type kulcsszó jó választás, ha később unionöket vagy utility típusokat is használsz.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Interface}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
interface Product {
|
||||||
|
sku: string;
|
||||||
|
price: number;
|
||||||
|
discount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p: Product = { sku: "A-1", price: 100 };
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Az interface különösen hasznos API-szerződéseknél és osztályoknál.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Type vs interface (gyakorlat)}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Interface: objektumszerződésekhez.
|
||||||
|
\item Type: unionokhoz, metszetekhez, utility típusokhoz.
|
||||||
|
\item Mindkettő használható objektumokhoz, a csapatdöntés a fontos.
|
||||||
|
\item A legfontosabb: kódbázison belül maradj következetes.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Readonly és opcionálisság}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type Settings = {
|
||||||
|
readonly appName: string;
|
||||||
|
theme?: "light" | "dark";
|
||||||
|
};
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item readonly: a property létrehozás után nem módosítható.
|
||||||
|
\item ?: az adott mezőt nem kötelező minden esetben megadni.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Mikor válts type aliasról interface-re?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Ha objektumszerződést publikus API-ként szeretnél kommunikálni.
|
||||||
|
\item Ha osztály implementálja a szerződést.
|
||||||
|
\item Ha a csapatod konvenciója ezt preferálja.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
\section{Középhaladó: Generikus típusok}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Generic alap}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function identity<T>(value: T): T {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = identity(42); // number
|
||||||
|
const b = identity("hello"); // string
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A generic lehetővé teszi, hogy ugyanaz a függvény több típusra is működjön.
|
||||||
|
\item A T helyére a fordító a hívás alapján következteti a típust.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Generic constraint}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function firstItem<T extends any[]>(items: T) {
|
||||||
|
return items[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = firstItem([10, 20, 30]);
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A constraint (extends) korlátozza, milyen típus adható a generic paraméternek.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Utility tipusok}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type User = { id: number; name: string; email: string };
|
||||||
|
|
||||||
|
type UserPatch = Partial<User>;
|
||||||
|
type UserPublic = Pick<User, "id" | "name">;
|
||||||
|
type UserWithoutEmail = Omit<User, "email">;
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Utility típusokkal gyorsan előállíthatsz új típusokat manuális másolás nélkül.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Record es Readonly}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type ScoreByName = Record<string, number>;
|
||||||
|
|
||||||
|
type Config = Readonly<{
|
||||||
|
apiUrl: string;
|
||||||
|
retries: number;
|
||||||
|
}>;
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Record: kulcs-érték struktúra típusosan.
|
||||||
|
\item Readonly: megakadályozza a véletlen módosításokat.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{A genericek tanulási sorrendje}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Először használj kész generic API-kat (pl. Promise<T>, Array<T>).
|
||||||
|
\item Utána írj egyszerű saját generic függvényt.
|
||||||
|
\item Végül vezesd be a constraintet és utility típusokat.
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
\section{Középhaladó: Modulok és aszinkron működés}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Modulok: export/import}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
// math.ts
|
||||||
|
export function add(a: number, b: number) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.ts
|
||||||
|
import { add } from "./math.js";
|
||||||
|
console.log(add(2, 3));
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Modulokra bontással átláthatóbb és újrafelhasználhatóbb kódot kapsz.
|
||||||
|
\item NodeNext módban importnál gyakran .js kiterjesztést használsz.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Async/Await tipusokkal}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type User = { id: number; name: string };
|
||||||
|
|
||||||
|
async function fetchUser(id: number): Promise<User> {
|
||||||
|
const res = await fetch(`/api/users/${id}`);
|
||||||
|
if (!res.ok) throw new Error("Request failed");
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A Promise<User> egyértelműen jelzi a hívó kódnak a várt eredményt.
|
||||||
|
\item Hibakezelés nélkül API-hívásnál nehezen diagnosztizálható hibák jelennek meg.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Kulso csomagok tipizalasa}
|
||||||
|
\begin{lstlisting}[style=NodeJS]
|
||||||
|
npm install lodash
|
||||||
|
npm install -D @types/lodash
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Sok csomag már tartalmaz beépített típusokat.
|
||||||
|
\item Ha nem, akkor @types csomagot kell telepíteni.
|
||||||
|
\item Típusok nélkül külső csomag használata gyorsan any-hoz vezet.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Axios pelda valasz-tipussal}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
type Todo = { id: number; title: string; done: boolean };
|
||||||
|
|
||||||
|
async function getTodo(id: number): Promise<Todo> {
|
||||||
|
const r = await axios.get<Todo>(`/api/todos/${id}`);
|
||||||
|
return r.data;
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A <Todo> generic paraméterrel a válasz struktúrája már a hívás helyén ellenőrizhető.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Gyakorlati minta API-khoz}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Definiálj külön típusokat a kérésekhez: Request DTO, Response DTO.
|
||||||
|
\item A hálózati réteget különítsd el service függvényekbe.
|
||||||
|
\item A komponensek csak a már tipizált adatot kapják.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
\section{Validáció}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{any vs unknown}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
let loose: any = 123;
|
||||||
|
loose = "bármi"; // nincs ellenőrzés
|
||||||
|
|
||||||
|
let safe: unknown = JSON.parse('{"id":1}');
|
||||||
|
if (typeof safe === "object" && safe !== null) {
|
||||||
|
// itt már szűkíthető
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Az any kikapcsolja a típusellenőrzést, ezért csak átmeneti esetben használd.
|
||||||
|
\item Az unknown biztonságosabb, mert használat előtt kötelező ellenőrizni.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Futásidejű validáció Type Guarddal}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function isUser(v: unknown): v is { id: number; name: string } {
|
||||||
|
return (
|
||||||
|
typeof v === "object" &&
|
||||||
|
v !== null &&
|
||||||
|
"id" in v &&
|
||||||
|
"name" in v
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A TypeScript nem helyettesíti a futásidejű validációt.
|
||||||
|
\item Külső adatnál (API, localStorage, URL-paraméter) mindig ellenőrizz.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Strict mód bekapcsolása}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Ezek a beállítások kezdetben több hibát mutatnak, de hosszabb távon stabilabb kódot adnak.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Gyakori hibák}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Túl sok any használata.
|
||||||
|
\item Null/undefined esetek figyelmen kívül hagyása.
|
||||||
|
\item Túl bonyolult típusok írása stabil alapok nélkül.
|
||||||
|
\item Típusok másolása utility típusok használata helyett.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Bevált gyakorlat röviden}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Alapértelmezésben legyen bekapcsolva a strict mód.
|
||||||
|
\item Külső adatot mindig validálj.
|
||||||
|
\item Bonyolult típus helyett használj több kicsi, beszédes típust.
|
||||||
|
\item Legyen következetes kódstílus és linterbeállítás.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
\section{Tanulási Segédlet}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{4 hetes tanulási terv}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item 1. hét: típusok, tömbök, objektumok, függvények.
|
||||||
|
\item 2. hét: unionök, narrowing, aliasok, interfészek.
|
||||||
|
\item 3. hét: generics, utility típusok, modulok.
|
||||||
|
\item 4. hét: aszinkron kód, API-típusok, futásidejű validáció.
|
||||||
|
\end{enumerate}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Napi 45-60 perc folyamatos gyakorlás már jól látható fejlődést ad.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Projektötletek kezdőknek}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Teendőlista-alkalmazás (CRUD) típusosan.
|
||||||
|
\item Egyszerű REST API-kliens.
|
||||||
|
\item Jegyzetelő CLI fájlba mentéssel.
|
||||||
|
\end{itemize}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Cél: az alaptípusok és a függvényszerződések rutinszerű használata.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Projektötletek középhaladóknak}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Modulokra bontott mini backend.
|
||||||
|
\item Validációs réteggel rendelkező API-kliens.
|
||||||
|
\item Kis NPM-csomag saját típusdefiníciókkal.
|
||||||
|
\end{itemize}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Cél: valós, projekt-szerű struktúrában gondolkodni.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Mit erdemes mindenkepp megtanulni?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A típusinferencia értése.
|
||||||
|
\item A union + narrowing magabiztos használata.
|
||||||
|
\item A generics és utility típusok alapjai.
|
||||||
|
\item Futásidejű validáció külső adat esetén.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Önellenőrző kérdések minden modul után}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Meg tudom magyarázni, mit vár és mit ad vissza a függvényem?
|
||||||
|
\item Használtam-e any-t ott, ahol az unknown biztonságosabb lenne?
|
||||||
|
\item Tudok-e példát mondani a union + narrowing együtt használatára?
|
||||||
|
\item Validáltam-e a külső adatot, mielőtt használtam?
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
\section{Haladó Típuskezelés}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{keyof és indexed access}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserKey = keyof User; // "id" | "name" | "active"
|
||||||
|
type NameType = User["name"]; // string
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A keyof segítségével dinamikusan hivatkozhatsz egy típus kulcsaira.
|
||||||
|
\item Az indexed access típus pontosan kiolvassa egy mező típusát.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Mapped type alapminta}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type Optional<T> = {
|
||||||
|
[K in keyof T]?: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReadOnly<T> = {
|
||||||
|
readonly [K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Mapped type-pal meglévő típusból generálhatsz új, következetes szerkezetet.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Conditional type és infer}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type ElementType<T> = T extends (infer U)[] ? U : T;
|
||||||
|
|
||||||
|
type A = ElementType<string[]>; // string
|
||||||
|
type B = ElementType<number>; // number
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item A conditional type típus-szintű "if" logikát ad.
|
||||||
|
\item Az infer kulcsszóval résztípusokat tudsz kinyerni.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Mikor használd ezeket?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Ha sok hasonló DTO típust kell karbantartanod.
|
||||||
|
\item Ha könyvtárszintű, újrafelhasználható típus-API-t építesz.
|
||||||
|
\item Ha ismétlődő kézi típusmásolást látsz a projektben.
|
||||||
|
\end{itemize}
|
||||||
|
\begin{alertblock}{Szabály}
|
||||||
|
Haladó típust csak akkor vezess be, ha egyszerűbbé teszi a használatot.
|
||||||
|
\end{alertblock}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
\section{Osztályok és Architekturális Minták}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Osztályok TypeScriptben}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
class Account {
|
||||||
|
constructor(
|
||||||
|
public owner: string,
|
||||||
|
private balance: number
|
||||||
|
) {}
|
||||||
|
|
||||||
|
deposit(amount: number): void {
|
||||||
|
this.balance += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBalance(): number {
|
||||||
|
return this.balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Interface + osztály együtt}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
interface Logger {
|
||||||
|
log(message: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleLogger implements Logger {
|
||||||
|
log(message: string): void {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Az interface szerződés, az osztály ennek konkrét megvalósítása.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Rétegezett felépítés röviden}
|
||||||
|
\begin{itemize}
|
||||||
|
\item domain: üzleti logika és modellek
|
||||||
|
\item service: alkalmazási műveletek
|
||||||
|
\item infrastructure: API, adatbázis, külső kapcsolatok
|
||||||
|
\item ui/controller: bemenet-kimenet kezelése
|
||||||
|
\end{itemize}
|
||||||
|
\begin{block}{Előny}
|
||||||
|
A jól rétegzett struktúra könnyebben tesztelhető és bővíthető.
|
||||||
|
\end{block}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Gyakori tervezési hiba}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Minden logika egyetlen fájlban van.
|
||||||
|
\item Az API-hívás közvetlenül a komponensben történik típusos határ nélkül.
|
||||||
|
\item A validáció és üzleti szabályok keverednek.
|
||||||
|
\end{itemize}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Megoldás: rétegek szétválasztása és tiszta típushatárok.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
\section{Hibakeresés és Fordítói Üzenetek}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Hogyan olvasd a TypeScript hibákat?}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Először a hiba kódját nézd (pl. TS2322).
|
||||||
|
\item Utána nézd meg: "mit kaptam" és "mit vár a típus".
|
||||||
|
\item A hiba gyakran a hívási pontnál jelenik meg, de az ok lehet korábban.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Tipikus hiba: TS2322}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
let count: number;
|
||||||
|
count = "10"; // TS2322
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Jelentés: nem kompatibilis típus-hozzárendelés.
|
||||||
|
\item Javítás: konvertálj (Number("10")) vagy javítsd az adatforrást.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Tipikus hiba: TS2532}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type User = { name?: string };
|
||||||
|
const u: User = {};
|
||||||
|
|
||||||
|
console.log(u.name.toUpperCase()); // TS2532
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Jelentés: az érték lehet undefined.
|
||||||
|
\item Javítás: ellenőrzés (if), optional chaining (u.name?.toUpperCase()).
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Gyakorlati hibakeresési rutin}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Szűkítsd a hibát a legkisebb reprodukálható példára.
|
||||||
|
\item Ellenőrizd a bemenet tényleges futásidejű értékét.
|
||||||
|
\item Egyeztesd a runtime adatot a deklarált típussal.
|
||||||
|
\item Írj tesztet, hogy a hiba ne térjen vissza.
|
||||||
|
\end{enumerate}
|
||||||
|
\end{frame}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
\section{Gyakorlóműhely}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Feladat 1: Típusos szűrés}
|
||||||
|
\begin{block}{Feladat}
|
||||||
|
Készíts függvényt, amely egy vegyes (string | number) tömbből csak a számokat adja vissza.
|
||||||
|
\end{block}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
function onlyNumbers(values: Array<string | number>): number[] {
|
||||||
|
return values.filter((v): v is number => typeof v === "number");
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Feladat 2: Generic API válasz}
|
||||||
|
\begin{block}{Feladat}
|
||||||
|
Definiálj újrafelhasználható API-válasz típust data, ok és error mezőkkel.
|
||||||
|
\end{block}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type ApiResponse<T> = {
|
||||||
|
data: T;
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
\end{lstlisting}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Feladat 3: Runtime validáció}
|
||||||
|
\begin{block}{Feladat}
|
||||||
|
Írj type guardot, amely ellenőrzi, hogy az adat Todo-e.
|
||||||
|
\end{block}
|
||||||
|
\begin{lstlisting}[style=JavaScript]
|
||||||
|
type Todo = { id: number; title: string; done: boolean };
|
||||||
|
|
||||||
|
function isTodo(v: unknown): v is Todo {
|
||||||
|
return (
|
||||||
|
typeof v === "object" && v !== null &&
|
||||||
|
"id" in v && "title" in v && "done" in v
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\begin{frame}[fragile]{Értékelési szempontok}
|
||||||
|
\begin{itemize}
|
||||||
|
\item Típusbiztonság: nincs felesleges any.
|
||||||
|
\item Olvashatóság: beszédes típus- és függvénynév.
|
||||||
|
\item Hibatűrés: külső adat validálása.
|
||||||
|
\item Bővíthetőség: új mező esetén is tiszta marad a kód.
|
||||||
|
\end{itemize}
|
||||||
|
\end{frame}
|
||||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
\documentclass[usenames,dvipsnames,aspectratio=169]{beamer}
|
||||||
|
\usepackage{./common/webfejl}
|
||||||
|
|
||||||
|
% Automatikus frame törés engedélyezése túl hosszú tartalomnál
|
||||||
|
\setbeamertemplate{frametitle continuation}[from second][\insertcontinuationcountroman]
|
||||||
|
|
||||||
|
\title[Webtechnológia és webalkalmazás-fejlesztés - TypeScript]{Webtechnológia és webalkalmazás-fejlesztés - TypeScript}
|
||||||
|
\subtitle{TypeScript}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\begin{frame}[plain]
|
||||||
|
\titlepage
|
||||||
|
\logoalul
|
||||||
|
\end{frame}
|
||||||
|
|
||||||
|
\input{typescript_content.tex}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
% TypeScript tananyag index fajl
|
||||||
|
% Ez a fajl csak osszefuzi a szekciokat kezdotol kozepes szintig.
|
||||||
|
|
||||||
|
\input{sections/01_intro_and_setup.tex}
|
||||||
|
\input{sections/02_basic_types.tex}
|
||||||
|
\input{sections/03_functions_and_unions.tex}
|
||||||
|
\input{sections/04_alias_interface_objects.tex}
|
||||||
|
\input{sections/05_generics_and_utility_types.tex}
|
||||||
|
\input{sections/06_modules_async_and_api.tex}
|
||||||
|
\input{sections/07_validation_and_best_practices.tex}
|
||||||
|
\input{sections/08_learning_path_and_practice.tex}
|
||||||
|
\input{sections/09_advanced_types.tex}
|
||||||
|
\input{sections/10_oop_and_architecture.tex}
|
||||||
|
\input{sections/11_debugging_and_error_reading.tex}
|
||||||
|
\input{sections/12_practice_workshop.tex}
|
||||||
Reference in New Issue
Block a user