Seeking advice on how to save a http response object to database

Hi Everyone. I’m looking for advice on how to best save a http response object to database. This is the endpoint I’m working with: https://www.googleapis.com/books/v1/volumes/beSP5CCpiGUC

And this is the sdl and service set-up, which is working fine using the cell below. However, I’m struggling to save the response object to database. I’ve read-up on and tried various approaches including mirroring the sdl types with prisma models, flattening the response, etc. but I haven’t got high-conviction on this is how it should be done. If anyone could point me in the right direction, that would be much appreciated. Thank you.

// graphql/book.sdl.ts
export const schema = gql`
  type Books {
    totalItems: Int!
    items: [Book!]
  }

  type Book {
    id: String!
    etag: String!
    volumeInfo: VolumeInfo!
  }

  type VolumeInfo {
    title: String!
    subtitle: String!
    authors: [String!]
  }

  type Query {
    getBookByVolumeId(volumeId: String!): Book! @skipAuth
  }
`
`
//services/book.ts

export const getGoogleBookVolumeByVolumeId: QueryResolvers['getGoogleBookVolumeByVolumeId'] =
  async ({ volumeId }) => {
    const response = await fetch(
      `https://www.googleapis.com/books/v1/volumes/${volumeId}`
    )
    const json = await response.json()

    return {
      id: volumeId,
      etag: json.etag,
      volumeInfo: {
        title: json.volumeInfo.title,
        subtitle: json.volumeInfo.subtitle,
        authors: json.volumeInfo.authors,
      },
    }
  }
//components/BookCell.ts

import type {
  FindGoogleBookQuery,
  FindGoogleBookQueryVariables,
} from 'types/graphql'

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

