Skip to content

Commit

Permalink
refactor: create dtos for requests and responses
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Nov 27, 2023
1 parent baa5c2c commit f48d65e
Show file tree
Hide file tree
Showing 38 changed files with 334 additions and 305 deletions.
4 changes: 2 additions & 2 deletions src/app/settings/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { redirect } from 'next/navigation'

import EditProfileForm from '@/components/edit-profile-form'
import { UserDTO } from '@/core/user/application/types'
import UserResponse from '@/core/user/dto/responses/user.response'
import { findUser } from '@/core/user/infrastructure/actions/find-user'
import { auth } from '@/lib/auth/auth'

Expand All @@ -12,7 +12,7 @@ export default async function Page() {
return redirect('/')
}

const user = (await findUser(email)) as UserDTO
const user = (await findUser(email)) as UserResponse

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions src/components/book-grid/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Book from '@/components/book'
import { BookDTO } from '@/core/book/application/types'
import BookResponse from '@/core/book/dto/responses/book.response'

export interface BookGridProperties {
books: BookDTO[]
books: BookResponse[]
}

export default function BookGrid(properties: BookGridProperties) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/book/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Card, CardBody, CardFooter, Image } from '@nextui-org/react'

import { BookDTO } from '@/core/book/application/types'
import BookResponse from '@/core/book/dto/responses/book.response'

export interface BookProperties {
book: BookDTO
book: BookResponse
}

export default async function Book(properties: BookProperties) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/edit-profile-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { useFormState } from 'react-dom'
import InputForm from '@/components/input-form'
import SubmitButton from '@/components/submit-button'
import { showToast } from '@/components/toast'
import { UserDTO } from '@/core/user/application/types'
import UserResponse from '@/core/user/dto/responses/user.response'
import { updateUser } from '@/core/user/infrastructure/actions/update-user'
import FormResponse from '@/lib/zod/form-response'

interface EditProfileFormProperties {
user: UserDTO
user: UserResponse
}

export default function EditProfileForm(properties: EditProfileFormProperties) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/header/header-authenticated-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import {
} from '@nextui-org/react'
import { useRouter } from 'next/navigation'

import { UserDTO } from '@/core/user/application/types'
import UserResponse from '@/core/user/dto/responses/user.response'
import gravatar from '@/lib/utils/gravatar'

interface HeaderAuthenticatedMenuProperties {
user: UserDTO
user: UserResponse
}

