Skip to content

Commit

Permalink
fix(types): 🐛 don't assume SinglePartBook won't have a series; comb…
Browse files Browse the repository at this point in the history
…ine redundant ApiBook types

After some testing, it seems Audible is inconsistent with how books in a series are labeled. To combat this, we will now allow single and multi part books to contain a series/publication. We can also remove BookSchema, because chapterInfo doesn't seem to ever be attached to a book, so not sure why this was created. Also, since series are optional, we can put those into the main book type

fixes #589
  • Loading branch information
djdembeck committed Apr 15, 2023
1 parent ad2263e commit 90a2a06
Show file tree
Hide file tree
Showing 14 changed files with 57 additions and 77 deletions.
27 changes: 6 additions & 21 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export const ApiNarratorOnBookSchema = PersonSchema
export type ApiNarratorOnBook = z.infer<typeof ApiNarratorOnBookSchema>

// Books
const ApiCoreBookSchema = z.object({
// What we expect to keep from Audible's API
export const ApiBookSchema = z.object({
asin: AsinSchema,
authors: z.array(ApiAuthorOnBookSchema),
description: z.string(),
Expand All @@ -90,24 +91,14 @@ const ApiCoreBookSchema = z.object({
region: RegionSchema,
releaseDate: z.date(),
runtimeLengthMin: z.number().or(z.literal(0)),
seriesPrimary: ApiSeriesSchema.optional(),
seriesSecondary: ApiSeriesSchema.optional(),
subtitle: z.string().optional(),
summary: z.string(),
title: TitleSchema
})

// What we expect to keep from Audible's API
export const ApiBookSchema = ApiCoreBookSchema.extend({
seriesPrimary: ApiSeriesSchema.optional(),
seriesSecondary: ApiSeriesSchema.optional()
})
export type ApiBook = z.infer<typeof ApiBookSchema>

// Final format of data stored
export const BookSchema = ApiBookSchema.extend({
chapterInfo: ApiChapterSchema.optional()
})
export type Book = z.infer<typeof BookSchema>

// What we expect to keep from Audible's HTML pages
export const HtmlBookSchema = z.object({
genres: z.array(ApiGenreSchema).min(1)
Expand Down Expand Up @@ -203,20 +194,14 @@ const podcastShape = z.object({

// This is the shape of the data we get from Audible's API for series content
const seriesShape = z.object({
content_delivery_type: z.literal('MultiPartBook'),
content_delivery_type: z.enum(['MultiPartBook', 'SinglePartBook']),
publication_name: z.string().optional(),
series: z.array(AudibleSeriesSchema).optional()
})

// Make a discriminated union of the base shape and the two types of content we get from Audible's API based on the content_delivery_type field
const resultShape = z
.discriminatedUnion('content_delivery_type', [
podcastShape,
seriesShape,
z.object({
content_delivery_type: z.literal('SinglePartBook')
})
])
.discriminatedUnion('content_delivery_type', [podcastShape, seriesShape])
.and(baseShape)

export const AudibleProductSchema = z.object({
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/authors/audible/SeedHelper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Book } from '#config/types'
import { ApiBook } from '#config/types'
import fetch from '#helpers/utils/fetchPlus'
import getErrorMessage from '#helpers/utils/getErrorMessage'

class SeedHelper {
book: Book
book: ApiBook

constructor(book: Book) {
constructor(book: ApiBook) {
this.book = book
}

Expand Down
6 changes: 3 additions & 3 deletions src/helpers/books/audible/ApiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class ApiHelper {
allSeries.forEach((series: AudibleSeries) => {
if (!this.audibleResponse) throw new Error(ErrorMessageNoData(this.asin, 'ApiHelper'))
// Only return series for MultiPartBook, makes linter happy
if (this.audibleResponse.content_delivery_type !== 'MultiPartBook') return undefined
if (this.audibleResponse.content_delivery_type === 'PodcastParent') return undefined
const seriesJson = this.getSeries(series)
// Check and set primary series
if (
Expand All @@ -222,7 +222,7 @@ class ApiHelper {
allSeries.forEach((series: AudibleSeries) => {
if (!this.audibleResponse) throw new Error(ErrorMessageNoData(this.asin, 'ApiHelper'))
// Only return series for MultiPartBook, makes linter happy
if (this.audibleResponse.content_delivery_type !== 'MultiPartBook') return undefined
if (this.audibleResponse.content_delivery_type === 'PodcastParent') return undefined
const seriesJson = this.getSeries(series)
// Check and set secondary series
if (
Expand Down Expand Up @@ -252,7 +252,7 @@ class ApiHelper {
let series2: ApiSeries | undefined
// Only return series for MultiPartBook, makes linter happy
if (
this.audibleResponse.content_delivery_type === 'MultiPartBook' &&
this.audibleResponse.content_delivery_type != 'PodcastParent' &&
this.audibleResponse.series
) {
series1 = this.getSeriesPrimary(this.audibleResponse.series)
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/books/audible/StitchHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CheerioAPI } from 'cheerio'

import { ApiBook, AudibleProduct, Book, BookSchema, HtmlBook } from '#config/types'
import { ApiBook, ApiBookSchema, AudibleProduct, HtmlBook } from '#config/types'
import ApiHelper from '#helpers/books/audible/ApiHelper'
import ScrapeHelper from '#helpers/books/audible/ScrapeHelper'
import getErrorMessage from '#helpers/utils/getErrorMessage'
Expand Down Expand Up @@ -78,26 +78,26 @@ class StitchHelper {
/**
* Sets genres key in returned json if it exists
*/
async includeGenres(): Promise<Book> {
async includeGenres(): Promise<ApiBook> {
if (this.scraperParsed?.genres?.length) {
const sortedObject = this.sharedHelper.sortObjectByKeys({
...this.apiParsed,
...this.scraperParsed
})
const parsed = BookSchema.safeParse(sortedObject)
const parsed = ApiBookSchema.safeParse(sortedObject)
if (parsed.success) return parsed.data
throw new Error(ErrorMessageSort(this.asin))
}
return this.apiParsed as Book
return this.apiParsed as ApiBook
}

/**
* Call fetch and parse functions only as necessary
* (prefer API over scraper).
* Returns the result of includeGenres()
* @returns {Promise<Book>}
* @returns {Promise<ApiBook>}
*/
async process(): Promise<Book> {
async process(): Promise<ApiBook> {
// First, we want to see if we can get all the data from the API
await this.fetchApiBook()
await this.parseApiResponse()
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/database/papr/audible/PaprAudibleBookHelper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BookModel, { BookDocument } from '#config/models/Book'
import { ApiBookSchema, ApiQueryString, Book } from '#config/types'
import { ApiBook, ApiBookSchema, ApiQueryString } from '#config/types'
import { isBookDocument } from '#config/typing/checkers'
import { PaprBookDocumentReturn, PaprBookReturn, PaprDeleteReturn } from '#config/typing/papr'
import getErrorMessage from '#helpers/utils/getErrorMessage'
Expand All @@ -14,7 +14,7 @@ import {

export default class PaprAudibleBookHelper {
asin: string
bookData!: Book
bookData!: ApiBook
options: ApiQueryString
sharedHelper = new SharedHelper()

Expand Down Expand Up @@ -107,7 +107,7 @@ export default class PaprAudibleBookHelper {
/**
* Set bookData in the class object
*/
setBookData(bookData: Book) {
setBookData(bookData: ApiBook) {
this.bookData = bookData
}

Expand Down
4 changes: 2 additions & 2 deletions src/helpers/database/redis/RedisHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { FastifyRedis } from '@fastify/redis'

import { Book } from '#config/types'
import { ApiBook } from '#config/types'
import getErrorMessage from '#helpers/utils/getErrorMessage'
import { ErrorMessageRedisDelete, ErrorMessageRedisSet } from '#static/messages'

Expand All @@ -12,7 +12,7 @@ export default class RedisHelper {
this.key = `${region}-${key}-${id}`
}

convertStringToDate(parsed: Book) {
convertStringToDate(parsed: ApiBook) {
parsed.releaseDate = new Date(parsed.releaseDate)
return parsed
}
Expand Down
18 changes: 10 additions & 8 deletions src/helpers/routes/BookShowHelper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FastifyRedis } from '@fastify/redis'

import { BookDocument } from '#config/models/Book'
import { ApiQueryString, Book, BookSchema } from '#config/types'
import { ApiBook, ApiBookSchema, ApiQueryString } from '#config/types'
import SeedHelper from '#helpers/authors/audible/SeedHelper'
import StitchHelper from '#helpers/books/audible/StitchHelper'
import PaprAudibleBookHelper from '#helpers/database/papr/audible/PaprAudibleBookHelper'
Expand All @@ -11,7 +11,7 @@ import { ErrorMessageDataType } from '#static/messages'

export default class BookShowHelper {
asin: string
bookInternal: Book | undefined = undefined
bookInternal: ApiBook | undefined = undefined
sharedHelper: SharedHelper
paprHelper: PaprAudibleBookHelper
redisHelper: RedisHelper
Expand Down Expand Up @@ -39,16 +39,18 @@ export default class BookShowHelper {
* making sure the data is the correct type.
* Then, sort the data and return it.
*/
async getBookWithProjection(): Promise<Book> {
async getBookWithProjection(): Promise<ApiBook> {
// 1. Get the book with projections
const bookToReturn = await this.paprHelper.findOneWithProjection()
// Make saure we get a book type back
if (bookToReturn.data === null) throw new Error(ErrorMessageDataType(this.asin, 'Book'))

// 2. Sort the object
const sort = this.sharedHelper.sortObjectByKeys(bookToReturn.data)
console.log(sort)
// Parse the data to make sure it's the correct type
const parsed = BookSchema.safeParse(sort)
const parsed = ApiBookSchema.safeParse(sort)
console.log(parsed)
// If the data is not the correct type, throw an error
if (!parsed.success) throw new Error(ErrorMessageDataType(this.asin, 'Book'))
// Return the data
Expand All @@ -66,7 +68,7 @@ export default class BookShowHelper {
* Get new book data and pass it to the create or update papr function.
* Then, set redis cache and return the book.
*/
async createOrUpdateBook(): Promise<Book> {
async createOrUpdateBook(): Promise<ApiBook> {
// Place the new book data into the papr helper
this.paprHelper.setBookData(await this.getNewBookData())

Expand Down Expand Up @@ -97,7 +99,7 @@ export default class BookShowHelper {
/**
* Actions to run when an update is requested
*/
async updateActions(): Promise<Book> {
async updateActions(): Promise<ApiBook> {
// 1. Check if it is updated recently
if (this.isUpdatedRecently()) return this.getBookWithProjection()

Expand All @@ -117,7 +119,7 @@ export default class BookShowHelper {
/**
* Main handler for the book show route
*/
async handler(): Promise<Book> {
async handler(): Promise<ApiBook> {
this.originalBook = await this.getBookFromPapr()

// If the book is already present
Expand All @@ -134,7 +136,7 @@ export default class BookShowHelper {
const redisBook = await this.redisHelper.findOrCreate(data)
if (redisBook) {
// Parse the data to make sure it's the correct type
const parsedData = BookSchema.safeParse(redisBook)
const parsedData = ApiBookSchema.safeParse(redisBook)
if (parsedData.success) return parsedData.data
}

Expand Down
4 changes: 2 additions & 2 deletions tests/audible/books/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ApiBook, AudibleProduct, Book } from '#config/types'
import type { ApiBook, AudibleProduct } from '#config/types'
import ApiHelper from '#helpers/books/audible/ApiHelper'
import type { MinimalResponse } from '#tests/datasets/audible/books/api'
import {
Expand All @@ -13,7 +13,7 @@ import {
let asin: string
let helper: ApiHelper
let minimalResponse: MinimalResponse
let minimalParsed: Book
let minimalParsed: ApiBook

describe('Audible API', () => {
describe('When fetching Project Hail Mary', () => {
Expand Down
4 changes: 2 additions & 2 deletions tests/audible/books/stitch.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiChapter, Book } from '#config/types'
import { ApiBook, ApiChapter } from '#config/types'
import ChapterHelper from '#helpers/books/audible/ChapterHelper'
import StitchHelper from '#helpers/books/audible/StitchHelper'
import { minimalB0036I54I6 } from '#tests/datasets/audible/books/api'
Expand All @@ -12,7 +12,7 @@ import { combinedB08C6YJ1LS, combinedB017V4IM1G } from '#tests/datasets/audible/
let asin: string
let helper: StitchHelper
let chapterHelper: ChapterHelper
let response: Book
let response: ApiBook
let chapters: ApiChapter | undefined

describe('Audible API and HTML Parsing', () => {
Expand Down
14 changes: 7 additions & 7 deletions tests/datasets/audible/books/api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
ApiBook,
ApiBookSchema,
ApiGenre,
ApiSeries,
AudibleProduct,
AudibleProductSchema,
Book,
BookSchema
AudibleProductSchema
} from '#config/types'

export interface MinimalResponse {
Expand All @@ -28,11 +28,11 @@ export function setupMinimalParsed(
description: string,
image: string,
genres: ApiGenre[]
): Book {
): ApiBook {
let seriesPrimary: ApiSeries | undefined
let seriesSecondary: ApiSeries | undefined
// Only return series for MultiPartBook, makes linter happy
if (response.content_delivery_type === 'MultiPartBook') {
if (response.content_delivery_type !== 'PodcastParent') {
if (response.series?.[0]) {
seriesPrimary = {
asin: response.series[0].asin,
Expand All @@ -48,7 +48,7 @@ export function setupMinimalParsed(
}
}
}
return BookSchema.parse({
return ApiBookSchema.parse({
asin: response.asin,
authors: response.authors,
description,
Expand Down Expand Up @@ -833,7 +833,7 @@ export const podcast = AudibleProductSchema.parse({
]
})

export const minimalB0036I54I6: Book = {
export const minimalB0036I54I6: ApiBook = {
asin: 'B0036I54I6',
authors: [
{ name: 'Diane Wood Middlebrook (Professor of English' },
Expand Down
6 changes: 3 additions & 3 deletions tests/datasets/audible/books/stitch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiGenre, Book } from '#config/types'
import { ApiBook, ApiGenre } from '#config/types'
import { B08C6YJ1LS, B017V4IM1G, setupMinimalParsed } from '#tests/datasets/audible/books/api'

let description: string
Expand Down Expand Up @@ -36,7 +36,7 @@ genres = [
{ asin: '18580607011', name: 'Fantasy', type: 'tag' }
]
image = 'https://m.media-amazon.com/images/I/91eopoUCjLL.jpg'
export const combinedB017V4IM1G: Book = setupMinimalParsed(
export const combinedB017V4IM1G: ApiBook = setupMinimalParsed(
B017V4IM1G.product,
description,
image,
Expand All @@ -55,7 +55,7 @@ genres = [
{ asin: '18574623011', name: 'Crime Thrillers', type: 'tag' }
]
image = 'https://m.media-amazon.com/images/I/91H9ynKGNwL.jpg'
export const combinedB08C6YJ1LS: Book = setupMinimalParsed(
export const combinedB08C6YJ1LS: ApiBook = setupMinimalParsed(
B08C6YJ1LS.product,
description,
image,
Expand Down
Loading

0 comments on commit 90a2a06

Please sign in to comment.