diff --git a/client/src/api.ts b/client/src/api.ts index 58ce1c1..80a065b 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -7,38 +7,52 @@ // a request structure, a response structure and // a checking function which determines request validity. -// Retrieve a list of songs. Basic artist information is included. -export const ListSongsEndpoint = '/song/list'; -export interface ListSongsRequest {} -export interface ListSongsResponseItem { - title: String; - id: Number; - artists: { - name: String; - id: Number; - }[]; - albums: { - name: String; - id: Number; - }[]; -} -export interface ListSongsResponse extends Array{}; -export function checkListSongsRequest(req:any): boolean { +// Query for songs. +export const QuerySongsEndpoint = '/song/query'; +export interface QuerySongsRequest {} +export interface QuerySongsResponse { + ids: Number[] +} +export function checkQuerySongsRequest(req:any): boolean { return true; } -// Retrieve a list of artists. -export const ListArtistsEndpoint = '/artist/list'; -export interface ListArtistsRequest {} -export interface ListArtistsResponseItem { - name: String; - id: Number; +// Get song details. +export const SongDetailsEndpoint = '/song/details'; +export interface SongDetailsRequest { + id: Number +} +export interface SongDetailsResponse { + title: String, + artistIds: Number[], + albumIds: Number[], } -export interface ListArtistsResponse extends Array{}; -export function checkListArtistsRequest(req:any): boolean { +export function checkSongDetailsRequest(req:any): boolean { + return "id" in req; +} + +// Query for artists. +export const QueryArtistsEndpoint = '/artist/query'; +export interface QueryArtistsRequest {} +export interface QueryArtistsResponse { + ids: Number[] +} +export function checkQueryArtistsRequest(req:any): boolean { return true; } +// Get artist details. +export const ArtistDetailsEndpoint = '/artist/details'; +export interface ArtistDetailsRequest { + id: Number +} +export interface ArtistDetailsResponse { + name: String +} +export function checkArtistDetailsRequest(req:any): boolean { + return "id" in req; +} + // Create a new song. export const CreateSongEndpoint = '/song/create'; export interface CreateSongRequest { @@ -54,6 +68,16 @@ export function checkCreateSongRequest(req:any): boolean { "title" in req.body; } +// Modify an existing song. +export const ModifySongEndpoint = '/song/modify'; +export interface ModifySongRequest extends CreateSongRequest { + id: Number; +} +export interface ModifySongResponse {} +export function checkModifySongRequest(req:any): boolean { + return true; +} + // Create a new artist. export const CreateArtistEndpoint = '/artist/create'; export interface CreateArtistRequest { @@ -67,4 +91,14 @@ export interface CreateArtistResponse { export function checkCreateArtistRequest(req:any): boolean { return "body" in req && "name" in req.body; +} + +// Modify an existing artist. +export const ModifyArtistEndpoint = '/artist/modify'; +export interface ModifyArtistRequest extends CreateArtistRequest { + id: Number; +} +export interface ModifyArtistResponse {} +export function checkModifyArtistRequest(req:any): boolean { + return true; } \ No newline at end of file diff --git a/server/app.ts b/server/app.ts index d034174..82d4864 100644 --- a/server/app.ts +++ b/server/app.ts @@ -6,6 +6,8 @@ import { CreateSongEndpointHandler } from './endpoints/CreateSongEndpointHandler import { CreateArtistEndpointHandler } from './endpoints/CreateArtistEndpointHandler'; import { ListSongsEndpointHandler } from './endpoints/ListSongsEndpointHandler'; import { ListArtistsEndpointHandler } from './endpoints/ListArtistsEndpointHandler'; +import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtistEndpointHandler'; +import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler'; import * as endpointTypes from './endpoints/types'; const invokeHandler = (handler:endpointTypes.EndpointHandler) => { @@ -28,6 +30,8 @@ const SetupApp = (app: any) => { app.get(api.ListSongsEndpoint, invokeHandler(ListSongsEndpointHandler)); app.post(api.CreateArtistEndpoint, invokeHandler(CreateArtistEndpointHandler)); app.get(api.ListArtistsEndpoint, invokeHandler(ListArtistsEndpointHandler)); + app.post(api.ModifyArtistEndpoint, invokeHandler(ModifyArtistEndpointHandler)); + app.post(api.ModifySongEndpoint, invokeHandler(ModifySongEndpointHandler)); } export { SetupApp } \ No newline at end of file diff --git a/server/endpoints/ArtistDetailsEndpointHandler.ts b/server/endpoints/ArtistDetailsEndpointHandler.ts new file mode 100644 index 0000000..5c8bb39 --- /dev/null +++ b/server/endpoints/ArtistDetailsEndpointHandler.ts @@ -0,0 +1,35 @@ +const models = require('../models'); +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; + +export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => { + if (!api.checkArtistDetailsRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid ArtistDetails request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.ArtistDetailsRequest = req.body; + + await models.Artist.findAll({ + where: { + id: reqObject.id + } + }) + .then((artists: any[]) => { + if (artists.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no artist with id ' + reqObject.id + '.', + httpStatus: 400 + }; + throw e; + } + let artist = artists[0]; + const response: api.ArtistDetailsResponse = { + name: artist.name + }; + res.send(response); + }) + .catch(catchUnhandledErrors); +} \ No newline at end of file diff --git a/server/endpoints/CreateArtistEndpointHandler.ts b/server/endpoints/CreateArtistEndpointHandler.ts index c8f6163..acd2eb5 100644 --- a/server/endpoints/CreateArtistEndpointHandler.ts +++ b/server/endpoints/CreateArtistEndpointHandler.ts @@ -1,10 +1,10 @@ const models = require('../models'); import * as api from '../../client/src/api'; -import { EndpointError, EndpointHandler } from './types'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; -export const CreateArtistEndpointHandler:EndpointHandler = async (req: any, res: any) => { +export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res: any) => { if (!api.checkCreateArtistRequest(req)) { - const e:EndpointError = { + const e: EndpointError = { internalMessage: 'Invalid CreateArtist request: ' + JSON.stringify(req.body), httpStatus: 400 }; @@ -17,5 +17,6 @@ export const CreateArtistEndpointHandler:EndpointHandler = async (req: any, res: id: artist.id }; res.status(200).send(responseObject); - }); + }) + .catch(catchUnhandledErrors); } \ No newline at end of file diff --git a/server/endpoints/CreateSongEndpointHandler.ts b/server/endpoints/CreateSongEndpointHandler.ts index 40ca556..2497a89 100644 --- a/server/endpoints/CreateSongEndpointHandler.ts +++ b/server/endpoints/CreateSongEndpointHandler.ts @@ -1,10 +1,10 @@ const models = require('../models'); import * as api from '../../client/src/api'; -import { EndpointError, EndpointHandler } from './types'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; -export const CreateSongEndpointHandler:EndpointHandler = async (req: any, res: any) => { +export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: any) => { if (!api.checkCreateSongRequest(req)) { - const e:EndpointError = { + const e: EndpointError = { internalMessage: 'Invalid CreateSong request: ' + JSON.stringify(req.body), httpStatus: 400 }; @@ -21,7 +21,7 @@ export const CreateSongEndpointHandler:EndpointHandler = async (req: any, res: a }) .then((artist: any[]) => { if (artist.length != 1) { - const e:EndpointError = { + const e: EndpointError = { internalMessage: 'There is no artist with id ' + artistId + '.', httpStatus: 400 }; @@ -42,7 +42,7 @@ export const CreateSongEndpointHandler:EndpointHandler = async (req: any, res: a }) .then((album: any[]) => { if (album.length != 1) { - const e:EndpointError = { + const e: EndpointError = { internalMessage: 'There is no album with id ' + albumId + '.', httpStatus: 400 }; @@ -75,5 +75,6 @@ export const CreateSongEndpointHandler:EndpointHandler = async (req: any, res: a }; res.status(200).send(responseObject); }) - }); + }) + .catch(catchUnhandledErrors); } \ No newline at end of file diff --git a/server/endpoints/ListArtistsEndpointHandler.ts b/server/endpoints/ListArtistsEndpointHandler.ts deleted file mode 100644 index f92616b..0000000 --- a/server/endpoints/ListArtistsEndpointHandler.ts +++ /dev/null @@ -1,23 +0,0 @@ -const models = require('../models'); -import * as api from '../../client/src/api'; -import { EndpointError, EndpointHandler } from './types'; - -export const ListArtistsEndpointHandler:EndpointHandler = async (req: any, res: any) => { - if (!api.checkListArtistsRequest(req)) { - const e:EndpointError = { - internalMessage: 'Invalid ListArtists request: ' + JSON.stringify(req.body), - httpStatus: 400 - }; - throw e; - } - await models.Artist.findAll() - .then((artists: any[]) => { - const response: api.ListArtistsResponse = artists.map((artist: any) => { - return { - name: artist.name, - id: artist.id, - }; - }); - res.send(response); - }); -} \ No newline at end of file diff --git a/server/endpoints/ListSongsEndpointHandler.ts b/server/endpoints/ListSongsEndpointHandler.ts deleted file mode 100644 index fcb9be0..0000000 --- a/server/endpoints/ListSongsEndpointHandler.ts +++ /dev/null @@ -1,38 +0,0 @@ -const models = require('../models'); -import * as api from '../../client/src/api'; -import { EndpointError, EndpointHandler } from './types'; - -export const ListSongsEndpointHandler:EndpointHandler = async (req: any, res: any) => { - if (!api.checkListSongsRequest(req)) { - const e:EndpointError = { - internalMessage: 'Invalid ListSongs request: ' + JSON.stringify(req.body), - httpStatus: 400 - }; - throw e; - } - await models.Song.findAll({ - include: [models.Artist, models.Album] - }) - .then((songs: any[]) => { - console.log(songs); - const response: api.ListSongsResponse = songs.map((song: any) => { - return { - title: song.title, - id: song.id, - artists: song.Artists.map((artist: any) => { - return { - name: artist.name, - id: artist.id, - }; - }), - albums: song.Albums.map((album: any) => { - return { - name: album.name, - id: album.id, - }; - }), - }; - }); - res.send(response); - }); -} \ No newline at end of file diff --git a/server/endpoints/ModifyArtistEndpointHandler.ts b/server/endpoints/ModifyArtistEndpointHandler.ts new file mode 100644 index 0000000..a6e24af --- /dev/null +++ b/server/endpoints/ModifyArtistEndpointHandler.ts @@ -0,0 +1,34 @@ +const models = require('../models'); +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; + +export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res: any) => { + if (!api.checkModifyArtistRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid ModifyArtist request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.ModifyArtistRequest = req.body; + + await models.Artist.findAll({ + where: { id: reqObject.id } + }) + .then(async (artists: any[]) => { + if (artists.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no artist with id ' + reqObject.id + '.', + httpStatus: 400 + }; + throw e; + } + let artist = artists[0]; + artist.name = reqObject.name; + await artist.save(); + }) + .then(() => { + res.status(200); + }) + .catch(catchUnhandledErrors); +} \ No newline at end of file diff --git a/server/endpoints/ModifySongEndpointHandler.ts b/server/endpoints/ModifySongEndpointHandler.ts new file mode 100644 index 0000000..66349e9 --- /dev/null +++ b/server/endpoints/ModifySongEndpointHandler.ts @@ -0,0 +1,86 @@ +const models = require('../models'); +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; + +export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: any) => { + if (!api.checkModifySongRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid ModifySong request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.ModifySongRequest = req.body; + + // Start retrieving the artist instances to link the song to. + var artistInstancePromises: Promise[] = []; + reqObject.artistIds?.forEach((artistId: Number) => { + artistInstancePromises.push( + models.Artist.findAll({ + where: { id: artistId } + }) + .then((artist: any[]) => { + if (artist.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no artist with id ' + artistId + '.', + httpStatus: 400 + }; + throw e; + } + return artist[0]; + }) + ); + }); + var artistInstancesPromise = Promise.all(artistInstancePromises); + + // Start retrieving the album instances to link the song to. + var albumInstancePromises: Promise[] = []; + reqObject.albumIds?.forEach((albumId: Number) => { + albumInstancePromises.push( + models.Album.findAll({ + where: { id: albumId } + }) + .then((album: any[]) => { + if (album.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no album with id ' + albumId + '.', + httpStatus: 400 + }; + throw e; + } + return album[0]; + }) + ); + }); + var albumInstancesPromise = Promise.all(albumInstancePromises); + + // Start retrieving the song to modify. + var songInstancePromise: Promise = + models.Song.findAll({ + where: { id: reqObject.id } + }) + .then((song: any[]) => { + if (song.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no song with id ' + reqObject.id + '.', + httpStatus: 400 + }; + throw e; + } + return song[0]; + }); + + // Upon finish retrieving artists and albums, create the song and associate it. + await Promise.all([artistInstancesPromise, albumInstancesPromise, songInstancePromise]) + .then(async (values: any) => { + var [artists, albums, song] = values; + song.setArtists(artists); + song.setAlbums(albums); + song.setTitle(reqObject.title); + await song.save(); + }) + .then(() => { + res.status(200).send({}); + }) + .catch(catchUnhandledErrors); +} \ No newline at end of file diff --git a/server/endpoints/QueryArtistsEndpointHandler.ts b/server/endpoints/QueryArtistsEndpointHandler.ts new file mode 100644 index 0000000..30788bd --- /dev/null +++ b/server/endpoints/QueryArtistsEndpointHandler.ts @@ -0,0 +1,23 @@ +const models = require('../models'); +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; + +export const QueryArtistsEndpointHandler: EndpointHandler = async (req: any, res: any) => { + if (!api.checkQueryArtistsRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid QueryArtists request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + await models.Artist.findAll() + .then((artists: any[]) => { + const response: api.QueryArtistsResponse = { + ids: artists.map((artist: any) => { + return artist.id + }) + } + res.send(response); + }) + .catch(catchUnhandledErrors); +} \ No newline at end of file diff --git a/server/endpoints/QuerySongsEndpointHandler.ts b/server/endpoints/QuerySongsEndpointHandler.ts new file mode 100644 index 0000000..ee4be62 --- /dev/null +++ b/server/endpoints/QuerySongsEndpointHandler.ts @@ -0,0 +1,23 @@ +const models = require('../models'); +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; + +export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res: any) => { + if (!api.checkQuerySongsRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid QuerySongs request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + await models.Song.findAll() + .then((songs: any[]) => { + const response: api.QuerySongsResponse = { + ids: songs.map((song: any) => { + return song.id; + }) + }; + res.send(response); + }) + .catch(catchUnhandledErrors); +} \ No newline at end of file diff --git a/server/endpoints/SongDetailsEndpointHandler copy.ts b/server/endpoints/SongDetailsEndpointHandler copy.ts new file mode 100644 index 0000000..f7c33a1 --- /dev/null +++ b/server/endpoints/SongDetailsEndpointHandler copy.ts @@ -0,0 +1,38 @@ +const models = require('../models'); +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; + +export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => { + if (!api.checkSongDetailsRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid SongDetails request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.SongDetailsRequest = req.body; + + await models.Song.findAll({ + include: [models.Artist, models.Album], + where: { + id: reqObject.id + } + }) + .then((songs: any[]) => { + if (songs.length != 1) { + const e: EndpointError = { + internalMessage: 'There is no song with id ' + reqObject.id + '.', + httpStatus: 400 + }; + throw e; + } + let song = songs[0]; + const response: api.SongDetailsResponse = { + title: song.title, + artistIds: song.ArtistIds, + albumIds: song.AlbumIds + } + res.send(response); + }) + .catch(catchUnhandledErrors); +} \ No newline at end of file diff --git a/server/endpoints/types.ts b/server/endpoints/types.ts index b676aeb..277beb7 100644 --- a/server/endpoints/types.ts +++ b/server/endpoints/types.ts @@ -1,6 +1,25 @@ export type EndpointHandler = (req: any, res: any) => Promise; export interface EndpointError { - internalMessage:String; - httpStatus:Number; + internalMessage: String; + httpStatus: Number; +} + +export function isEndpointError(obj: any): obj is EndpointError { + return obj.internalMessage !== undefined && + obj.httpStatus !== undefined; +} + +export const catchUnhandledErrors = (_e: any) => { + if (isEndpointError(_e)) { + // Rethrow + throw _e; + } + + // This is an unhandled error, make an internal server error out of it. + const e: EndpointError = { + internalMessage: _e, + httpStatus: 500 + } + throw e; } \ No newline at end of file diff --git a/server/test/integration/flows/CreateArtistFlow.js b/server/test/integration/flows/CreateArtistFlow.js new file mode 100644 index 0000000..c0b1518 --- /dev/null +++ b/server/test/integration/flows/CreateArtistFlow.js @@ -0,0 +1,51 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const express = require('express'); +const models = require('../../../models'); +import { SetupApp } from '../../../app'; +import { expect } from 'chai'; + +async function init() { + chai.use(chaiHttp); + const app = express(); + SetupApp(app); + await models.sequelize.sync({ force: true }); + return app; +} + +describe('POST /artist/create with no name', () => { + it('should fail', done => { + init().then((app) => { + chai + .request(app) + .post('/artist/create') + .send({}) + .end((err, res) => { + expect(err).to.be.null; + expect(res).to.have.status(400); + done(); + }); + }); + }); +}); + +describe('POST /artist/create with a correct request', () => { + it('should succeed', done => { + init().then((app) => { + chai + .request(app) + .post('/artist/create') + .send({ + name: "MyArtist" + }) + .end((err, res) => { + expect(err).to.be.null; + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + id: 1 + }); + done(); + }); + }); + }); +}); diff --git a/server/test/integration/flows/CreateSongFlow.js b/server/test/integration/flows/CreateSongFlow.js index 7d6e4ed..beef2e6 100644 --- a/server/test/integration/flows/CreateSongFlow.js +++ b/server/test/integration/flows/CreateSongFlow.js @@ -13,6 +13,22 @@ async function init() { return app; } +describe('POST /song/create with no title', () => { + it('should fail', done => { + init().then((app) => { + chai + .request(app) + .post('/song/create') + .send({}) + .end((err, res) => { + expect(err).to.be.null; + expect(res).to.have.status(400); + done(); + }); + }); + }); +}); + describe('POST /song/create with only a title', () => { it('should return the first available id', done => { init().then((app) => { @@ -134,4 +150,41 @@ describe('POST /song/create with two existing artist Ids', () => { }); }); +describe('POST /song/create with an existent and a nonexistent artist Id', () => { + it('should fail', done => { + init().then((app) => { + async function createArtist(name, expectId) { + await chai.request(app) + .post('/artist/create') + .send({ + name: name + }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + id: expectId + }); + }); + } + + async function createSong() { + chai.request(app) + .post('/song/create') + .send({ + title: "MySong", + artistIds: [1, 2] + }) + .then((res) => { + expect(res).to.have.status(400); + }); + } + + init() + .then(() => { createArtist('Artist1', 1); }) + .then(createSong) + .then(done); + }); + }); +}); + export { }; \ No newline at end of file diff --git a/server/test/integration/flows/ModifyArtistFlow.js b/server/test/integration/flows/ModifyArtistFlow.js new file mode 100644 index 0000000..1a472fe --- /dev/null +++ b/server/test/integration/flows/ModifyArtistFlow.js @@ -0,0 +1,71 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const express = require('express'); +const models = require('../../../models'); +import { SetupApp } from '../../../app'; +import { expect } from 'chai'; + +async function init() { + chai.use(chaiHttp); + const app = express(); + SetupApp(app); + await models.sequelize.sync({ force: true }); + return app; +} + +describe('POST /artist/modify on nonexistent artist', () => { + it('should fail', done => { + init().then((app) => { + chai + .request(app) + .post('/artist/modify') + .send({ + id: 1, + name: "NewArtistName" + }) + .then((res) => { + expect(res).to.have.status(400); + }) + .then(done) + }); + }); +}); + +describe('POST /artist/modify with an existing artist', () => { + it('should succeed', done => { + init().then((app) => { + async function createArtist() { + await chai.request(app) + .post('/artist/create') + .send({ + name: "MyArtist" + }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + id: 1 + }); + }); + } + + async function modifyArtist() { + chai.request(app) + .post('/artist/modify') + .send({ + name: "MyNewArtist", + id: 1 + }) + .then((res) => { + expect(res).to.have.status(200); + }); + } + + // TODO: Check artist + + init() + .then(createArtist) + .then(modifyArtist) + .then(done); + }); + }); +});