Пример #1
0
def update_book_genre(genre_id: Union[str, uuid4],
                      book_genre_id: Union[str, uuid4]) -> Optional[dict]:
    """Updates a book_genre by a given id.

    Args:
        genre_id: The FK to the genre table.
        book_genre_id: The PK of a book_genre.

    Returns:
        An updated book_genre with the given id else None.

    Raises:
        InvalidParamException: If any of the given params are None.
        UniqueEntityException: If a book_id/genre_id match already exists in the table.
        ModifyingPrimaryException: If attempting to modify to replace the book's primary genre.
    """
    validate_params(func='update_book_genre',
                    params={
                        'genre_id': genre_id,
                        'book_genre_id': book_genre_id
                    })
    book_genre = data.get_book_genre_by_id(book_genre_id=book_genre_id)
    validate_entity_is_unique(
        func=data.get_book_genres_by_book_id_and_genre_id,
        book_id=book_genre.get('book_id'),
        genre_id=genre_id)
    validate_primary_genre(
        book_id=data.get_book_genre_by_id(book_genre_id).get('book',
                                                             {}).get('id'),
        genre_id=genre_id,
        method='PATCH')

    return data.update_book_genre(genre_id=genre_id,
                                  book_genre_id=book_genre_id)
Пример #2
0
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)
Пример #3
0
def create_book_genre(
        book_id: Union[str, uuid4],
        genre_id: Union[str, uuid4],
        book_genre_id: Optional[Union[str, uuid4]] = None) -> Optional[dict]:
    """Creates a new book_genre.

    Args:
        book_id: The FK to the books table.
        genre_id: The FK to the genres table.
        book_genre_id: The PK to assign to the new book_genre.

    Returns:
        A newly created book_genre else None.

    Raises:
        InvalidParamException: If any of the given params are None.
        UniqueEntityException: If a book_id/genre_id match already exists in the table.
        MultiplePrimaryException: If the given book id already has a primary genre.
    """
    validate_params(func='create_book_genre',
                    params={
                        'book_id': book_id,
                        'genre_id': genre_id
                    })
    validate_entity_is_unique(
        func=data.get_book_genres_by_book_id_and_genre_id,
        book_id=book_id,
        genre_id=genre_id)
    validate_primary_genre(book_id=book_id, genre_id=genre_id, method='POST')

    return data.create_book_genre(book_id=book_id,
                                  genre_id=genre_id,
                                  book_genre_id=book_genre_id)
Пример #4
0
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)
Пример #5
0
def update_genre(user_id: Union[str, uuid4], name: str, display_name: str,
                 genre_id: Union[str, uuid4]) -> Optional[dict]:
    """Updates a genre by a given id.

    Args:
        user_id: The FK to the users table.
        name: The name to modify in the genre with the given id.
        display_name: The display_name to modify in the genre with the given id.
        genre_id: The PK of a genre.

    Returns:
        An updated genre with the given id else None.

    Raises:
        InvalidParamException: If any of the given params are None.
        UniqueEntityException: If a matching genre is found by the display name and/or user_id.
    """
    validate_params(func='update_genre',
                    params={
                        'name': name,
                        'display_name': display_name,
                        'genre_id': genre_id
                    })
    validate_entity_is_unique(func=data.get_genre_by_display_name_and_user_id,
                              display_name=display_name,
                              user_id=user_id)

    return data.update_genre(name=name,
                             display_name=display_name,
                             genre_id=genre_id)
Пример #6
0
def get_book_genres_by_genre_id(genre_id: Optional[Union[str, uuid4]]) -> list:
    """Gets book_genres from the table filtered by given params.

    Args:
        genre_id: The FK to the genres table.

    Returns:
        A list of book_genres filtered by any given params.
    """
    validate_params(func='get_book_genres_by_genre_id',
                    params={'genre_id': genre_id})
    return data.get_book_genres_by_genre_id(genre_id=genre_id)
Пример #7
0
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'))
Пример #8
0
def get_book_by_id(book_id: Union[str, uuid4]) -> Optional[dict]:
    """Gets a book from the table by a given id.

    Args:
        book_id: The PK of a book.

    Returns:
        A book from the table by a given id else None.

    Raises:
        InvalidParamException: If the given book_id is None.
    """
    validate_params(func='get_book_by_id', params={'book_id': book_id})
    return data.get_book_by_id(book_id=book_id)
