Skip to content

Latest commit

 

History

History
183 lines (162 loc) · 5.42 KB

stepzen.md

File metadata and controls

183 lines (162 loc) · 5.42 KB

Designing a Spotify GraphQL schema with StepZen: Depaginating results

Spotify returns an insane amount of basic track data, so whittling it down to a compact GraphQL type using StepZen is a life-saver. The most important data point from a data portability perspective is the ISRC: an International Standard Recording Code, which we can use to write playlists at Amazon Music, Apple Music, or any of the streaming services.

type Track {
  added_at: String
  track_id: String
  track_name: String
  artist_id: String
  artist_name: String
  popularity: Int
  preview_url: String
  isrc: String
}

type TrackEdge {
  node: Track
  cursor: String
}

type TrackConnection {
  pageInfo: PageInfo!
  edges: [TrackEdge]
}

type Query {
  get_saved_tracks(
    access_token: String!
    first: Int! = 50
    after: String! = ""
  ): TrackConnection
    @rest(
      endpoint: "https://api.spotify.com/v1/me/tracks?limit=$first&offset=$after"
      headers: [{
        name: "Authorization",
        value: "Bearer $access_token"
      }]
      resultroot: "items[]"
      pagination: {
        type: OFFSET
        setters: [{field:"total", path: "total"}]
      }
      setters: [
        { field: "track_id", path: "track.id" }
        { field: "track_name", path: "track.name" }
        { field: "artist_id", path: "track.artists[].id" }
        { field: "artist_name", path: "track.artists[].name" }
        { field: "popularity", path: "track.popularity" }
        { field: "preview_url", path: "track.preview_url" }
        { field: "isrc", path: "track.external_ids.isrc" }
      ]
    )
}

StepZen implements the GraphQL pagination spec, so by setting options as above (notice the required TrackEdge and TrackConnection types) we can hit our StepZen GraphQL endpoint using standard cursor syntax.

// remix > app > routes > tracks.tsx

export async function getTracks(token: String, cursor: String = '') {
  let res = await fetch(`${process.env.STEPZEN_ENDPOINT}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `${process.env.STEPZEN_API_KEY}`
    },
    body: JSON.stringify({
      query: `
        query MyQuery($access_token: String!, $after: String!) {
          get_saved_tracks(access_token: $access_token, after: $after) {
            edges {
              node {
                added_at
                artist_id
                artist_name
                isrc
                popularity
                preview_url
                track_id
                track_name
              }
              cursor
            }
            pageInfo {
              endCursor
              hasNextPage
              hasPreviousPage
              startCursor
            }
          }
      }`,
      variables: {
        access_token: token,
        after: cursor
      },
    }),
  })
  let data = await res.json();
  // console.log('data in function: ', data);
  let tracks = data.data.get_saved_tracks;
  // console.log('tracks in function', tracks);
  return tracks;
}

This proves immediately useful in the loader for our /tracks route, which uses a while statement to keep track of the returned hasNextPage boolean, until the request is fully depaginated.

// remix > app > routes > tracks.tsx

export const loader: LoaderFunction = async ({ 
  request 
}) => {
  const session = await getSession(
    request.headers.get("Cookie")
    );
  const token = session.get("token") || null;
  // console.log('token: ', token)
  let tracks = await getTracks(token);
  // console.log('tracks in loader: ', tracks);
  let edges = tracks.edges;
  // console.log('edges in loader: ', edges.length);
  let endCursor = tracks.pageInfo.endCursor;
  // console.log('endCursor in loader: ', endCursor);
  let hasNextPage = tracks.pageInfo.hasNextPage;
  while (hasNextPage){
    // console.log('endCursor: ', endCursor);
    let moreTracks = await getTracks(token, endCursor)
    let moreEdges = moreTracks.edges;
    // console.log('moreEdges in loader: ', moreEdges.length);
    Array.prototype.push.apply(edges, moreEdges);
    console.log('edges after push ', edges.length);
    let moreNext = moreTracks.pageInfo.hasNextPage;
    // console.log('moreEdges in loader: ', moreEdges.length);
    let moreCursor = moreTracks.pageInfo.endCursor;
    // console.log('moreCursor in loader: ', moreCursor)
    endCursor = moreCursor;
    hasNextPage = moreNext;
    ;
  }
  return edges;
}

Mass presentable on first load (if not yet design-presentable) and suitable for cross-API transferability.

// remix > app > routes > tracks.tsx

export default function Tracks() {
  const edges = useLoaderData();
  // console.log(edges)
  const tracks = edges?.map((track, i) =>
    <li key={i}>
      {track.node.artist_name}, “{track.node.track_name}<br></br>
       <audio 
          controls
          src={track.node.preview_url}>
       </audio>
    </li>
  );
  
  return (
    <ul>
      {tracks}
    </ul>
  )
}

Comparing a massive playlist like this against multiple streaming music APIs: that'll be the subject of the next installment–and another apt showcase for StepZen's benefits, as our options for multi-API contributions to a GraphQL data model get very interesting (ie. potentially quota-busting, if not tuned carefully.)