Refactoring query and details endpoints.

pull/7/head
Sander Vocke 5 years ago
parent e159cfec37
commit 63ac5b5d84
  1. 82
      client/src/api.ts
  2. 4
      server/app.ts
  3. 35
      server/endpoints/ArtistDetailsEndpointHandler.ts
  4. 9
      server/endpoints/CreateArtistEndpointHandler.ts
  5. 13
      server/endpoints/CreateSongEndpointHandler.ts
  6. 23
      server/endpoints/ListArtistsEndpointHandler.ts
  7. 38
      server/endpoints/ListSongsEndpointHandler.ts
  8. 34
      server/endpoints/ModifyArtistEndpointHandler.ts
  9. 86
      server/endpoints/ModifySongEndpointHandler.ts
  10. 23
      server/endpoints/QueryArtistsEndpointHandler.ts
  11. 23
      server/endpoints/QuerySongsEndpointHandler.ts
  12. 38
      server/endpoints/SongDetailsEndpointHandler copy.ts
  13. 23
      server/endpoints/types.ts
  14. 51
      server/test/integration/flows/CreateArtistFlow.js
  15. 53
      server/test/integration/flows/CreateSongFlow.js
  16. 71
      server/test/integration/flows/ModifyArtistFlow.js

@ -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;
}[];
// Query for songs.
export const QuerySongsEndpoint = '/song/query';
export interface QuerySongsRequest {}
export interface QuerySongsResponse {
ids: Number[]
}
export interface ListSongsResponse extends Array<ListSongsResponseItem>{};
export function checkListSongsRequest(req:any): boolean {
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 function checkSongDetailsRequest(req:any): boolean {
return "id" in req;
}
export interface ListArtistsResponse extends Array<ListArtistsResponseItem>{};
export function checkListArtistsRequest(req:any): boolean {
// 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 {
@ -68,3 +92,13 @@ 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;
}

@ -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 }

@ -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);
}

@ -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);
}

@ -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);
}

@ -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);
});
}

@ -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);
});
}

@ -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);
}

@ -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<any>[] = [];
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<any>[] = [];
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<any> =
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);
}

@ -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);
}

@ -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);
}

@ -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);
}

@ -1,6 +1,25 @@
export type EndpointHandler = (req: any, res: any) => Promise<void>;
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;
}

@ -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();
});
});
});
});

@ -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 { };

@ -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);
});
});
});
Loading…
Cancel
Save