Пример #9
0
def delete_book_genres(book_id: Union[str, uuid4]) -> Optional[list]:
    """Deletes book_genres from the table using the given params.

    Args:
        book_id: The FK to the book table.

    Returns:
        A list of book_genres deleted using the given params.

    Raises:
        InvalidParamException: If any of the given params are None.
    """
    validate_params(func='delete_book_genres', params={'book_id': book_id})
    return data.delete_book_genres(book_id=book_id)
Пример #10
0
def get_session_by_user(user_id: Union[str, uuid4]) -> Optional[dict]:
    """Gets a session using the given user_id.

    Args:
        user_id: The primary key associated with a user entity in the dim_users table.

    Returns:
        A session with the given user_id else None.

    Raises:
        InvalidParamException: If the user_id is None.
    """
    validate_params(func='get_session_by_user', params={'user_id': user_id})
    return data.get_session_by_user(user_id=user_id)
Пример #11
0
def delete_genres(user_id: Union[str, uuid4]) -> Optional[list]:
    """Deletes genres from the table using the given params.

    Args:
        user_id: The ID of the user to delete genres by.

    Returns:
        A list of genres deleted using the given params.

    Raises:
        InvalidParamException: If any of the given params are None.
    """
    validate_params(func='delete_genres', params={'user_id': user_id})
    return data.delete_genres_by_user_id(user_id=user_id)
Пример #12
0
def get_genre_by_id(genre_id: Union[str, uuid4]) -> Optional[dict]:
    """Gets a genre from the table by a given id.

    Args:
        genre_id: The PK of a genre.

    Returns:
        A genre from the table by a given id else None.

    Raises:
        InvalidParamException: If the given genre_id is None.
    """
    validate_params(func='get_genre_by_id', params={'genre_id': genre_id})
    return data.get_genre_by_id(genre_id=genre_id)
Пример #13
0
def get_session_by_token(token: str) -> Optional[dict]:
    """Gets a session using the given token.

    Args:
        token: An encoded JSON web token associated with a session in the fct_sessions table.

    Returns:
        A session with the given token else None.

    Raises:
        InvalidParamException: If the token is None.
    """
    validate_params(func='get_session_by_token', params={'token': token})
    return data.get_session_by_token(token=token)
Пример #14
0
def create_session(user_id: Union[str, uuid4], token: str) -> Optional[dict]:
    """Creates a new session in the fct_sessions table.

    Args:
        user_id: The primary key associated with a user entity in the dim_users table.
        token: An encoded JSON web token associated with the given user entity.

    Returns:
        A newly created session else None.

    Raises:
        InvalidParamException: If the user_id or token is None.
    """
    validate_params(func='create_session', params={'user_id': user_id, 'token': token})
    return data.create_session(user_id=user_id, token=token)
Пример #15
0
def delete_book_genre_by_id(
        book_genre_id: Union[str, uuid4]) -> Optional[dict]:
    """Deletes a book_genre from the table by the given id.

    Args:
        book_genre_id: The PK of a book_genre.

    Returns:
        A deleted book_genre with the given id else None.

    Raises:
        InvalidParamException: If the given book_genre_id is None.
    """
    validate_params(func='delete_book_genre_by_id',
                    params={'book_genre_id': book_genre_id})
    return data.delete_book_genre_by_id(book_genre_id=book_genre_id)
Пример #16
0
def authorize_user(
    email: str,
    password: str,
    name: Optional[str] = None,
) -> Optional[dict]:
    """Authorizes a user with the users service using the given params.

    Args:
        email: The unique email associated with a user entity in the dim_users table.
        password: The plain string password associated with a user entity in the dim_users table.
        name: The full name of a new user.

    Returns:
        A dict containing an access token and auth results else None.

    Raises:
        InvalidParamException: If email or password is None.
    """
    validate_params(func='authorize_user', params={'email': email, 'password': password})

    user = (
        create_user(name=name, email=email, password=password)
        if name else get_user_by_email(email=email)
    )
    if not user:
        log.debug(
            f'Failed to GET user by email "{email}" from Users Service.'
            if not name else f'Failed to POST new user with name "{name}" and email "{email}".'
        )
        return None

    is_user_validated = (
        validate.validate_password(password=password, hashed=user.get('password'))
        if not name else True
    )
    if not is_user_validated:
        log.debug(
            f'Failed to validate given plain text password "{password}" with the entity '
            f'associated with the given email "{email}".'
        )
        return None

    access_token, auth_results = encode.encode_json_web_token(user=user)
    log.info(user)
    create_session(user_id=user.get('id'), token=access_token)

    return {'access_token': access_token, 'auth_results': auth_results}