export default function HeaderAuthenticatedMenu(
Expand Down
4 changes: 2 additions & 2 deletions src/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import Link from 'next/link'
import HeaderAuthenticatedMenu from '@/components/header/header-authenticated-menu'
import HeaderUnauthenticatedMenu from '@/components/header/header-unauthenticated-menu'
import ThemeSwitcher from '@/components/theme-switcher'
import { UserDTO } from '@/core/user/application/types'
import UserResponse from '@/core/user/dto/responses/user.response'

interface HeaderProperties {
user?: UserDTO
user?: UserResponse
}

export default function Header(properties: HeaderProperties) {
Expand Down
39 changes: 11 additions & 28 deletions src/core/book/application/create-book.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import { ulid } from 'ulid'

import CreateBookUseCase from '@/core/book/application/create-book.use-case'
import { CreateBookCommand } from '@/core/book/application/types'
import BookIdAlreadyExistsError from '@/core/book/domain/errors/book-id-already-exists.error'
import Book from '@/core/book/domain/model/book.entity'
import CreateBookRequest from '@/core/book/dto/requests/create-book.request'
import BookResponse from '@/core/book/dto/responses/book.response'
import BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import unexpected from '@/lib/utils/unexpected'
import BooksExamples from '@/tests/examples/books.examples'

describe('CreateBookUseCase', () => {
it('should create a new book', async () => {
// Arrange
const books = new BooksInMemory()
const book = Book.create({
authors: ['Jane Doe'],
id: ulid(),
image: 'http://example.com/book.jpeg',
title: 'A book',
})._unsafeUnwrap()
const book = BooksExamples.basic()

const command = CreateBookCommand.with({
authors: book.authors.map((author) => author.value),
id: book.id.value,
image: book.image.value,
title: book.title.value,
})
const command = CreateBookRequest.with(
BookResponse.fromModel(book) satisfies CreateBookRequest,
)
const useCase = new CreateBookUseCase(books)

// Act
Expand All @@ -42,20 +33,12 @@ describe('CreateBookUseCase', () => {
it('should rejects to create a book with the same id', async () => {
// Arrange
const books = new BooksInMemory()
const book = Book.create({
authors: ['Jane Doe'],
id: ulid(),
image: 'http://example.com/book.jpeg',
title: 'A book',
})._unsafeUnwrap()
const book = BooksExamples.basic()
books.books.set(book.id.value, book)

const command = CreateBookCommand.with({
authors: book.authors.map((author) => author.value),
id: book.id.value,
image: book.image.value,
title: book.title.value,
})
const command = CreateBookRequest.with(
BookResponse.fromModel(book) satisfies CreateBookRequest,
)
const useCase = new CreateBookUseCase(books)

// Act
Expand Down
11 changes: 6 additions & 5 deletions src/core/book/application/create-book.use-case.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { errAsync } from 'neverthrow'

import BookIdAlreadyExistsError from '@/core/book/domain/errors/book-id-already-exists.error'
import Book from '@/core/book/domain/model/book.entity'
import BookFactory from '@/core/book/domain/model/book.factory'
import Books from '@/core/book/domain/services/books.repository'
import CreateBookRequest from '@/core/book/dto/requests/create-book.request'
import BookId from '@/core/common/domain/value-objects/book-id'

import { CreateBookCommand } from './types'

export default class CreateBookUseCase {
constructor(private readonly books: Books) {}

async with(command: CreateBookCommand) {
async with(command: CreateBookRequest) {
return await BookId.create(command.id)
.asyncAndThen((bookId) => this.books.findById(bookId))
.match(
(book) => errAsync(BookIdAlreadyExistsError.withId(book.id)),
() =>
Book.create(command).asyncAndThen((_book) => this.books.save(_book)),
BookFactory.create(command).asyncAndThen((_book) =>
this.books.save(_book),
),
)
}
}
15 changes: 4 additions & 11 deletions src/core/book/application/find-books.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { ulid } from 'ulid'

import FindBooksUseCase from '@/core/book/application/find-books.use-case'
import { BookDTO } from '@/core/book/application/types'
import Book from '@/core/book/domain/model/book.entity'
import BookResponse from '@/core/book/dto/responses/book.response'
import BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import unexpected from '@/lib/utils/unexpected'
import BooksExamples from '@/tests/examples/books.examples'

describe('FindBooksUseCase', () => {
it('should get all books', async () => {
// Arrange
const books = new BooksInMemory()
const book = Book.create({
authors: ['Jane Doe'],
id: ulid(),
image: 'http://example.com/book.jpeg',
title: 'A book',
})._unsafeUnwrap()
const book = BooksExamples.basic()
books.books.set(book.id.value, book)

const useCase = new FindBooksUseCase(books)
Expand All @@ -26,7 +19,7 @@ describe('FindBooksUseCase', () => {
// Assert
result.match(
(_books) => {
expect(_books).toEqual([BookDTO.fromModel(book)])
expect(_books).toEqual([BookResponse.fromModel(book)])
},
(error) => unexpected.error(error),
)
Expand Down
6 changes: 3 additions & 3 deletions src/core/book/application/find-books.use-case.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { okAsync, ResultAsync } from 'neverthrow'

import { BookDTO } from '@/core/book/application/types'
import Books from '@/core/book/domain/services/books.repository'
import BookResponse from '@/core/book/dto/responses/book.response'
import ApplicationError from '@/core/common/domain/errors/application-error'

export default class FindBooksUseCase {
constructor(private readonly books: Books) {}

with(): ResultAsync<BookDTO[], ApplicationError> {
with(): ResultAsync<BookResponse[], ApplicationError> {
return this.books.findAll().andThen((books) => {
return okAsync(books.map((book) => BookDTO.fromModel(book)))
return okAsync(books.map((book) => BookResponse.fromModel(book)))
})
}
}
33 changes: 0 additions & 33 deletions src/core/book/application/types.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/core/book/domain/errors/book-domain.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import FullNameError from '@/core/common/domain/value-objects/fullname/fullname.error'
import IdError from '@/core/common/domain/value-objects/id/id.error'
import ImageError from '@/core/common/domain/value-objects/image/image.error'
import TitleError from '@/core/common/domain/value-objects/title/title.error'

type BookDomainError = IdError | TitleError | FullNameError | ImageError

export default BookDomainError
29 changes: 0 additions & 29 deletions src/core/book/domain/model/book.entity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { ok, Result, safeTry } from 'neverthrow'

import { BookDTO } from '@/core/book/application/types'
import DomainError from '@/core/common/domain/errors/domain-error'
import BookId from '@/core/common/domain/value-objects/book-id'
import FullNameError from '@/core/common/domain/value-objects/fullname/fullname.error'
import FullNames from '@/core/common/domain/value-objects/fullnames'
import IdError from '@/core/common/domain/value-objects/id/id.error'
import Image from '@/core/common/domain/value-objects/image'
import ImageError from '@/core/common/domain/value-objects/image/image.error'
import Title from '@/core/common/domain/value-objects/title'
import TitleError from '@/core/common/domain/value-objects/title/title.error'

export default class Book {
constructor(
Expand All @@ -19,27 +11,6 @@ export default class Book {
private _image: Image,
) {}

static create(
bookDTO: BookDTO,
): Result<Book, IdError | TitleError | FullNameError | ImageError> {
return safeTry<Book, DomainError>(function* () {
const bookId = yield* BookId.create(bookDTO.id)
.mapErr((error) => error)
.safeUnwrap()
const title = yield* Title.create(bookDTO.title)
.mapErr((error) => error)
.safeUnwrap()
const authors = yield* FullNames.create(bookDTO.authors)
.mapErr((error) => error)
.safeUnwrap()
const image = yield* Image.create(bookDTO.image)
.mapErr((error) => error)
.safeUnwrap()

return ok(new Book(bookId, title, authors, image))
})
}

get id(): BookId {
return this._id
}
Expand Down
39 changes: 39 additions & 0 deletions src/core/book/domain/model/book.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ok, Result, safeTry } from 'neverthrow'

import BookDomainError from '@/core/book/domain/errors/book-domain.error'
import Book from '@/core/book/domain/model/book.entity'
import BookResponse from '@/core/book/dto/responses/book.response'
import BookId from '@/core/common/domain/value-objects/book-id'
import FullName from '@/core/common/domain/value-objects/fullname'
import FullNames from '@/core/common/domain/value-objects/fullnames'
import Image from '@/core/common/domain/value-objects/image'
import Title from '@/core/common/domain/value-objects/title'

const BookFactory = {
create: (bookResponse: BookResponse): Result<Book, BookDomainError> =>
safeTry<Book, BookDomainError>(function* () {
const bookId = yield* BookId.create(bookResponse.id)
.mapErr((error) => error)
.safeUnwrap()
const title = yield* Title.create(bookResponse.title)
.mapErr((error) => error)
.safeUnwrap()
const authors = yield* FullNames.create(bookResponse.authors)
.mapErr((error) => error)
.safeUnwrap()
const image = yield* Image.create(bookResponse.image)
.mapErr((error) => error)
.safeUnwrap()

return ok(new Book(bookId, title, authors, image))
}),
with: (bookResponse: BookResponse): Book =>
new Book(
new BookId(bookResponse.id),
new Title(bookResponse.title),
new FullNames(bookResponse.authors.map((author) => new FullName(author))),
new Image(bookResponse.image),
),
}

export default BookFactory
14 changes: 14 additions & 0 deletions src/core/book/dto/requests/create-book.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { DeepReadonly } from 'ts-essentials'

type CreateBookRequest = DeepReadonly<{
authors: string[]
id: string
image: string
title: string
}>

const CreateBookRequest = {
with: (properties: CreateBookRequest) => properties,
}

export default CreateBookRequest
22 changes: 22 additions & 0 deletions src/core/book/dto/responses/book.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DeepReadonly } from 'ts-essentials'

import Book from '@/core/book/domain/model/book.entity'

type BookResponse = DeepReadonly<{
authors: string[]
id: string
image: string
title: string
}>

const BookResponse = {
fromModel: (book: Book): BookResponse => ({
authors: book.authors.map((author) => author.value),
id: book.id.value,
image: book.image.value,
title: book.title.value,
}),
with: (properties: BookResponse) => properties,
}

export default BookResponse
Loading

0 comments on commit f48d65e

Please sign in to comment.