Merge pull request 'Add back-end support for albums.' (#7) from backend_albums into master

Reviewed-on: #7
pull/10/head
Sander Vocke 5 years ago
commit af05e2fd5e
  1. 30
      .drone.yml
  2. 43
      client/src/api.ts
  3. 6
      server/app.ts
  4. 40
      server/endpoints/AlbumDetailsEndpointHandler.ts
  5. 63
      server/endpoints/CreateAlbumEndpointHandler.ts
  6. 2
      server/endpoints/CreateSongEndpointHandler.ts
  7. 62
      server/endpoints/ModifyAlbumEndpointHandler.ts
  8. 82
      server/endpoints/ModifySongEndpointHandler.ts
  9. 97
      server/test/integration/flows/AlbumFlow.js
  10. 43
      server/test/integration/flows/helpers.js

@ -1,18 +1,18 @@
kind: pipeline
type: kubernetes
name: front-end
steps:
- name: install dependencies
image: node
commands:
- npm install
- cd client && npm install; cd ..
- name: front-end build
image: node
commands:
- cd client && npm run-script build; cd ..
#kind: pipeline
#type: kubernetes
#name: front-end
#
#steps:
#- name: install dependencies
# image: node
# commands:
# - npm install
# - cd client && npm install; cd ..
#
#- name: front-end build
# image: node
# commands:
# - cd client && npm run-script build; cd ..
---

@ -187,6 +187,49 @@ export function checkModifySongRequest(req: any): boolean {
return true;
}
// Create a new album (POST).
export const CreateAlbumEndpoint = '/album';
export interface CreateAlbumRequest {
name: String;
tagIds?: Number[];
artistIds?: Number[];
storeLinks?: String[];
}
export interface CreateAlbumResponse {
id: Number;
}
export function checkCreateAlbumRequest(req: any): boolean {
return "body" in req &&
"name" in req.body;
}
// Modify an existing album (PUT).
export const ModifyAlbumEndpoint = '/album/:id';
export interface ModifyAlbumRequest {
name?: String;
tagIds?: Number[];
artistIds?: Number[];
storeLinks?: String[];
}
export interface ModifyAlbumResponse { }
export function checkModifyAlbumRequest(req: any): boolean {
return true;
}
// Get album details (GET).
export const AlbumDetailsEndpoint = '/album/:id';
export interface AlbumDetailsRequest { }
export interface AlbumDetailsResponse {
name: String;
tagIds: Number[];
artistIds: Number[];
songIds: Number[];
storeLinks: String[];
}
export function checkAlbumDetailsRequest(req: any): boolean {
return true;
}
// Create a new artist (POST).
export const CreateArtistEndpoint = '/artist';
export interface CreateArtistRequest {

@ -11,6 +11,9 @@ import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler
import { CreateTagEndpointHandler } from './endpoints/CreateTagEndpointHandler';
import { ModifyTagEndpointHandler } from './endpoints/ModifyTagEndpointHandler';
import { TagDetailsEndpointHandler } from './endpoints/TagDetailsEndpointHandler';
import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbumEndpointHandler';
import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandler';
import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler';
import * as endpointTypes from './endpoints/types';
const invokeHandler = (handler:endpointTypes.EndpointHandler) => {
@ -42,6 +45,9 @@ const SetupApp = (app: any) => {
app.post(api.CreateTagEndpoint, invokeHandler(CreateTagEndpointHandler));
app.put(api.ModifyTagEndpoint, invokeHandler(ModifyTagEndpointHandler));
app.get(api.TagDetailsEndpoint, invokeHandler(TagDetailsEndpointHandler));
app.post(api.CreateAlbumEndpoint, invokeHandler(CreateAlbumEndpointHandler));
app.put(api.ModifyAlbumEndpoint, invokeHandler(ModifyAlbumEndpointHandler));
app.get(api.AlbumDetailsEndpoint, invokeHandler(AlbumDetailsEndpointHandler));
}
export { SetupApp }

@ -0,0 +1,40 @@
const models = require('../models');
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkAlbumDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid AlbumDetails request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
try {
const albums = await models.Album.findAll({
include: [models.Artist, models.Tag, models.Song],
where: {
id: req.params.id
}
});
if (albums.length != 1) {
const e: EndpointError = {
internalMessage: 'There is no album with id ' + req.params.id + '.',
httpStatus: 400
};
throw e;
}
let album = albums[0];
const response: api.AlbumDetailsResponse = {
name: album.name,
artistIds: album.Artists.map((artist: any) => artist.id),
tagIds: album.Tags.map((tag: any) => tag.id),
songIds: album.Songs.map((song: any) => song.id),
storeLinks: album.storeLinks,
}
await res.send(response);
} catch (e) {
catchUnhandledErrors(e);
}
}

@ -0,0 +1,63 @@
const models = require('../models');
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize");
export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkCreateAlbumRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid CreateAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.CreateAlbumRequest = req.body;
// Start retrieving the artist instances to link the album to.
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
});
// Start retrieving the tag instances to link the album to.
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({
where: {
id: {
[Op.in]: reqObject.tagIds
}
}
});
// Upon finish retrieving artists and tags, create the album and associate it.
await Promise.all([artistInstancesPromise, tagInstancesPromise])
.then((values: any) => {
var [artists, tags] = values;
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
var album = models.Album.build({
name: reqObject.name,
storeLinks: reqObject.storeLinks || [],
});
artists && album.addArtists(artists);
tags && album.addTags(tags);
return album.save();
})
.then((album: any) => {
const responseObject: api.CreateSongResponse = {
id: album.id
};
res.status(200).send(responseObject);
})
.catch(catchUnhandledErrors);
}

