diff --git a/client/src/api.ts b/client/src/api.ts index c5b7c6b..ebbc92e 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -31,60 +31,62 @@ export interface SongQueryElem { children?: SongQueryElem[] childrenOperator?: SongQueryElemOp, } -export interface SongQuery extends SongQueryElem {} +export interface SongQuery extends SongQueryElem { } export interface QuerySongsRequest { query: SongQuery } export interface QuerySongsResponse { ids: Number[] } -export function checkQuerySongsElem(elem:any): boolean { - if(elem.childrenOperator && elem.children) { - elem.children.forEach((child:any) => { - if(!checkQuerySongsElem(child)) { +export function checkQuerySongsElem(elem: any): boolean { + if (elem.childrenOperator && elem.children) { + elem.children.forEach((child: any) => { + if (!checkQuerySongsElem(child)) { return false; } }); } return (elem.childrenOperator && elem.children) || - (elem.prop && elem.propOperand && elem.propOperator) || - Object.keys(elem).length == 0; + (elem.prop && elem.propOperand && elem.propOperator) || + Object.keys(elem).length == 0; } -export function checkQuerySongsRequest(req:any): boolean { +export function checkQuerySongsRequest(req: any): boolean { return "query" in req && checkQuerySongsElem(req.query); } // Get song details (GET). export const SongDetailsEndpoint = '/song/:id'; -export interface SongDetailsRequest {} +export interface SongDetailsRequest { } export interface SongDetailsResponse { title: String, storeLinks: String[], artistIds: Number[], albumIds: Number[], + tagIds: Number[], } -export function checkSongDetailsRequest(req:any): boolean { +export function checkSongDetailsRequest(req: any): boolean { return true; } // Query for artists. export const QueryArtistsEndpoint = '/artist/query'; -export interface QueryArtistsRequest {} +export interface QueryArtistsRequest { } export interface QueryArtistsResponse { ids: Number[] } -export function checkQueryArtistsRequest(req:any): boolean { +export function checkQueryArtistsRequest(req: any): boolean { return true; } // Get artist details (GET). export const ArtistDetailsEndpoint = '/artist/:id'; -export interface ArtistDetailsRequest {} +export interface ArtistDetailsRequest { } export interface ArtistDetailsResponse { name: String, + tagIds: Number[], storeLinks: String[], } -export function checkArtistDetailsRequest(req:any): boolean { +export function checkArtistDetailsRequest(req: any): boolean { return true; } @@ -94,14 +96,15 @@ export interface CreateSongRequest { title: String; artistIds?: Number[]; albumIds?: Number[]; + tagIds?: Number[]; storeLinks?: String[]; } export interface CreateSongResponse { id: Number; } -export function checkCreateSongRequest(req:any): boolean { +export function checkCreateSongRequest(req: any): boolean { return "body" in req && - "title" in req.body; + "title" in req.body; } // Modify an existing song (PUT). @@ -110,10 +113,11 @@ export interface ModifySongRequest { title?: String; artistIds?: Number[]; albumIds?: Number[]; + tagIds?: Number[]; storeLinks?: String[]; } -export interface ModifySongResponse {} -export function checkModifySongRequest(req:any): boolean { +export interface ModifySongResponse { } +export function checkModifySongRequest(req: any): boolean { return true; } @@ -121,26 +125,26 @@ export function checkModifySongRequest(req:any): boolean { export const CreateArtistEndpoint = '/artist'; export interface CreateArtistRequest { name: String; - songIds?: Number[]; - albumIds?: Number[]; + tagIds?: Number[]; storeLinks?: String[]; } export interface CreateArtistResponse { id: Number; } -export function checkCreateArtistRequest(req:any): boolean { +export function checkCreateArtistRequest(req: any): boolean { return "body" in req && - "name" in req.body; + "name" in req.body; } // Modify an existing artist (PUT). export const ModifyArtistEndpoint = '/artist/:id'; export interface ModifyArtistRequest { name?: String, + tagIds?: Number[]; storeLinks?: String[], } -export interface ModifyArtistResponse {} -export function checkModifyArtistRequest(req:any): boolean { +export interface ModifyArtistResponse { } +export function checkModifyArtistRequest(req: any): boolean { return true; } @@ -153,9 +157,9 @@ export interface CreateTagRequest { export interface CreateTagResponse { id: Number; } -export function checkCreateTagRequest(req:any): boolean { +export function checkCreateTagRequest(req: any): boolean { return "body" in req && - "name" in req.body; + "name" in req.body; } // Modify an existing tag (PUT). @@ -164,28 +168,28 @@ export interface ModifyTagRequest { name?: String, parentId?: Number; } -export interface ModifyTagResponse {} -export function checkModifyTagRequest(req:any): boolean { +export interface ModifyTagResponse { } +export function checkModifyTagRequest(req: any): boolean { return true; } // Query for tags. export const QueryTagEndpoint = '/tag/query'; -export interface QueryTagsRequest {} +export interface QueryTagsRequest { } export interface QueryTagsResponse { ids: Number[] } -export function checkQueryTagsRequest(req:any): boolean { +export function checkQueryTagsRequest(req: any): boolean { return true; } // Get tag details (GET). export const TagDetailsEndpoint = '/tag/:id'; -export interface TagDetailsRequest {} +export interface TagDetailsRequest { } export interface TagDetailsResponse { name: String, parentId?: Number, } -export function checkTagDetailsRequest(req:any): boolean { +export function checkTagDetailsRequest(req: any): boolean { return true; } \ No newline at end of file diff --git a/server/endpoints/ArtistDetailsEndpointHandler.ts b/server/endpoints/ArtistDetailsEndpointHandler.ts index 47e4123..3b20bad 100644 --- a/server/endpoints/ArtistDetailsEndpointHandler.ts +++ b/server/endpoints/ArtistDetailsEndpointHandler.ts @@ -11,27 +11,30 @@ export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, re throw e; } - await models.Artist.findAll({ - where: { - id: req.params.id - } - }) - .then((artists: any[]) => { - if (artists.length != 1) { - const e: EndpointError = { - internalMessage: 'There is no artist with id ' + req.params.id + '.', - httpStatus: 400 - }; - throw e; - } - let artist = artists[0]; - const storeLinks = Array.isArray(artist.storeLinks) ? artist.storeLinks : - (artist.storeLinks ? [ artist.storeLinks ] : []); - const response: api.ArtistDetailsResponse = { - name: artist.name, - storeLinks: storeLinks, + try { + const artists: any[] = await models.Artist.findAll({ + where: { + id: req.params.id + }, + include: [models.Tag] + }); + if (artists.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no artist with id ' + req.params.id + '.', + httpStatus: 400 }; - res.send(response); - }) - .catch(catchUnhandledErrors); + throw e; + } + let artist = artists[0]; + const storeLinks = Array.isArray(artist.storeLinks) ? artist.storeLinks : + (artist.storeLinks ? [artist.storeLinks] : []); + const response: api.ArtistDetailsResponse = { + name: artist.name, + tagIds: artist.Tags.map((tag: any) => tag.id), + storeLinks: storeLinks, + }; + await res.send(response); + } catch (e) { + catchUnhandledErrors(e) + } } \ No newline at end of file diff --git a/server/endpoints/CreateArtistEndpointHandler.ts b/server/endpoints/CreateArtistEndpointHandler.ts index acd2eb5..8d02ac8 100644 --- a/server/endpoints/CreateArtistEndpointHandler.ts +++ b/server/endpoints/CreateArtistEndpointHandler.ts @@ -1,6 +1,7 @@ const models = require('../models'); import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +const { Op } = require("sequelize"); export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res: any) => { if (!api.checkCreateArtistRequest(req)) { @@ -11,9 +12,38 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res throw e; } const reqObject: api.CreateArtistRequest = req.body; - await models.Artist.create(reqObject) + + // Start retrieving the tag instances to link the artist to. + var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({ + where: { + id: { + [Op.in]: reqObject.tagIds + } + } + }); + + // Upon finish retrieving artists and albums, create the artist and associate it. + await Promise.all([tagInstancesPromise]) + .then((values: any) => { + var [tags] = values; + + if (reqObject.tagIds && tags.length !== reqObject.tagIds.length) { + const e: EndpointError = { + internalMessage: 'Not all atags exist for CreateArtist request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + + var artist = models.Artist.build({ + name: reqObject.name, + storeLinks: reqObject.storeLinks || [], + }); + tags && artist.addTags(tags); + return artist.save(); + }) .then((artist: any) => { - const responseObject: api.CreateArtistResponse = { + const responseObject: api.CreateSongResponse = { id: artist.id }; res.status(200).send(responseObject); diff --git a/server/endpoints/CreateSongEndpointHandler.ts b/server/endpoints/CreateSongEndpointHandler.ts index de060fb..976deef 100644 --- a/server/endpoints/CreateSongEndpointHandler.ts +++ b/server/endpoints/CreateSongEndpointHandler.ts @@ -31,15 +31,25 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: } }); + // Start retrieving the tag instances to link the song to. + var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({ + where: { + id: { + [Op.in]: reqObject.tagIds + } + } + }); + // Upon finish retrieving artists and albums, create the song and associate it. - await Promise.all([artistInstancesPromise, albumInstancesPromise]) + await Promise.all([artistInstancesPromise, albumInstancesPromise, tagInstancesPromise]) .then((values: any) => { - var [artists, albums] = values; + var [artists, albums, tags] = values; if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || - (reqObject.albumIds && albums.length !== reqObject.albumIds.length)) { + (reqObject.albumIds && albums.length !== reqObject.albumIds.length) || + (reqObject.tagIds && tags.length !== reqObject.tagIds.length)) { const e: EndpointError = { - internalMessage: 'Not all albums and/or artists exist for CreateSong request: ' + JSON.stringify(req.body), + internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; @@ -51,6 +61,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: }); artists && song.addArtists(artists); albums && song.addAlbums(albums); + tags && song.addTags(tags); return song.save(); }) .then((song: any) => { diff --git a/server/endpoints/SongDetailsEndpointHandler.ts b/server/endpoints/SongDetailsEndpointHandler.ts index 9ed591f..15a87d0 100644 --- a/server/endpoints/SongDetailsEndpointHandler.ts +++ b/server/endpoints/SongDetailsEndpointHandler.ts @@ -11,28 +11,30 @@ export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: throw e; } - await models.Song.findAll({ - include: [models.Artist, models.Album], - where: { - id: req.params.id - } - }) - .then((songs: any[]) => { - if (songs.length != 1) { - const e: EndpointError = { - internalMessage: 'There is no song with id ' + req.params.id + '.', - httpStatus: 400 - }; - throw e; - } - let song = songs[0]; - const response: api.SongDetailsResponse = { - title: song.title, - artistIds: song.Artists.map((artist:any) => artist.id), - albumIds: song.Albums.map((album:any) => album.id), - storeLinks: song.storeLinks, + try { + const songs = await models.Song.findAll({ + include: [models.Artist, models.Album, models.Tag], + where: { + id: req.params.id } - res.send(response); - }) - .catch(catchUnhandledErrors); + }); + if (songs.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no song with id ' + req.params.id + '.', + httpStatus: 400 + }; + throw e; + } + let song = songs[0]; + const response: api.SongDetailsResponse = { + title: song.title, + artistIds: song.Artists.map((artist: any) => artist.id), + albumIds: song.Albums.map((album: any) => album.id), + tagIds: song.Tags.map((tag: any) => tag.id), + storeLinks: song.storeLinks, + } + await res.send(response); + } catch (e) { + catchUnhandledErrors(e); + } } \ No newline at end of file diff --git a/server/test/integration/flows/ArtistFlow.js b/server/test/integration/flows/ArtistFlow.js index ac5414e..dcfb39a 100644 --- a/server/test/integration/flows/ArtistFlow.js +++ b/server/test/integration/flows/ArtistFlow.js @@ -73,8 +73,22 @@ describe('PUT /artist with an existing artist', () => { init().then((app) => { var req = chai.request(app).keepOpen(); helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }) - .then(() => helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200) ) - .then(() => helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [] } ) ) + .then(() => helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200)) + .then(() => helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] })) + .then(req.close) + .then(done); + }); + }); +}); + +describe('POST /artist with tags', () => { + it('should succeed', done => { + init().then((app) => { + var req = chai.request(app).keepOpen(); + helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) + .then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })) + .then(() => helpers.createArtist(req, { name: "MyArtist", tagIds: [ 1, 2 ] }, 200, { id: 1 })) + .then(() => helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [ 1, 2 ] })) .then(req.close) .then(done); }); diff --git a/server/test/integration/flows/SongFlow.js b/server/test/integration/flows/SongFlow.js index 8560d86..15cf56a 100644 --- a/server/test/integration/flows/SongFlow.js +++ b/server/test/integration/flows/SongFlow.js @@ -238,4 +238,18 @@ describe('POST /song/query with several songs and filters', () => { .then(done) }) }); -}); \ No newline at end of file +}); + +describe('POST /song with tags', () => { + it('should succeed', done => { + init().then((app) => { + var req = chai.request(app).keepOpen(); + helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) + .then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })) + .then(() => helpers.createSong(req, { title: "Song", tagIds: [ 1, 2 ] }, 200, { id: 1 })) + .then(() => helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [ 1, 2 ], albumIds: [], artistIds: [] })) + .then(req.close) + .then(done); + }); + }); +}); diff --git a/server/test/integration/flows/helpers.js b/server/test/integration/flows/helpers.js index 34aa52d..19c3506 100644 --- a/server/test/integration/flows/helpers.js +++ b/server/test/integration/flows/helpers.js @@ -15,6 +15,34 @@ export async function createSong( }); } +export async function modifySong( + req, + id = 1, + props = { name: "NewSong" }, + expectStatus = undefined, +) { + await req + .put('/song/' + id) + .send(props) + .then((res) => { + expectStatus && expect(res).to.have.status(expectStatus); + }); +} + +export async function checkSong( + req, + id, + expectStatus = undefined, + expectResponse = undefined, +) { + await req + .get('/song/' + id) + .then((res) => { + expectStatus && expect(res).to.have.status(expectStatus); + expectResponse && expect(res.body).to.deep.equal(expectResponse); + }) +} + export async function createArtist( req, props = { name: "Artist" },