Skip to content

Commit

Permalink
refactor: update book with new core logic
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Nov 26, 2023
1 parent b9cad07 commit baa5c2c
Show file tree
Hide file tree
Showing 42 changed files with 790 additions and 448 deletions.
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"@auth/prisma-adapter": "^1.0.8",
"@heroicons/react": "^2.0.18",
"@nextui-org/react": "2.2.9",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "^5.6.0",
"framer-motion": "^10.16.5",
"neverthrow": "^6.1.0",
Expand All @@ -40,6 +39,8 @@
"react-hot-toast": "^2.4.1",
"server-only": "^0.0.1",
"sharp": "^0.32.6",
"ts-essentials": "^9.4.1",
"ulid": "^2.3.0",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
Expand Down Expand Up @@ -71,7 +72,6 @@
"eslint-config-next": "14.0.3",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-neverthrow": "^1.1.4",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-sort": "^2.11.0",
Expand Down Expand Up @@ -128,7 +128,6 @@
"simple-import-sort",
"sort",
"unused-imports",
"neverthrow",
"prettier"
],
"root": true,
Expand All @@ -148,13 +147,13 @@
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"neverthrow/must-use-result": "warn",
"no-shadow": "off",
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error",
"sort/destructuring-properties": "error",
"sort/object-properties": "error",
"sort/type-properties": "error",
"unicorn/no-useless-undefined": "off",
"unicorn/prefer-node-protocol": "off",
"unused-imports/no-unused-imports": "error"
}
Expand Down
72 changes: 72 additions & 0 deletions src/core/book/application/create-book.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import unexpected from '@/lib/utils/unexpected'

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 command = CreateBookCommand.with({
authors: book.authors.map((author) => author.value),
id: book.id.value,
image: book.image.value,
title: book.title.value,
})
const useCase = new CreateBookUseCase(books)

// Act
const result = await useCase.with(command)

// Assert
result.match(
(_book) => {
const savedBook = books.books.get(_book.id.value)
expect(savedBook).toEqual(_book)
},
(error) => unexpected.error(error),
)
})

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()
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 useCase = new CreateBookUseCase(books)

// Act
const result = await useCase.with(command)

// Assert
result.match(
(success) => unexpected.success(success),
(error) => {
expect(error).toBeInstanceOf(BookIdAlreadyExistsError)
},
)
})
})
39 changes: 16 additions & 23 deletions src/core/book/application/create-book.use-case.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import { err, ok, Result } from 'neverthrow'
import { errAsync } from 'neverthrow'

import Book from '../domain/model/book.entity'
import BookId from '../domain/model/id.value-object'
import Books from '../domain/services/books.repository'
import { BookError, CreateBookCommand } from './types'
import BookIdAlreadyExistsError from '@/core/book/domain/errors/book-id-already-exists.error'
import Book from '@/core/book/domain/model/book.entity'
import Books from '@/core/book/domain/services/books.repository'
import BookId from '@/core/common/domain/value-objects/book-id'

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

async with(command: CreateBookCommand): Promise<Result<true, BookError>> {
const bookId = BookId.create(command.id)

if (await this.bookRepository.findById(bookId)) {
return err(BookError.becauseAlreadyExists(bookId))
}
import { CreateBookCommand } from './types'

const book = Book.create(
command.id,
command.authors,
command.title,
command.image,
)

await this.bookRepository.save(book)
export default class CreateBookUseCase {
constructor(private readonly books: Books) {}

return ok(true)
async with(command: CreateBookCommand) {
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)),
)
}
}
34 changes: 34 additions & 0 deletions src/core/book/application/find-books.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 BooksInMemory from '@/core/book/infrastructure/services/books-in-memory.repository'
import unexpected from '@/lib/utils/unexpected'

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()
books.books.set(book.id.value, book)

const useCase = new FindBooksUseCase(books)

// Act
const result = await useCase.with()

// Assert
result.match(
(_books) => {
expect(_books).toEqual([BookDTO.fromModel(book)])
},
(error) => unexpected.error(error),
)
})
})
21 changes: 9 additions & 12 deletions src/core/book/application/find-books.use-case.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Books from '@/core/book/domain/services/books.repository'
import { okAsync, ResultAsync } from 'neverthrow'

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

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

async with(): Promise<BookDTO[]> {
const books = await this.booksRepository.findAll()
constructor(private readonly books: Books) {}

return books.map((book) => ({
authors: book.authors,
id: book.id,
image: book.image,
title: book.title,
}))
with(): ResultAsync<BookDTO[], ApplicationError> {
return this.books.findAll().andThen((books) => {
return okAsync(books.map((book) => BookDTO.fromModel(book)))
})
}
}
46 changes: 23 additions & 23 deletions src/core/book/application/types.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import BookId from '@/core/book/domain/model/id.value-object'
import { DeepReadonly } from 'ts-essentials'

export class CreateBookCommand {
constructor(
public readonly id: string,
public readonly title: string,
public readonly authors: string[],
public readonly image: string,
) {}
}
import Book from '@/core/book/domain/model/book.entity'

export interface BookDTO {
type CreateBookCommand = DeepReadonly<{
authors: string[]
id: string
image: string
title: string
}>

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

export class BookError extends Error {
constructor(
message: string,
public readonly type: string,
) {
super(message)
}
type BookDTO = DeepReadonly<{
authors: string[]
id: string
image: string
title: string
}>

static becauseAlreadyExists(id: BookId) {
return new BookError(
`Book with id ${id.value} already exists`,
'DUPLICATE_NAME',
)
}
const BookDTO = {
fromModel: (book: Book): BookDTO => ({
authors: book.authors.map((author) => author.value),
id: book.id.value,
image: book.image.value,
title: book.title.value,
}),
with: (properties: BookDTO) => properties,
}

export { BookDTO, CreateBookCommand }
8 changes: 8 additions & 0 deletions src/core/book/domain/errors/book-id-already-exists.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ApplicationError from '@/core/common/domain/errors/application-error'
import BookId from '@/core/common/domain/value-objects/book-id'

export default class BookIdAlreadyExistsError extends ApplicationError {
static withId(id: BookId): BookIdAlreadyExistsError {
return new BookIdAlreadyExistsError(`book with ${id.value} already exists.`)
}
}
8 changes: 8 additions & 0 deletions src/core/book/domain/errors/book-not-found.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ApplicationError from '@/core/common/domain/errors/application-error'
import BookId from '@/core/common/domain/value-objects/book-id'

export default class BookNotFoundError extends ApplicationError {
static withId(id: BookId): BookNotFoundError {
return new BookNotFoundError(`book with ${id.value} not found`)
}
}
14 changes: 0 additions & 14 deletions src/core/book/domain/model/author.value-object.ts

This file was deleted.

Loading

0 comments on commit baa5c2c

Please sign in to comment.