From ff42bda8d232a37ae554c70208488fb2a7bb1352 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 14 Aug 2020 12:01:34 +0200 Subject: [PATCH 1/3] Add POST API for albums. --- client/src/api.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/src/api.ts b/client/src/api.ts index 431ce8a..994e166 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -187,6 +187,22 @@ 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; +} + // Create a new artist (POST). export const CreateArtistEndpoint = '/artist'; export interface CreateArtistRequest { From 6ad8c3c9b8743a5c16662235cbb1e42563fc1a48 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 14 Aug 2020 17:33:41 +0200 Subject: [PATCH 2/3] Add and fix tests and endpoints. --- client/src/api.ts | 27 ++++++ server/app.ts | 6 ++ .../endpoints/AlbumDetailsEndpointHandler.ts | 40 ++++++++ .../endpoints/CreateAlbumEndpointHandler.ts | 63 ++++++++++++ server/endpoints/CreateSongEndpointHandler.ts | 2 +- .../endpoints/ModifyAlbumEndpointHandler.ts | 62 ++++++++++++ server/endpoints/ModifySongEndpointHandler.ts | 90 +++++++---------- server/test/integration/flows/AlbumFlow.js | 97 +++++++++++++++++++ server/test/integration/flows/helpers.js | 43 ++++++++ 9 files changed, 373 insertions(+), 57 deletions(-) create mode 100644 server/endpoints/AlbumDetailsEndpointHandler.ts create mode 100644 server/endpoints/CreateAlbumEndpointHandler.ts create mode 100644 server/endpoints/ModifyAlbumEndpointHandler.ts create mode 100644 server/test/integration/flows/AlbumFlow.js diff --git a/client/src/api.ts b/client/src/api.ts index 994e166..34887ee 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -203,6 +203,33 @@ export function checkCreateAlbumRequest(req: any): boolean { "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 { diff --git a/server/app.ts b/server/app.ts index 3eb1619..d1d6936 100644 --- a/server/app.ts +++ b/server/app.ts @@ -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 } \ No newline at end of file diff --git a/server/endpoints/AlbumDetailsEndpointHandler.ts b/server/endpoints/AlbumDetailsEndpointHandler.ts new file mode 100644 index 0000000..85300ba --- /dev/null +++ b/server/endpoints/AlbumDetailsEndpointHandler.ts @@ -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); + } +} \ No newline at end of file diff --git a/server/endpoints/CreateAlbumEndpointHandler.ts b/server/endpoints/CreateAlbumEndpointHandler.ts new file mode 100644 index 0000000..3fe833a --- /dev/null +++ b/server/endpoints/CreateAlbumEndpointHandler.ts @@ -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); +} \ No newline at end of file diff --git a/server/endpoints/CreateSongEndpointHandler.ts b/server/endpoints/CreateSongEndpointHandler.ts index 976deef..8d55e71 100644 --- a/server/endpoints/CreateSongEndpointHandler.ts +++ b/server/endpoints/CreateSongEndpointHandler.ts @@ -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; diff --git a/server/endpoints/ModifyAlbumEndpointHandler.ts b/server/endpoints/ModifyAlbumEndpointHandler.ts new file mode 100644 index 0000000..ed92c6d --- /dev/null +++ b/server/endpoints/ModifyAlbumEndpointHandler.ts @@ -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); +} \ No newline at end of file diff --git a/server/endpoints/ModifySongEndpointHandler.ts b/server/endpoints/ModifySongEndpointHandler.ts index a234a4e..7b73752 100644 --- a/server/endpoints/ModifySongEndpointHandler.ts +++ b/server/endpoints/ModifySongEndpointHandler.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 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[] = []; - 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 = reqObject.artistIds && models.Artist.findAll({ + where: { + id: { + [Op.in]: reqObject.artistIds + } + } }); - 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 = 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 + } + } }); - var albumInstancesPromise = Promise.all(albumInstancePromises); // Start retrieving the song to modify. - var songInstancePromise: Promise = - 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; - } - return song[0]; - }); + var songInstancePromise = models.Song.findAll({ + where: { + id: req.params.id + } + }); // 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(() => { diff --git a/server/test/integration/flows/AlbumFlow.js b/server/test/integration/flows/AlbumFlow.js new file mode 100644 index 0000000..91fcc61 --- /dev/null +++ b/server/test/integration/flows/AlbumFlow.js @@ -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); + }); + }); +}); + diff --git a/server/test/integration/flows/helpers.js b/server/test/integration/flows/helpers.js index 19c3506..7bd89d0 100644 --- a/server/test/integration/flows/helpers.js +++ b/server/test/integration/flows/helpers.js @@ -127,4 +127,47 @@ export async function checkTag( expectStatus && expect(res).to.have.status(expectStatus); 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); + }) } \ No newline at end of file From d60d6f27c300a5a08fda34270fc0166b1af0d693 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 14 Aug 2020 17:42:15 +0200 Subject: [PATCH 3/3] Disable front-end build. --- .drone.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.drone.yml b/.drone.yml index e7f7a1d..67fda88 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 .. --- @@ -32,4 +32,4 @@ steps: commands: - cd server && npm test; cd .. - \ No newline at end of file +