531 lines
14 KiB
TeX
531 lines
14 KiB
TeX
\section{DTO}
|
|
|
|
\begin{frame}{Mi az a DTO?}
|
|
\begin{block}{Data Transfer Object}
|
|
\begin{itemize}
|
|
\item Adatok szállítására szolgáló objektum
|
|
\item Elválasztja az adatbázis sémát\\
|
|
a kliens felőli interfésztől
|
|
\item Kontroll az átadott adatok felett
|
|
\item Validáció és transzformáció helye
|
|
\end{itemize}
|
|
\end{block}
|
|
|
|
\begin{exampleblock}{Példa}
|
|
Felhasználó entitás tartalmaz jelszó hash-t,\\
|
|
de a DTO nem adja ki.
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Miért használjunk DTO-t?}
|
|
\begin{block}{Előnyök}
|
|
\begin{itemize}
|
|
\item \textbf{Biztonság} - Érzékeny mezők elrejtése (pl. jelszó)
|
|
\item \textbf{Verziókezelés} - API változhat a DB séma változtatása nélkül
|
|
\item \textbf{Validáció} - Bejövő adatok ellenőrzése
|
|
\item \textbf{Dokumentáció} - Világos szerződés a kliens és szerver között
|
|
\item \textbf{Transzformáció} - Adatok átalakítása (pl. dátum formázás)
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Prisma modell vs DTO}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{block}{Prisma Model}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
model User {
|
|
id Int @id
|
|
@default(autoincrement())
|
|
email String @unique
|
|
password String
|
|
name String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{column}
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{block}{UserDTO (kimenet)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
{
|
|
id: 1,
|
|
email: "john@example.com",
|
|
name: "John Doe"
|
|
// password nincs benne!
|
|
// createdAt, updatedAt
|
|
// opcionális
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Input DTO - Létrehozás}
|
|
\begin{block}{CreateUserDTO - Constructor}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class CreateUserDTO {
|
|
constructor(data) {
|
|
this.email = data.email;
|
|
this.password = data.password;
|
|
this.name = data.name;
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Input DTO - Validáció}
|
|
\begin{block}{CreateUserDTO - Validate módszer}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class CreateUserDTO {
|
|
// ...
|
|
|
|
validate() {
|
|
if (!this.email || !this.email.includes('@')) {
|
|
throw new Error('Érvénytelen email cím');
|
|
}
|
|
if (!this.password || this.password.length < 8) {
|
|
throw new Error('Jelszó túl rövid (min. 8 karakter)');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Output DTO - Válasz}
|
|
\begin{block}{UserResponseDTO}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class UserResponseDTO {
|
|
constructor(user) {
|
|
this.id = user.id;
|
|
this.email = user.email;
|
|
this.name = user.name;
|
|
// password nincs másolva!
|
|
}
|
|
static fromPrismaUser(user) {
|
|
return new UserResponseDTO(user);
|
|
}
|
|
static fromManyPrismaUsers(users) {
|
|
return users.map(u => new UserResponseDTO(u));
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{DTO használata service-ben}
|
|
\begin{block}{UserService példa (1)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
const prisma = require('./prisma');
|
|
const bcrypt = require('bcrypt');
|
|
class UserService {
|
|
async createUser(createUserDTO) {
|
|
createUserDTO.validate(); // Validálás
|
|
const hashedPassword = await bcrypt.hash(
|
|
createUserDTO.password, 10
|
|
); // Jelszó hash-elés
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{DTO használata service-ben (folyt.)}
|
|
\begin{block}{UserService példa (2)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: createUserDTO.email,
|
|
password: hashedPassword,
|
|
name: createUserDTO.name
|
|
} // Mentés Prisma-val
|
|
});
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{DTO használata service-ben (folyt.)}
|
|
\begin{block}{UserService példa (3)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
// DTO-vá alakítás visszaadás előtt
|
|
return UserResponseDTO.fromPrismaUser(user);
|
|
}
|
|
async getAllUsers() {
|
|
const users = await prisma.user.findMany();
|
|
return UserResponseDTO.fromManyPrismaUsers(users);
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{DTO használata service-ben (folyt.)}
|
|
\begin{block}{UserService példa (4)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
async getUserById(id) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id }
|
|
});
|
|
if (!user) return null;
|
|
return UserResponseDTO.fromPrismaUser(user);
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Express integráció DTO-val}
|
|
\begin{block}{REST endpoint (POST)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
const express = require('express');
|
|
const userService = new UserService();
|
|
const app = express();
|
|
app.post('/api/users', async (req, res) => {
|
|
try {
|
|
const createDTO = new CreateUserDTO(req.body);
|
|
const userDTO = await userService.createUser(createDTO);
|
|
res.status(201).json(userDTO);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Express integráció DTO-val (folyt.)}
|
|
\begin{block}{REST endpoint (GET)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
app.get('/api/users', async (req, res) => {
|
|
const usersDTO = await userService.getAllUsers();
|
|
res.json(usersDTO);
|
|
});
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Update DTO - Constructor}
|
|
\begin{block}{UpdateUserDTO}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class UpdateUserDTO {
|
|
constructor(data) {
|
|
// Csak a megadott mezők kerülnek be
|
|
if (data.email !== undefined)
|
|
this.email = data.email;
|
|
if (data.name !== undefined)
|
|
this.name = data.name;
|
|
if (data.password !== undefined)
|
|
this.password = data.password;
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Update DTO - Validáció}
|
|
\begin{block}{UpdateUserDTO - Validate}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class UpdateUserDTO {
|
|
// ...
|
|
|
|
validate() {
|
|
if (this.email && !this.email.includes('@')) {
|
|
throw new Error('Érvénytelen email');
|
|
}
|
|
if (this.password && this.password.length < 8) {
|
|
throw new Error('Jelszó túl rövid');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Nested DTO - Relációkkal}
|
|
\begin{block}{PostWithAuthorDTO}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class PostWithAuthorDTO {
|
|
constructor(post) {
|
|
this.id = post.id;
|
|
this.title = post.title;
|
|
this.content = post.content;
|
|
this.createdAt = post.createdAt;
|
|
// Nested DTO az author-hoz
|
|
if (post.author) {
|
|
this.author = new UserResponseDTO(post.author);
|
|
}
|
|
}
|
|
static fromPrismaPost(post) {
|
|
return new PostWithAuthorDTO(post);
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Nested DTO használata}
|
|
\begin{block}{Service metódus}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class PostService {
|
|
async getPostWithAuthor(postId) {
|
|
const post = await prisma.post.findUnique({
|
|
where: { id: postId },
|
|
include: {
|
|
author: true
|
|
}
|
|
});
|
|
|
|
if (!post) return null;
|
|
|
|
return PostWithAuthorDTO.fromPrismaPost(post);
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Validációs könyvtár - Joi}
|
|
\begin{block}{Joi telepítése}
|
|
\begin{lstlisting}[language=bash]
|
|
npm install joi
|
|
\end{lstlisting}
|
|
\end{block}
|
|
|
|
\begin{block}{Validációs séma}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
const Joi = require('joi');
|
|
|
|
const createUserSchema = Joi.object({
|
|
email: Joi.string().email().required(),
|
|
password: Joi.string().min(8).required(),
|
|
name: Joi.string().optional()
|
|
});
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{DTO Joi validációval}
|
|
\begin{block}{CreateUserDTO sémával}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class CreateUserDTO {
|
|
constructor(data) {
|
|
const { error, value } =
|
|
createUserSchema.validate(data);
|
|
if (error)
|
|
throw new Error(error.details[0].message);
|
|
Object.assign(this, value);
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Pagináció DTO}
|
|
\begin{block}{PaginatedResponseDTO}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class PaginatedResponseDTO {
|
|
constructor(data, total, page, pageSize) {
|
|
this.data = data;
|
|
this.pagination = {
|
|
total, page, pageSize,
|
|
totalPages: Math.ceil(total / pageSize)
|
|
};
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Pagináció DTO használata}
|
|
\begin{block}{PaginatedResponseDTOUsage}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
// Használat
|
|
const users = await prisma.user.findMany({
|
|
skip: (page - 1) * pageSize, take: pageSize
|
|
});
|
|
const total = await prisma.user.count();
|
|
return new PaginatedResponseDTO(
|
|
UserResponseDTO.fromManyPrismaUsers(users),
|
|
total, page, pageSize
|
|
);
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Szűrési DTO - Constructor}
|
|
\begin{block}{UserFilterDTO}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class UserFilterDTO {
|
|
constructor(query) {
|
|
this.email = query.email;
|
|
this.name = query.name;
|
|
this.page = parseInt(query.page) || 1;
|
|
this.pageSize = parseInt(query.pageSize) || 10;
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Szűrési DTO - Prisma konverzió}
|
|
\begin{block}{toPrismaWhere módszer}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
class UserFilterDTO {
|
|
// ...
|
|
|
|
toPrismaWhere() {
|
|
const where = {};
|
|
if (this.email)
|
|
where.email = { contains: this.email };
|
|
if (this.name)
|
|
where.name = { contains: this.name };
|
|
return where;
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Szűrési DTO használata}
|
|
\begin{block}{Express endpoint szűréssel (1)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
app.get('/api/users', async (req, res) => {
|
|
const filterDTO = new UserFilterDTO(req.query);
|
|
const where = filterDTO.toPrismaWhere();
|
|
const users = await prisma.user.findMany({
|
|
where,
|
|
skip: (filterDTO.page - 1) * filterDTO.pageSize,
|
|
take: filterDTO.pageSize
|
|
});
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Szűrési DTO használata (folyt.)}
|
|
\begin{block}{Express endpoint szűréssel (2)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
const total = await prisma.user.count({ where });
|
|
const result = new PaginatedResponseDTO(
|
|
UserResponseDTO.fromManyPrismaUsers(users),
|
|
total, filterDTO.page, filterDTO.pageSize
|
|
);
|
|
res.json(result);
|
|
});
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Mapper függvények}
|
|
\begin{block}{Alternatív megközelítés (1)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
function toUserResponseDTO(user) {
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name
|
|
};
|
|
}
|
|
function toUserResponseDTOs(users) {
|
|
return users.map(toUserResponseDTO);
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Mapper függvények (folyt.)}
|
|
\begin{block}{Alternatív megközelítés (2)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
module.exports = {
|
|
toUserResponseDTO,
|
|
toUserResponseDTOs
|
|
};
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Mapper használata}
|
|
\begin{block}{Service-ben (1)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
const { toUserResponseDTO, toUserResponseDTOs }
|
|
= require('./dto/user.mapper');
|
|
class UserService {
|
|
async getAllUsers() {
|
|
const users = await prisma.user.findMany();
|
|
return toUserResponseDTOs(users);
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Mapper használata (folyt.)}
|
|
\begin{block}{Service-ben (2)}
|
|
\begin{lstlisting}[language=JavaScript]
|
|
async getUserById(id) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id }
|
|
});
|
|
if (!user) return null;
|
|
return toUserResponseDTO(user);
|
|
}
|
|
}
|
|
\end{lstlisting}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}{DTO Best Practices}
|
|
\begin{block}{Ajánlások}
|
|
\begin{itemize}
|
|
\item Külön DTO minden use case-hez (Create, Update, Response)
|
|
\item Ne add vissza közvetlenül a Prisma modellt
|
|
\item Validálj minden bejövő adatot
|
|
\item Használj validációs könyvtárat (Joi, Yup, Zod)
|
|
\item Tartsd egyszerűnek - ne túlbonyolítsd
|
|
\item Dokumentáld a DTO mezőket
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}{DTO vs Prisma Select}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{block}{Prisma Select}
|
|
\begin{itemize}
|
|
\item Egyszerű mezők kiválasztása
|
|
\item Kevesebb kód
|
|
\item Nincs validáció
|
|
\item DB szintű szűrés
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{column}
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{block}{DTO}
|
|
\begin{itemize}
|
|
\item Komplex transzformációk
|
|
\item Validáció és logika
|
|
\item Típusbiztonság
|
|
\item API verziókezelés
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{column}
|
|
\end{columns}
|
|
|
|
\vspace{0.5cm}
|
|
|
|
\begin{alertblock}{Kombináld őket!}
|
|
Használd a Prisma select-et a DB szinten, és DTO-t a transzformációhoz.
|
|
\end{alertblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Összefoglalás}
|
|
\begin{block}{DTO előnyei}
|
|
\begin{itemize}
|
|
\item \textbf{Biztonság} - Érzékeny adatok elrejtése
|
|
\item \textbf{Validáció} - Adatok ellenőrzése
|
|
\item \textbf{Transzformáció} - Adatok átalakítása
|
|
\item \textbf{Dokumentáció} - Világos API szerződés
|
|
\item \textbf{Karbantarthatóság} - Könnyebb változtatások
|
|
\item \textbf{Tesztelhetőség} - Egyszerűbb unit tesztek
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{frame}
|