export const QUERY = gql`
  query FindGoogleBookQuery($volumeId: String!) {
    googleBook: getGoogleBookVolumeByVolumeId(volumeId: $volumeId) {
      id
      etag
      volumeInfo {
        title
        subtitle
        authors
      }
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({
  error,
}: CellFailureProps<FindGoogleBookQueryVariables>) => (
  <div style={{ color: 'red' }}>Error: {error?.message}</div>
)

export const Success = ({
  googleBook,
}: CellSuccessProps<FindGoogleBookQuery, FindGoogleBookQueryVariables>) => {
  return <div>{JSON.stringify(googleBook)}</div>
}

Hi. May I ask why you have a volumeId string but the response data has volumeInfo which is an object?

Do you need a separate volume info model or do you want to flatten and store title, subtitle and authors in the Book model?

Hi @dthyresson Thanks. Apologies, I posted the wrong sdl. I have just edited now. To your second question, I would ideally like to flatten volumeInfo and store in a Book model.

I am assuming from your SDL that your GraphQL API is not backed by any Prisma models – that is you are not trying to persist the book data to a database and just want to piggyback every get book request to the Google API call.

If that’s the case, I don’t see why you have to have two types:

  type Book {
    id: String!
    etag: String!
    volumeInfo: VolumeInfo!
  }

  type VolumeInfo {
    title: String!
    subtitle: String!
    authors: [String!]
  }

and not just a single Book:

  type Book {
    id: String!
    etag: String!
    title: String!
    subtitle: String!
    authors: [String!]
  }

And then you can restructure the response

return {
      id: volumeId,
      etag: json.etag,
      ...json.volumeInfo,
      },
    }
  }

You also should:

  • add try/catch for error handling
  • add tests to mock the Google API call
  • consider moving the google book api calls to a client in lib that does the google api interaction
  • logs the response for debugging

But, I have to say, I don’t understand

  type Books {
    totalItems: Int!
    items: [Book!]
  }

I don’t understand where this data is coming from of why the type is structured the way it is.

If you intend to have a query to get many books then that would be a query not an items on a model.

Edit:

Actually, I just looked up the API docs Getting Started  |  Google Books APIs  |  Google Developers and what I shared above won’t exactly work…

https://www.googleapis.com/books/v1/volumes/s1gVAAAAYAAJ returns

{
  "kind": "books#volume",
  "id": "s1gVAAAAYAAJ",
  "etag": "HGIRF7yutiw",
  "selfLink": "https://www.googleapis.com/books/v1/volumes/s1gVAAAAYAAJ",
  "volumeInfo": {
    "title": "Pride and Prejudice",
    "authors": [
      "Jane Austen"
    ],
    "publisher": "C. Scribner's sons",
    "publishedDate": "1918",
    "description": "Austen’s most celebrated novel tells the story of Elizabeth Bennet, a bright, lively young woman with four sisters, and a mother determined to marry them to wealthy men. At a party near the Bennets’ home in the English countryside, Elizabeth meets the wealthy, proud Fitzwilliam Darcy. Elizabeth initially finds Darcy haughty and intolerable, but circumstances continue to unite the pair. Mr. Darcy finds himself captivated by Elizabeth’s wit and candor, while her reservations about his character slowly vanish. The story is as much a social critique as it is a love story, and the prose crackles with Austen’s wry wit.",
    "readingModes": {
      "text": true,
      "image": true
    },
    "pageCount": 401,
    "printedPageCount": 448,
    "dimensions": {
      "height": "18.00 cm"
    },
    "printType": "BOOK",
    "averageRating": 4,
    "ratingsCount": 372,
    "maturityRating": "NOT_MATURE",
    "allowAnonLogging": false,
    "contentVersion": "1.3.10.0.full.3",
    "panelizationSummary": {
      "containsEpubBubbles": false,
      "containsImageBubbles": false
    },
    "imageLinks": {
      "smallThumbnail": "http://books.google.com/books/content?id=s1gVAAAAYAAJ&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE731dyPxNh3IPAHwEMGDdN8jVmY_qdNMEbj6ZTooVSWDxCeFt9m0F8YZaAYPJo5mIKuNYqpl1_E0X94_K9HjyLAo_3b3kitgecpghoYh8P9oWGP4SFFfyAenO3ABJ876_IN568yn&source=gbs_api",
      "thumbnail": "http://books.google.com/books/content?id=s1gVAAAAYAAJ&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE73YKPyLG4wQ0EA9hisMK-wGcEd1BUmDt_XwjAFKwBcrsaN3UMwejfhs4A_smBmFek2meVz8XQODbgC68KyTMK5UuEQR1fwt5RiK4EBZwdmt0EEE6axrB3sCyVmUq2k8mcV_FuDU&source=gbs_api",
      "small": "http://books.google.com/books/content?id=s1gVAAAAYAAJ&printsec=frontcover&img=1&zoom=2&edge=curl&imgtk=AFLRE70Y_ZgKhxcmKrs_VpSw_CZy6yyaENiQ-JxlCnjbtamoRePm05FTgQEz7xeuI2DxuvPSaTNpEiCZpopEPiFuTXrbBcz9uN_T_s7TmRZY_pJ9CCFFMyAcD1NDK8eBory0dr_LZUco&source=gbs_api",
      "medium": "http://books.google.com/books/content?id=s1gVAAAAYAAJ&printsec=frontcover&img=1&zoom=3&edge=curl&imgtk=AFLRE71qP9tXwSfPO6gjiGEyn6m4BWILRnHfG0NxlRwWF4Z8dowYVxTQMIeBhgfmzd5bugAYwBRQjBaHb2NYDcX_snVb12Ih5U6Weo5nnx23cgkm1beMH5vgLmTJWaMFuOs_f5HN_f1P&source=gbs_api",
      "large": "http://books.google.com/books/content?id=s1gVAAAAYAAJ&printsec=frontcover&img=1&zoom=4&edge=curl&imgtk=AFLRE73g77Afnpd1DyM7iGG_9hoUycz78R0KxCQzWUDAN-kWjSk970sUrZQhqYp2kuuevGzpIp5cZjI5AVhjM2aI0tf3DGWp6pHddxzGgYRSco0f0ERKuESx5qXhk06XFMFxC59a-0h6&source=gbs_api",
      "extraLarge": "http://books.google.com/books/content?id=s1gVAAAAYAAJ&printsec=frontcover&img=1&zoom=6&edge=curl&imgtk=AFLRE70loW7kE8Hw5uYFWod4mkRe2u_mDziihMqC2m1ho8OESBKHfx8qGRARxL-v4OE2Jjscjgv0ADxqkwx3wz9swP2tUv9Ilpoe3rC1CMejp_nW2hxSeT4BTegUMxyNC1RiirQhgUs1&source=gbs_api"
    },
    "language": "en",
    "previewLink": "http://books.google.com/books?id=s1gVAAAAYAAJ&hl=&source=gbs_api",
    "infoLink": "https://play.google.com/store/books/details?id=s1gVAAAAYAAJ&source=gbs_api",
    "canonicalVolumeLink": "https://play.google.com/store/books/details?id=s1gVAAAAYAAJ"
  },
  "layerInfo": {
    "layers": [
      {
        "layerId": "geo",
        "volumeAnnotationsVersion": "16"
      }
    ]
  },
  "saleInfo": {
    "country": "US",
    "saleability": "FREE",
    "isEbook": true,
    "buyLink": "https://play.google.com/store/books/details?id=s1gVAAAAYAAJ&rdid=book-s1gVAAAAYAAJ&rdot=1&source=gbs_api"
  },
  "accessInfo": {
    "country": "US",
    "viewability": "ALL_PAGES",
    "embeddable": true,
    "publicDomain": true,
    "textToSpeechPermission": "ALLOWED",
    "epub": {
      "isAvailable": true,
      "downloadLink": "http://books.google.com/books/download/Pride_and_Prejudice.epub?id=s1gVAAAAYAAJ&hl=&output=epub&source=gbs_api"
    },
    "pdf": {
      "isAvailable": true,
      "downloadLink": "http://books.google.com/books/download/Pride_and_Prejudice.pdf?id=s1gVAAAAYAAJ&hl=&output=pdf&sig=ACfU3U3dQw5JDWdbVgk2VRHyDjVMT4oIaA&source=gbs_api"
    },
    "webReaderLink": "http://play.google.com/books/reader?id=s1gVAAAAYAAJ&hl=&printsec=frontcover&source=gbs_api",
    "accessViewStatus": "FULL_PUBLIC_DOMAIN",
    "quoteSharingAllowed": false
  }
}

So, you cannot destruct volumeInfo … instead…

{
  id: volumeId,
  etag: json.etag,
  title: json.volumeInfo.title,
  subtitle: json.volumeInfo.subtitle,
  authors: json.volumeInfo.authors,
}
1 Like

@dthyresson Many thanks for this. I should have removed type Books as that relates to a different query I was experimenting with, i.e. https://www.googleapis.com/books/v1/volumes?q=isbn:0716604892 which returns multiple volumes as items. Hence, the Books type.

So that is exactly what I would like to do, i.e. create a Primsa model and save/persist the book data to database. That is where I was getting hung-up. In services/book.tx, I was attempting to create a new book record after const json = await response.json()… I will try again given your feedback and let you know how it goes. Thank you.