def delete_user_book_by_id(
    user_id: Union[str, uuid4],
    book_id: Union[str, uuid4]
) -> Optional[dict]:
    """Deletes the book by the given ID.

    Args:
        user_id: The unique ID of the user pulled off of the authorized JWT.
        book_id: The unique ID of the book to be deleted.

    Returns:
        The deleted book else None.

    Raises:
        InvalidParamException: If any of the required parameters are None.
        UnauthorizedAccessException: If the user ID associated with the book does not match the user
            ID pulled from the request's JWT.
    """
    log.info(f'Deleting book with id "{book_id}".')
    validate_params(
        func='delete_user_book_by_id',
        params={'user_id': user_id, 'book_id': book_id}
    )
    security.validate_user_book(user_id=user_id, book_id=book_id)

    book_genres = book_genres_service.get_book_genres_by_book_id(book_id=book_id)
    genre_ids = [book_genre.get('genre', {}).get('id') for book_genre in book_genres]
    for book_genre in book_genres:
        book_genres_service.delete_book_genre_by_id(book_genre_id=book_genre.get('id'))

    for genre_id in genre_ids:
        if not book_genres_service.get_book_genres_by_genre_id(genre_id=genre_id):
            genres_service.delete_genre_by_id(genre_id=genre_id)

    return books_service.delete_book_by_id(book_id=book_id)
def create_secondary_book_genre(
    user_id: Union[str, uuid4],
    book_id: Union[str, uuid4],
    secondary_genre_name: str
) -> Optional[dict]:
    """Creates a new secondary genre tied to the given book ID.

    Args:
        user_id: The unique ID of the user pulled off of the authorized JWT.
        book_id: The unique ID of the book to tie the new secondary genre to.
        secondary_genre_name: The unique name of the genre to tie to the book.

    Returns:
        The updated secondary book genre else None.

    Raises:
        InvalidParamException: If any of the required parameters are None.
        UnauthorizedAccessException: If the user ID associated with the book does not match the user
            ID pulled from the request's JWT.
    """
    log.info(
        f'Tying new secondary genre "{secondary_genre_name}" to book with id "{book_id}"'
    )
    validate_params(
        func='create_secondary_book_genre',
        params={
            'user_id': user_id,
            'book_id': book_id,
            'secondary_genre_name': secondary_genre_name
        }
    )
    security.validate_user_book(user_id=user_id, book_id=book_id)

    existing_genre = genres_service.get_genre_by_display_name_and_user_id(
        display_name=secondary_genre_name,
        user_id=user_id,
    )
    if not existing_genre:
        log.info(f'Creating new genre "{secondary_genre_name}" tied to user id: "{user_id}"')
        new_genre = genres_service.create_genre(
            user_id=user_id,
            display_name=secondary_genre_name,
            name=secondary_genre_name
        )

    new_book_genre = book_genres_service.create_book_genre(
        book_id=book_id,
        genre_id=existing_genre.get('id') if existing_genre else new_genre.get('id')
    )
    return book_genres_service.get_book_genre_by_id(new_book_genre.get('id'))
def get_user_book_by_id(
    user_id: Union[str, uuid4],
    book_id: Union[str, uuid4]
) -> Optional[dict]:
    """Gets a user book by a given ID.

    Args:
        user_id: The unique user ID associated with the JWT authorized in the request.
        book_id: The unique ID associated with the book being retrieved.

    Returns:
        A book populated with aggregated information else None.

    Raises:
        InvalidParamException: If the given user ID or book ID are None.
        UnauthorizedAccessException: If the user ID associated with the book does not match the user
            ID pulled from the request's JWT.
    """
    log.info(
        f'Attempting to retrieve aggregated book data for book with id "{book_id}" for user with '
        f'id "{user_id}".'
    )
    validate_params(func='get_user_book_by_id', params={'user_id': user_id, 'book_id': book_id})
    book = security.validate_user_book(user_id=user_id, book_id=book_id)
    return populate_genres_for_user_book(user_book=book)
