negyedik gyakorlat + megoldasok

This commit is contained in:
magdo
2026-03-04 20:02:39 +01:00
parent afc3777ac9
commit 388aa908de
217 changed files with 19791 additions and 0 deletions
@@ -0,0 +1,2 @@
PORT=3000
DATABASE_URL="file:./dev.db"
@@ -0,0 +1,5 @@
node_modules/
.env
dev.db
dev.db-journal
prisma/migrations/
@@ -0,0 +1,80 @@
# Második Gyakorlat - MINTA Megoldás
Ez a mappa tartalmazza a második gyakorlat **teljes, működő megoldását**.
## Amit implementáltunk:
### 1. **Prisma ORM + SQLite**
- Prisma schema definiálása
- User model adatbázis szinten
- Migrációk kezelése
### 2. **CQRS Pattern + Layered Architecture**
- **API Layer**: Controllers, Routes
- **Application Layer**: Commands, Queries, Handlers
- **Domain Layer**: Models, Interfaces
- **Infrastructure Layer**: Prisma, Repositories
### 3. **Repository Pattern**
- `IUserRepository` interface
- `UserRepository` Prisma implementáció
- Minden adatbázis művelet a repository-n keresztül
### 4. **Domain Model**
- User osztály üzleti logikával
- Validációk
- Helper metódusok (isAdmin, canEdit, validate)
## Indítás
```bash
npm install
npx prisma generate
npx prisma migrate dev --name init
npm run dev
```
A szerver elindul a `http://localhost:3000` címen.
## API Endpoints
- `GET /api/users` - Összes user
- `GET /api/users/:id` - Egy user
- `POST /api/users` - User létrehozása
- `PUT /api/users/:id` - User frissítése
- `DELETE /api/users/:id` - User törlése
## Példa Használat
```bash
# User létrehozása
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "test@test.com",
"name": "Test User",
"password": "password123"
}'
# Összes user lekérése
curl http://localhost:3000/api/users
# User frissítése
curl -X PUT http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Name"
}'
# User törlése
curl -X DELETE http://localhost:3000/api/users/1
```
## Tanulási Pontok
1. **Prisma ORM**: Modern ORM használata Node.js-ben
2. **SQLite**: Egyszerű file-based adatbázis
3. **CQRS**: Command/Query szétválasztás
4. **Layered Architecture**: Rétegezett architektúra
5. **Domain Model**: Üzleti logika a domain rétegben
6. **Repository Pattern**: Adatelérés absztrakciója
@@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "js,json",
"ignore": ["node_modules"],
"exec": "node ./src/api/server.js"
}
@@ -0,0 +1,25 @@
{
"name": "user-management-backend-minta",
"version": "1.0.0",
"description": "MINTA - User management backend with CQRS and layered architecture",
"main": "./src/api/server.js",
"scripts": {
"start": "node ./src/api/server.js",
"dev": "nodemon ./src/api/server.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
},
"keywords": ["express", "prisma", "sqlite", "cqrs"],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"@prisma/client": "^5.8.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.2",
"prisma": "^5.8.0"
}
}
@@ -0,0 +1,21 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String
role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -0,0 +1,116 @@
const CreateUserCommand = require('../../application/user/command/CreateUserCommand');
const CreateUserHandler = require('../../application/user/command/CreateUserHandler');
const UpdateUserCommand = require('../../application/user/command/UpdateUserCommand');
const UpdateUserHandler = require('../../application/user/command/UpdateUserHandler');
const DeleteUserCommand = require('../../application/user/command/DeleteUserCommand');
const DeleteUserHandler = require('../../application/user/command/DeleteUserHandler');
const GetUserQuery = require('../../application/user/query/GetUserQuery');
const GetUserHandler = require('../../application/user/query/GetUserHandler');
const GetAllUsersQuery = require('../../application/user/query/GetAllUsersQuery');
const GetAllUsersHandler = require('../../application/user/query/GetAllUsersHandler');
class UserController {
constructor() {
this.createUserHandler = new CreateUserHandler();
this.updateUserHandler = new UpdateUserHandler();
this.deleteUserHandler = new DeleteUserHandler();
this.getUserHandler = new GetUserHandler();
this.getAllUsersHandler = new GetAllUsersHandler();
}
async getAllUsers(req, res) {
try {
const query = new GetAllUsersQuery();
const users = await this.getAllUsersHandler.handle(query);
res.json({
success: true,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
async getUserById(req, res) {
try {
const query = new GetUserQuery(parseInt(req.params.id));
const user = await this.getUserHandler.handle(query);
res.json({
success: true,
data: user
});
} catch (error) {
res.status(404).json({
success: false,
error: error.message
});
}
}
async createUser(req, res) {
try {
const { email, name, password, role } = req.body;
const command = new CreateUserCommand(email, name, password, role);
const user = await this.createUserHandler.handle(command);
res.status(201).json({
success: true,
data: user,
message: 'User sikeresen létrehozva'
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
async updateUser(req, res) {
try {
const { email, name, role } = req.body;
const command = new UpdateUserCommand(
parseInt(req.params.id),
email,
name,
role
);
const user = await this.updateUserHandler.handle(command);
res.json({
success: true,
data: user,
message: 'User sikeresen frissítve'
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
async deleteUser(req, res) {
try {
const command = new DeleteUserCommand(parseInt(req.params.id));
const result = await this.deleteUserHandler.handle(command);
res.json({
success: true,
message: result.message
});
} catch (error) {
res.status(404).json({
success: false,
error: error.message
});
}
}
}
module.exports = UserController;
@@ -0,0 +1,16 @@
const express = require('express');
const UserController = require('../controllers/UserController');
const router = express.Router();
const userController = new UserController();
// Query routes (olvasás)
router.get('/', (req, res) => userController.getAllUsers(req, res));
router.get('/:id', (req, res) => userController.getUserById(req, res));
// Command routes (írás)
router.post('/', (req, res) => userController.createUser(req, res));
router.put('/:id', (req, res) => userController.updateUser(req, res));
router.delete('/:id', (req, res) => userController.deleteUser(req, res));
module.exports = router;
@@ -0,0 +1,51 @@
require('dotenv').config();
const express = require('express');
const userRoutes = require('./routes/user.routes');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/users', userRoutes);
// Alapértelmezett route
app.get('/', (req, res) => {
res.json({
message: 'User Management API - MINTA Megoldás',
endpoints: {
users: {
getAll: 'GET /api/users',
getById: 'GET /api/users/:id',
create: 'POST /api/users',
update: 'PUT /api/users/:id',
delete: 'DELETE /api/users/:id'
}
}
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'Endpoint nem található!'
});
});
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: 'Szerver hiba történt!'
});
});
app.listen(PORT, () => {
console.log(`✅ Szerver fut a http://localhost:${PORT} címen`);
console.log(`📊 Prisma Studio: npx prisma studio`);
});
@@ -0,0 +1,10 @@
class CreateUserCommand {
constructor(email, name, password, role) {
this.email = email;
this.name = name;
this.password = password;
this.role = role;
}
}
module.exports = CreateUserCommand;
@@ -0,0 +1,41 @@
const UserRepository = require('../../../infrastructure/repositories/UserRepository');
class CreateUserHandler {
constructor() {
this.userRepository = new UserRepository();
}
async handle(command) {
// 1. Validáció
if (!command.email || !command.name || !command.password) {
throw new Error('Email, name és password kötelező');
}
// 2. Email formátum ellenőrzés
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(command.email)) {
throw new Error('Hibás email formátum');
}
// 3. Jelszó hossz ellenőrzés
if (command.password.length < 6) {
throw new Error('Jelszó minimum 6 karakter kell legyen');
}
// 4. Duplikáció ellenőrzés
const existing = await this.userRepository.findByEmail(command.email);
if (existing) {
throw new Error('Email már használatban van');
}
// 5. User létrehozása
return await this.userRepository.create({
email: command.email,
name: command.name,
password: command.password, // Production környezetben hash-elni kell!
role: command.role || 'user'
});
}
}
module.exports = CreateUserHandler;
@@ -0,0 +1,7 @@
class DeleteUserCommand {
constructor(id) {
this.id = id;
}
}
module.exports = DeleteUserCommand;
@@ -0,0 +1,22 @@
const UserRepository = require('../../../infrastructure/repositories/UserRepository');
class DeleteUserHandler {
constructor() {
this.userRepository = new UserRepository();
}
async handle(command) {
const user = await this.userRepository.findById(command.id);
if (!user) {
throw new Error('User nem található');
}
await this.userRepository.delete(command.id);
return {
message: 'User sikeresen törölve',
id: command.id
};
}
}
module.exports = DeleteUserHandler;
@@ -0,0 +1,10 @@
class UpdateUserCommand {
constructor(id, email, name, role) {
this.id = id;
this.email = email;
this.name = name;
this.role = role;
}
}
module.exports = UpdateUserCommand;
@@ -0,0 +1,34 @@
const UserRepository = require('../../../infrastructure/repositories/UserRepository');
class UpdateUserHandler {
constructor() {
this.userRepository = new UserRepository();
}
async handle(command) {
// 1. User létezésének ellenőrzése
const user = await this.userRepository.findById(command.id);
if (!user) {
throw new Error('User nem található');
}
// 2. Email egyediség ellenőrzése (ha változott)
if (command.email && command.email !== user.email) {
const existing = await this.userRepository.findByEmail(command.email);
if (existing) {
throw new Error('Email már használatban van');
}
}
// 3. Frissítendő adatok összeállítása
const updateData = {};
if (command.email) updateData.email = command.email;
if (command.name) updateData.name = command.name;
if (command.role) updateData.role = command.role;
// 4. Frissítés végrehajtása
return await this.userRepository.update(command.id, updateData);
}
}
module.exports = UpdateUserHandler;
@@ -0,0 +1,13 @@
const UserRepository = require('../../../infrastructure/repositories/UserRepository');
class GetAllUsersHandler {
constructor() {
this.userRepository = new UserRepository();
}
async handle(query) {
return await this.userRepository.findAll();
}
}
module.exports = GetAllUsersHandler;
@@ -0,0 +1,5 @@
class GetAllUsersQuery {
constructor() {}
}
module.exports = GetAllUsersQuery;
@@ -0,0 +1,17 @@
const UserRepository = require('../../../infrastructure/repositories/UserRepository');
class GetUserHandler {
constructor() {
this.userRepository = new UserRepository();
}
async handle(query) {
const user = await this.userRepository.findById(query.id);
if (!user) {
throw new Error('User nem található');
}
return user;
}
}
module.exports = GetUserHandler;
@@ -0,0 +1,7 @@
class GetUserQuery {
constructor(id) {
this.id = id;
}
}
module.exports = GetUserQuery;
@@ -0,0 +1,33 @@
/**
* IUserRepository Interface
*
* Ez definiálja a UserRepository-nak kötelező metódusokat.
* A Repository Pattern szerint az Infrastructure réteg implementálja ezt.
*/
class IUserRepository {
async create(userData) {
throw new Error('create() metódus nincs implementálva');
}
async findById(id) {
throw new Error('findById() metódus nincs implementálva');
}
async findByEmail(email) {
throw new Error('findByEmail() metódus nincs implementálva');
}
async findAll() {
throw new Error('findAll() metódus nincs implementálva');
}
async update(id, userData) {
throw new Error('update() metódus nincs implementálva');
}
async delete(id) {
throw new Error('delete() metódus nincs implementálva');
}
}
module.exports = IUserRepository;
@@ -0,0 +1,56 @@
class User {
constructor(data) {
this.id = data.id;
this.email = data.email;
this.name = data.name;
this.password = data.password;
this.role = data.role || 'user';
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
}
/**
* Ellenőrzi, hogy a user admin-e
*/
isAdmin() {
return this.role === 'admin';
}
/**
* Ellenőrzi, hogy a user szerkeszthet-e egy másik usert
* @param {User} targetUser - A szerkeszteni kívánt user
*/
canEdit(targetUser) {
return this.isAdmin() || this.id === targetUser.id;
}
/**
* User adatok validálása
*/
validate() {
if (!this.email || !this.name || !this.password) {
throw new Error('Email, name és password kötelező');
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(this.email)) {
throw new Error('Hibás email formátum');
}
if (this.password.length < 6) {
throw new Error('Jelszó min. 6 karakter');
}
return true;
}
/**
* User objektum JSON-ként (jelszó nélkül!)
*/
toJSON() {
const { password, ...userWithoutPassword } = this;
return userWithoutPassword;
}
}
module.exports = User;
@@ -0,0 +1,7 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient({
log: ['query', 'error', 'warn'],
});
module.exports = prisma;
@@ -0,0 +1,79 @@
const prisma = require('../database/prisma');
const IUserRepository = require('../../domain/interfaces/IUserRepository');
/**
* UserRepository - Prisma implementáció
*
* Ez az osztály implementálja az IUserRepository interfészt
* és Prisma ORM-et használ az adatbázis műveletekhez.
*/
class UserRepository extends IUserRepository {
/**
* User létrehozása
* @param {Object} userData - { email, name, password, role }
* @returns {Promise<User>}
*/
async create(userData) {
return await prisma.user.create({
data: userData
});
}
/**
* User keresése ID alapján
* @param {number} id
* @returns {Promise<User|null>}
*/
async findById(id) {
return await prisma.user.findUnique({
where: { id: parseInt(id) }
});
}
/**
* User keresése email alapján
* @param {string} email
* @returns {Promise<User|null>}
*/
async findByEmail(email) {
return await prisma.user.findUnique({
where: { email }
});
}
/**
* Összes user lekérése
* @returns {Promise<User[]>}
*/
async findAll() {
return await prisma.user.findMany({
orderBy: { createdAt: 'desc' }
});
}
/**
* User frissítése
* @param {number} id
* @param {Object} userData - Frissítendő mezők
* @returns {Promise<User>}
*/
async update(id, userData) {
return await prisma.user.update({
where: { id: parseInt(id) },
data: userData
});
}
/**
* User törlése
* @param {number} id
* @returns {Promise<User>}
*/
async delete(id) {
return await prisma.user.delete({
where: { id: parseInt(id) }
});
}
}
module.exports = UserRepository;