@ -40,7 +40,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
}
});
// Upon finish retrieving artists and albums, create the song and associate it.
// Upon finish retrieving dependents, create the song and associate it.
await Promise.all([artistInstancesPromise, albumInstancesPromise, tagInstancesPromise])
.then((values: any) => {
var [artists, albums, tags] = values;

@ -0,0 +1,62 @@
const models = require('../models');
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize");
export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkModifyAlbumRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid ModifyAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.ModifyAlbumRequest = req.body;
// Start retrieving the artist instances to link the album to.
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
});
// Start retrieving the tag instances to link the album to.
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({
where: {
id: {
[Op.in]: reqObject.tagIds
}
}
});
// Start retrieving the album to modify.
var albumInstancePromise = models.Album.findOne({
where: {
id: req.params.id
}
});
// Upon finish retrieving artists and albums, modify the album.
await Promise.all([artistInstancesPromise, tagInstancesPromise, albumInstancePromise])
.then(async (values: any) => {
var [artists, tags, album] = values;
if (!album) {
const e: EndpointError = {
internalMessage: 'There is no album with id ' + req.params.id + '.',
httpStatus: 400
};
throw e;
}
if (reqObject.artistIds) { album.setArtists(artists) };
if (reqObject.tagIds) { album.setTags(tags) };
if (reqObject.name) { album.name = reqObject.name };
if (reqObject.storeLinks) { album.setStoreIds(reqObject.storeLinks) };
await album.save();
})
.then(() => {
res.status(200).send({});
})
.catch(catchUnhandledErrors);
}

@ -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 ModifySongEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkModifySongRequest(req)) {
@ -13,71 +14,48 @@ export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res:
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;
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
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;
var albumInstancesPromise = reqObject.albumIds && models.Album.findAll({
where: {
id: {
[Op.in]: reqObject.albumIds
}
}
});
// Start retrieving the tag instances to link the song to.
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({
where: {
id: {
[Op.in]: reqObject.tagIds
}
}
return album[0];
})
);
});
var albumInstancesPromise = Promise.all(albumInstancePromises);
// Start retrieving the song to modify.
var songInstancePromise: Promise<any> =
models.Song.findAll({
where: { id: req.params.id }
})
.then((song: any[]) => {
if (song.length != 1) {
const e: EndpointError = {
internalMessage: 'There is no song with id ' + req.params.id + '.',
httpStatus: 400
};
throw e;
var songInstancePromise = models.Song.findAll({
where: {
id: req.params.id
}
return song[0];
});
// Upon finish retrieving artists and albums, modify the song.
await Promise.all([artistInstancesPromise, albumInstancesPromise, songInstancePromise])
await Promise.all([artistInstancesPromise, albumInstancesPromise, tagInstancesPromise, songInstancePromise])
.then(async (values: any) => {
var [artists, albums, song] = values;
if(reqObject.artistIds) { song.setArtists(artists) };
if(reqObject.albumIds) { song.setAlbums(albums) };
if(reqObject.title) { song.setTitle(reqObject.title) };
if(reqObject.storeLinks) { song.setStoreIds(reqObject.storeLinks) };
var [artists, albums, tags, song] = values;
if (reqObject.artistIds) { song.setArtists(artists) };
if (reqObject.albumIds) { song.setAlbums(albums) };
if (reqObject.tagIds) { song.setTags(tags) };
if (reqObject.title) { song.setTitle(reqObject.title) };
if (reqObject.storeLinks) { song.setStoreIds(reqObject.storeLinks) };
await song.save();
})
.then(() => {

@ -0,0 +1,97 @@
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';
import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app);
await models.sequelize.sync({ force: true });
return app;
}
describe('POST /album with no name', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/album')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
});
});
});
describe('POST /album with a correct request', () => {
it('should succeed', done => {
init().then((app) => {
chai
.request(app)
.post('/album')
.send({
name: "MyAlbum"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
});
});
});
describe('PUT /album on nonexistent album', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.put('/album/1')
.send({
id: 1,
name: "NewAlbumName"
})
.then((res) => {
expect(res).to.have.status(400);
done();
})
})
});
});
describe('PUT /album with an existing album', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 })
.then(() => helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200))
.then(() => helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] }))
.then(req.close)
.then(done);
});
});
});
describe('POST /album 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.createAlbum(req, { name: "MyAlbum", tagIds: [ 1, 2 ] }, 200, { id: 1 }))
.then(() => helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [ 1, 2 ], songIds: [], artistIds: [] }))
.then(req.close)
.then(done);
});
});
});

@ -128,3 +128,46 @@ export async function checkTag(
expectResponse && expect(res.body).to.deep.equal(expectResponse);
})
}
export async function createAlbum(
req,
props = { name: "Album" },
expectStatus = undefined,
expectResponse = undefined
) {
await req
.post('/album')
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
});
}
export async function modifyAlbum(
req,
id = 1,
props = { name: "NewAlbum" },
expectStatus = undefined,
) {
await req
.put('/album/' + id)
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
});
}
export async function checkAlbum(
req,
id,
expectStatus = undefined,
expectResponse = undefined,
) {
await req
.get('/album/' + id)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
})
}
Loading…
Cancel
Save