Пример #17
0
def create_book(
    user_id: Union[str, uuid4],
    author: str,
    title: str,
    image_key: Optional[str] = None,
    synopsis: Optional[str] = None,
    book_id: Optional[Union[str, uuid4]] = None
) -> Optional[dict]:
    """Creates a new book.

    Args:
        user_id: The FK to the users table.
        author: The author to associate with the new book.
        image_key: The image_key to associate with the new book.
        synopsis: The synopsis to associate with the new book.
        title: The title to associate with the new book.
        book_id: The PK to assign to the new book.

    Returns:
        A newly created book else None.

    Raises:
        InvalidParamException: If any of the given params are None.
        UniqueEntityException: If a title/user_id match already exists in the table.
    """
    validate_params(
        func='create_book',
        params={'user_id': user_id, 'author': author, 'title': title}
    )
    validate_entity_is_unique(
        func=data.get_book_by_title_and_user_id,
        title=title,
        user_id=user_id
    )

    return data.create_book(
        book_status_id=INITIAL_BOOK_STATUS_ID,
        user_id=user_id,
        author=author,
        image_key=image_key,
        synopsis=synopsis,
        timestamp=datetime.utcnow().replace(tzinfo=pytz.utc),
        title=title,
        book_id=book_id
    )
Пример #18
0
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)
Пример #19
0
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
Пример #20
0
def get_user_books(user_id: Union[str, uuid4]) -> list:
    """Gets all of the books associated with the user ID authorized using the given JWT.

    Args:
        user_id: The unique user ID associated with the JWT authorized in the request.

    Returns:
        A list of all books associated with the given user ID else an empty list.

    Raises:
        InvalidParamException: If the user ID is None.
    """
    log.info(f'Retrieving aggregated book data for user with id "{user_id}".')
    validate_params(func='get_user_books', params={'user_id': user_id})

    return [
        populate_genres_for_user_book(user_book=user_book)
        for user_book in books_service.get_books(user_id=user_id)
    ]
Пример #21
0
def update_book(
    book_id: Union[str, uuid4],
    user_id: Union[str, uuid4],
    title: Optional[str] = None,
    author: Optional[str] = None,
    synopsis: Optional[str] = None,
    image_key: Optional[str] = None,
    book_status_id: Optional[Union[str, uuid4]] = None
) -> Optional[dict]:
    """Updates a book by a given id.

    Args:
        book_id: The PK of a book.
        title: The title to modify in the book with the given id.
        author: The author to modify in the book with the given id.
        synopsis: The synopsis to modify in the book with the given id.
        image_key: The image_key to modify in the book with the given id.
        book_status_id: The FK to the book_status table.

    Returns:
        An updated book with the given id else None.

    Raises:
        InvalidParamException: If any of the given params are None.
        UniqueEntityException: If a title/user_id match already exists in the table.
    """
    validate_params(func='update_book', params={'book_id': book_id})
    if title:
        validate_entity_is_unique(
            func=data.get_book_by_title_and_user_id,
            title=title,
            user_id=user_id
        )

    return data.update_book(
        book_id=book_id,
        title=title,
        author=author,
        synopsis=synopsis,
        image_key=image_key,
        book_status_id=book_status_id
    )
Пример #22
0
def delete_user_books(user_id: Union[str, uuid4]) -> list:
    """Deletes all of the books associated with the user_id pulled from the authorized JWT.

    Args:
        user_id: The unique ID of the user pulled off of the authorized JWT.

    Returns:
        A list of all of the books that were deleted.

    Raises:
        InvalidParamException: If the given user ID is None.
    """
    log.info(f'Deleting all books for user with id "{user_id}".')
    validate_params(func='delete_user_books', params={'user_id': user_id})
    user_books = books_service.get_books(user_id)
    for user_book in user_books:
        book_genres = book_genres_service.get_book_genres_by_book_id(book_id=user_book.get('id'))
        for book_genre in book_genres:
            book_genres_service.delete_book_genre_by_id(book_genre_id=book_genre.get('id'))
    genres_service.delete_genres(user_id=user_id)
    return books_service.delete_books(user_id=user_id)
Пример #23
0
def authenticate_user(
    email: str,
    password: str,
    json_web_token: str
) -> Optional[dict]:
    """Authenticates a user's json web token with the given parameters.

    Args:
        email: The unique email associated with a user entity in the dim_users table.
        password: The plain string password associated with a user entity in the dim_users table.
        json_web_token: The token pulled from the request.

    Returns:
        A dict containing the decoded json web token.

    Raises:
        InvalidParamException: If any of the given params is None.
    """
    validate_params(
        func='authenticate_user',
        params={'email': email, 'password': password, 'json_web_token': json_web_token}
    )

    user = get_user_by_email(email=email)
    if not user:
        log.debug(f'Failed to GET user by email "{email}" from Users Service.')
        return None
    if not password == user.get('password'):
        log.debug(
            f'Failed to validate given plain text password "{password}" with the entity '
            f'associated with the given email "{email}".'
        )
        return None

    return decode.decode_json_web_token(
        json_web_token=json_web_token,
        secret=user.get('password'),
    )