def update_user_book_by_id(
    user_id: Union[str, uuid4],
    book_id: Union[str, uuid4],
    title: Optional[str],
    author: Optional[str],
    synopsis: Optional[str],
    image_key: Optional[str],
    book_status_id: Optional[Union[str, uuid4]],
) -> Optional[dict]:
    """Updates a user's book using the given parameters.

    Args:
        secondary_genre_ids: [description]
        user_id: The unique ID of the user creating the book.
        book_id: The unique ID associate with the book to update.
        title: The updated title of the new book.
        author: The updated name of the person writing the book.
        synopsis: An updated short summary of the book.
        image_key: The S3 object_key associated with the user's book cover.
        book_status_id: The updated status for the book.

    Returns:
        A book populated with the updated aggregated information else None.

    Raises:
        InvalidParamException: If any of the required parameters are None.
        UnauthorizedAccessException: If the user ID associated with the book does not match the user
            ID pulled from the request's JWT.
    """
    log.info(f'Updating book with id "{book_id}".')
    validate_params(func='update_user_book_by_id', params={'user_id': user_id, 'book_id': book_id})
    security.validate_user_book(user_id=user_id, book_id=book_id)

    updated_book = books_service.update_book(
        book_id=book_id,
        user_id=user_id,
        title=title,
        author=author,
        synopsis=synopsis,
        image_key=image_key,
        book_status_id=book_status_id
    )

    return populate_genres_for_user_book(user_book=updated_book)
def delete_secondary_book_genre(
    user_id: Union[str, uuid4],
    book_id: Union[str, uuid4],
    book_genre_id: Union[str, uuid4]
) -> Optional[dict]:
    """Deletes the secondary genre from the given book.

    Args:
        user_id: The unique ID of the user pulled off of the authorized JWT.
        book_id: The unique ID of the book to tie the new secondary genre to.
        book_genre_id: The unique ID of the book genre to delete from the book.

    Returns:
        The deleted book genre else None.

    Raises:
        InvalidParamException: If any of the required parameters are None.
        UnauthorizedAccessException: If the user ID associated with the book does not match the user
            ID pulled from the request's JWT.
    """
    log.info(
        f'Deleting secondary book genre with id "{book_genre_id}" from book with id "{book_id}".'
    )
    validate_params(
        func='delete_secondary_book_genre',
        params={
            'user_id': user_id,
            'book_id': book_id,
            'book_genre_id': book_genre_id
        }
    )
    security.validate_user_book(user_id=user_id, book_id=book_id)

    book_genre = book_genres_service.get_book_genre_by_id(book_genre_id=book_genre_id)
    deleted_book_genre = book_genres_service.delete_book_genre_by_id(
        book_genre_id=book_genre_id
    )

    genre_id = book_genre.get('genre').get('id')
    if not book_genres_service.get_book_genres_by_genre_id(genre_id=genre_id):
        genres_service.delete_genre_by_id(genre_id=genre_id)

    return deleted_book_genre
def update_user_book_genre_by_id(
    user_id: Union[str, uuid4],
    book_id: Union[str, uuid4],
    genre_name: str,
    book_genre_id: Union[str, uuid4]
) -> Optional[dict]:
    """Updates a book genre with the given ID using the params passed in the body of the request.

    Args:
        user_id: The unique ID of the user pulled off of the authorized JWT.
        book_id: The unique ID of the book associated with the genre being updated.
        genre_name: The unique name of the new genre to associate with the given book genre.
        book_genre_id: The unique ID of the book genre to update.

    Returns:
        An updated book genre else None.

    Raises:
        InvalidParamException: If any of the given parameters are None.
        UnauthorizedAccessException: If the user ID associated with the book does not match the user
            ID pulled from the request's JWT.
    """
    log.info(
        f'Updating book genre with id "{book_genre_id}" to be associated with "{genre_name}".'
    )
    validate_params(
        func='update_user_book_genre_by_id',
        params={
            'user_id': user_id,
            'book_id': book_id,
            'genre_name': genre_name,
            'book_genre_id': book_genre_id
        }
    )
    security.validate_user_book(user_id=user_id, book_id=book_id)

    original_book_genre = book_genres_service.get_book_genre_by_id(book_genre_id)
    original_genre = genres_service.get_genre_by_id(
        genre_id=original_book_genre.get('genre', {}).get('id')
    )
    original_genre_id = original_genre.get('id')

    existing_genre = genres_service.get_genre_by_display_name_and_user_id(
        display_name=genre_name,
        user_id=user_id,
    )
    if not existing_genre:
        log.info(f'Creating new genre "{genre_name}" tied to user id: "{user_id}"')
        new_genre = genres_service.create_genre(
            user_id=user_id,
            display_name=genre_name,
            name=genre_name
        )

    updated_book_genre = book_genres_service.update_book_genre(
        genre_id=existing_genre.get('id') if existing_genre else new_genre.get('id'),
        book_genre_id=book_genre_id
    )

    if not book_genres_service.get_book_genres_by_genre_id(genre_id=original_genre_id):
        genres_service.delete_genre_by_id(genre_id=original_genre_id)

    return book_genres_service.get_book_genre_by_id(updated_book_genre.get('id'))