negyedik gyakorlat + megoldasok
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user