Пример #24
0
def refresh_authorization(email: str) -> Optional[dict]:
    """Refreshes the authenication token for a user session with the given email.

    Args:
        email: The unique email associated with a session entity in the fct_sessions table.

    Returns:
        A dict containing an access token and auth results else None.

    Raises:
        InvalidParamException: If the given email is None.
    """
    validate_params(func='refresh_authorization', params={'email': email})

    user = get_user_by_email(email=email)
    if not user:
        log.debug(f'Failed to GET user by email "{email}" from Users Service.')
        return None

    access_token, auth_results = encode.encode_json_web_token(user=user)
    create_session(user_id=user.get('id'), token=access_token)

    return {'access_token': access_token, 'auth_results': auth_results}
Пример #25
0
def create_genre(
        user_id: Union[str, uuid4],
        display_name: str,
        name: str,
        genre_id: Optional[Union[str, uuid4]] = None) -> Optional[dict]:
    """Creates a new genre.

    Args:
        user_id: The FK to the users table.
        display_name: The display_name to associate with the new genre.
        name: The name to associate with the new genre.
        genre_id: The PK to assign to the new genre.

    Returns:
        A newly created genre else None.

    Raises:
        InvalidParamException: If any of the given params are None.
        UniqueEntityException: If a matching genre is found by the display name and/or user_id.
    """
    validate_params(func='create_genre',
                    params={
                        'user_id': user_id,
                        'display_name': display_name,
                        'name': name
                    })
    validate_entity_is_unique(func=data.get_genre_by_display_name_and_user_id,
                              display_name=display_name,
                              user_id=user_id)

    return data.create_genre(user_id=user_id,
                             bucket_name=None,
                             display_name=display_name,
                             is_primary=False,
                             name=name,
                             genre_id=genre_id)
Пример #26
0
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'))
Пример #27
0
def create_user_book(
    user_id: Union[str, uuid4],
    author: str,
    title: str,
    primary_genre_id: Union[str, uuid4],
    image_key: Optional[str],
    synopsis: Optional[str],
    secondary_genre_names: Optional[list],
    book_id: Optional[Union[str, uuid4]]
) -> Optional[dict]:
    """Creates a new book associated with the user using the given body parameters.

    Args:
        user_id: The unique ID of the user creating the book.
        author: The name of the person writing the book.
        title: The title of the new book.
        primary_genre_id: The unique ID of the primary genre for the book.
        image_key: The S3 object_key associated with the user's book cover.
        synopsis: A short summary of the book.
        secondary_genre_names: A list of names of secondary genres to associate with the new book.
        book_id: Optional unique ID to associate with the new book.

    Returns:
        A dict containing aggregated information for the newly created book else None.

    Raises:
        InvalidParamException: If any of the required parameters are None.
    """
    log.info(f'Creating new book "{title}" for user with id "{user_id}".')
    validate_params(
        func='create_user_book',
        params={
            'user_id': user_id,
            'author': author,
            'title': title,
            'primary_genre_id': primary_genre_id
        }
    )

    new_book = books_service.create_book(
        user_id=user_id,
        author=author,
        title=title,
        image_key=image_key,
        synopsis=synopsis,
        book_id=book_id
    )

    primary_book_genre = book_genres_service.create_book_genre(
        book_id=new_book.get('id'),
        genre_id=primary_genre_id
    )

    secondary_book_genres = []
    for secondary_genre_name in secondary_genre_names:
        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
            )

        log.info(f'Tying genre "{secondary_genre_name}" to new book "{new_book.get("title")}"')
        secondary_book_genres.append(
            book_genres_service.create_book_genre(
                book_id=new_book.get('id'),
                genre_id=existing_genre.get('id') if existing_genre else new_genre.get('id')
            )
        )

    new_book.update({
        'primary_genre': (
            book_genres_service.get_book_genre_by_id(primary_book_genre.get('id'))
        ),
        'secondary_genres': [
            book_genres_service.get_book_genre_by_id(secondary_book_genre.get('id'))
            for secondary_book_genre in secondary_book_genres
        ]
    })

    return new_book