In this lesson we will:
- Finish the Query API adding relationships for Artists > Albums > Track.
- Add a mutation to insert Artists, plus Albums with Tracks.
-
Update
schema.graphqlto link up Artists > Albums > Trackstype Artist { id: Int! name: String! albums: [Album] } type Album { id: Int! title: String! tracks: [Track] } type Track { id: Int! name: String! composer: String milliseconds: Int bytes: Int unitPrice: Float } type Query { ...
-
Add definitions for
AlbumandTrackand addalbumstoArtistin our modelmodel/Track.tsexport default interface Track { id: number; name: string; composer: string; milliseconds: number; bytes: number; unitPrice: number; };
model/Album.tsimport Track from "./Track"; export default interface Album { id: number; title: string; tracks: Track[]; };
model/Artist.tsimport Album from "./Album"; export default interface Artist { id: number; name: string; albums: Album[]; };
-
Add the album and track select statements and data access methods to the top of
ChinookService.tsexport class ChinookService { private static readonly artistSelect = 'select ArtistId as id, Name as name from artists'; private static readonly albumSelect = 'select AlbumId as id, Title as title from albums'; private static readonly trackSelect = ` select TrackId as id, Name as name, Composer as composer, Milliseconds as milliseconds, Bytes as bytes, unitPrice as unitPrice from tracks`; private file: string; private lockId: string; private db: Database | undefined; constructor(file: string) { this.file = file; this.lockId = uuid(); } public async artist(id: number): Promise<Artist> { return this.get<Artist>(`${ChinookService.artistSelect} where ArtistId = ?`, id); } public async artistsByName(nameLike: string): Promise<Artist[]> { return this.all<Artist>(`${ChinookService.artistSelect} where Name like ? order by Name`, nameLike); } public async artists(): Promise<Artist[]> { return this.all<Artist>(ChinookService.artistSelect); } public async album(id: number): Promise<Album[]> { return this.all<Album>(`${ChinookService.albumSelect} where AlbumId = ?`, id); } public async albums(): Promise<Album[]> { return this.all<Album>(ChinookService.albumSelect); } public async albumsByTitle(titleLike: string): Promise<Album[]> { return this.all<Album>(`${ChinookService.albumSelect} where Title like ? order by Title`, titleLike); } public async albumsByArtist(artistId: number): Promise<Album[]> { return this.all<Album>(`${ChinookService.albumSelect} where ArtistId = ? order by Title`, artistId); } public async tracksByAlbum(albumId: number): Promise<Track[]> { return this.all<Track>(`${ChinookService.trackSelect} where AlbumId = ?`, albumId); } public async tracksByComposer(composerLike: string): Promise<Track[]> { return this.all<Track>(`${ChinookService.trackSelect} where Composer like ? order by Name`, composerLike); } public async testConnection(): Promise<void> { ...
Add the required imports using VS Code quick fix (Ctrl+. or Ctrl+Enter)
-
Add corresponding album and track tests to
ChinookService.spec.tsdescribe("#albums", () => { it("should return albums", async () => { const albums = await new ChinookService(databaseFile).albums(); console.log(albums); }); }); describe("#album", () => { it("should return a single album", async () => { const albums = await new ChinookService(databaseFile).album(1); console.log(albums); }); }); describe("#albumsByTitleLike back%", () => { it("should return albums matching the specified title", async () => { const albums = await new ChinookService(databaseFile).albumsByTitle( "back%" ); console.log(albums); }); }); describe("#albumsByArtist", () => { it("should return albums for the specified artist", async () => { const albums = await new ChinookService(databaseFile).albumsByArtist(1); console.log(albums); }); }); describe("#tracksByAlbum", () => { it("should return tracks for the specified album", async () => { const tracks = await new ChinookService(databaseFile).tracksByAlbum(1); console.log(tracks); }); }); describe("#tracksByComposerLike %lars%", () => { it("should return tracks matching the specified composer", async () => { const albums = await new ChinookService(databaseFile).tracksByComposer( "%lars%" ); console.log(albums); }); });
✔ If the Mocha sidebar is running all the new tests should appear and turn green
-
Add
ArtistandAlbumentries withalbumsandtracksfunctions inresolvers.ts.Add them below the
Querymember..Query { ... }, Artist: { albums: async (source: Artist) => chinookService.albumsByArtist(source.id), }, Album: { tracks: async (source: Album) => chinookService.tracksByAlbum(source.id), },
Note that the parent object instance will be the first
sourceresolver argument. We're not usingargsthis time, although you'd often include optional filtering, paging or ordering parameters.
Experiment running different queries returning albums and tracks for artists, e.g.
{
artist(id: 1) {
name
albums {
title
tracks {
name
composer
milliseconds
bytes
unitPrice
}
}
}
}This simply adds in another entry point for querying Albums and Tracks without going through Artists.
-
Add functions into the
Querydefinition inschema.graphql""" Returns all albums """ albums: [Album] """ Finds an album by id """ album(id: Int!): Album """ Finds albums with a matching title. Supports \`%\` \`like\` syntax. """ albumsByTitle(titleLike: String!): [Album] """ Finds tracks with a matching composer. Supports \`%\` \`like\` syntax. """ tracksByComposer(composerLike: String!): [Track]
-
Add the corresponding resolver functions to
Queryinresolvers.tsalbums: async () => chinookService.albums(), album: async (source: any, { id }: { id: number }) => chinookService.album(id), albumsByTitle: async (source: any, { titleLike }: { titleLike: string }) => chinookService.albumsByTitle(titleLike), tracksByComposer: async (source: any, { composerLike }: { composerLike: string }) => chinookService.tracksByComposer(composerLike),
Experiment running different album and track queries, e.g.
{
tracksByComposer(composerLike: "%kirk%") {
name
composer
milliseconds
bytes
unitPrice
}
albums {
title
}
}-
Add an
insertArtistfunction toChinookService.tspublic async insertArtist(name: string): Promise<Artist> { const statement = await this.run('insert into Artists (Name) values (?)', name); return { id: statement.lastID, name, albums: [], }; }
-
Add a test to
ChinookService.spec.tsdescribe("#insertArtist", () => { it("should create a single artist", async () => { const artist = await new ChinookService(databaseFile).insertArtist( "The Dirty Floors" ); console.log(artist); }); });
-
Add a
Mutationentry with acreateArtistfunction inresolvers.ts... Mutation: { createArtist: async (source: any, { name }: { name: string }) => chinookService.insertArtist(name), },
-
Add a Mutation type to
schema.graphqltype Mutation { createArtist(name: String!): Artist }
mutation {
createArtist(name: "Broken Code") {
id
name
}
}-
Add an
insertArtistfunction toChinookService.tspublic async insertAlbum(artistId: number, title: string, tracks: Track[]): Promise<Album> { const albumInsertStatement = await this.run( 'insert into Albums (Title, ArtistId) values (?, ?)', title, artistId, ); const album: Album = { id: albumInsertStatement.lastID, title, tracks, }; const trackInsertStatement = await this.prepare( 'insert into Tracks (AlbumId, Name, Composer, Milliseconds, Bytes, UnitPrice, MediaTypeId) values (?, ?, ?, ?, ?, ?, ?)', ); await Promise.all( tracks.map(async track => { const statement = await trackInsertStatement.run( album.id, track.name, track.composer, track.milliseconds, track.bytes, track.unitPrice, 5, // <<< 'AAC Audio File' ); track.id = statement.lastID; }), ); return album; }
-
Add a second
MutationfunctioncreateAlbuminresolvers.ts, accepting theartistId,titleof the album and list oftracks.Mutation: { ... createAlbum: async ( source: any, { artistId, title, tracks }: { artistId: number; title: string; tracks: Track[] }, ) => chinookService.insertAlbum(artistId, title, tracks), },
-
Add a
TrackInputtype toschema.graphql.- Note the use of
inputkeyword in place of thetypekeyword. - GraphQL is forces the pattern of declaring output and input types separately.
input TrackInput { name: String! composer: String milliseconds: Int bytes: Int unitPrice: Float }
- Note the use of
-
Add a second
Mutationdefinition forcreateAlbuminschema.graphqltype Mutation { ... createAlbum(artistId: Int!, title: String!, tracks: [TrackInput]): Album }
For this mutation query, we'll use parameters and a variables map instead of string encoding values into the query string.
mutation($artistId: Int!, $title: String!, $tracks: [TrackInput]) {
createAlbum(artistId: $artistId, title: $title, tracks: $tracks) {
id
title
tracks {
id
name
composer
milliseconds
bytes
}
}
}{
"artistId": 275,
"title": "Classic Hits",
"tracks": [
{
"name": "I Love Work",
"milliseconds": 3000,
"bytes": 100,
"unitPrice": 0.99
},
{
"name": "I Love My Wife",
"milliseconds": 4050,
"bytes": 200,
"unitPrice": 0.99
}
]
}We really built a lot in this lesson.
✔ Added resolvers between entities using the source (parent object) argument.
✔ Added a reasonably complex mutation function.
✔ Used a parameterised query to safely and easily specify input variables.
That's it for the server-side for now. Next we'll look at graphql from a consumer point of view.