diff --git a/client/src/api.ts b/client/src/api.ts index e9825bc..701acbb 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -7,14 +7,51 @@ // a request structure, a response structure and // a checking function which determines request validity. -// Query for songs. +// Query for songs (POST). export const QuerySongsEndpoint = '/song/query'; -export interface QuerySongsRequest {} +export enum SongQueryElemOp { + And = "AND", + Or = "OR", +} +export enum SongQueryFilterOp { + Eq = "EQ", + Ne = "NE", + In = "IN", + NotIn = "NOTIN" +} +export enum SongQueryElemProperty { + id = "id", + artistIds = "artistIds", + albumIds = "albumIds" +} +export interface SongQueryElem { + prop?: SongQueryElemProperty, + propOperand?: any, + propOperator?: SongQueryFilterOp, + children?: SongQueryElem[] + childrenOperator?: SongQueryElemOp, +} +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)) { + return false; + } + }); + } + return (elem.childrenOperator && elem.children) || + (elem.prop && elem.propOperand && elem.propOperator) || + Object.keys(elem).length == 0; +} export function checkQuerySongsRequest(req:any): boolean { - return true; + return "query" in req && checkQuerySongsElem(req.query); } // Get song details (GET). diff --git a/server/app.ts b/server/app.ts index dc203a1..851a499 100644 --- a/server/app.ts +++ b/server/app.ts @@ -16,6 +16,7 @@ const invokeHandler = (handler:endpointTypes.EndpointHandler) => { return async (req: any, res: any) => { console.log("Incoming", req.method, " @ ", req.url); await handler(req, res) + .catch(endpointTypes.catchUnhandledErrors) .catch((_e:endpointTypes.EndpointError) => { let e:endpointTypes.EndpointError = _e; console.log("Error handling request: ", e.internalMessage); @@ -31,9 +32,9 @@ const SetupApp = (app: any) => { // Set up REST API endpoints app.post(api.CreateSongEndpoint, invokeHandler(CreateSongEndpointHandler)); - app.get(api.QuerySongsEndpoint, invokeHandler(QuerySongsEndpointHandler)); + app.post(api.QuerySongsEndpoint, invokeHandler(QuerySongsEndpointHandler)); app.post(api.CreateArtistEndpoint, invokeHandler(CreateArtistEndpointHandler)); - app.get(api.QueryArtistsEndpoint, invokeHandler(QueryArtistsEndpointHandler)); + app.post(api.QueryArtistsEndpoint, invokeHandler(QueryArtistsEndpointHandler)); app.put(api.ModifyArtistEndpoint, invokeHandler(ModifyArtistEndpointHandler)); app.put(api.ModifySongEndpoint, invokeHandler(ModifySongEndpointHandler)); app.get(api.SongDetailsEndpoint, invokeHandler(SongDetailsEndpointHandler)); diff --git a/server/endpoints/QuerySongsEndpointHandler.ts b/server/endpoints/QuerySongsEndpointHandler.ts index ee4be62..bfd4265 100644 --- a/server/endpoints/QuerySongsEndpointHandler.ts +++ b/server/endpoints/QuerySongsEndpointHandler.ts @@ -1,7 +1,46 @@ const models = require('../models'); +const { Op } = require("sequelize"); import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +const getSequelizeWhere = (queryElem: api.SongQueryElem) => { + var and = []; + + var sequelizeOps:any = {}; + sequelizeOps[api.SongQueryFilterOp.Eq] = Op.eq; + sequelizeOps[api.SongQueryFilterOp.Ne] = Op.ne; + sequelizeOps[api.SongQueryFilterOp.In] = Op.in; + sequelizeOps[api.SongQueryFilterOp.NotIn] = Op.notIn; + sequelizeOps[api.SongQueryElemOp.And] = Op.and; + sequelizeOps[api.SongQueryElemOp.Or] = Op.or; + + var sequelizeProps:any = {}; + sequelizeProps[api.SongQueryElemProperty.id] = "id"; + sequelizeProps[api.SongQueryElemProperty.artistIds] = "artistIds"; + sequelizeProps[api.SongQueryElemProperty.albumIds] = "albumIds"; + + if (queryElem.prop && queryElem.propOperator && queryElem.propOperand) { + const prop = sequelizeProps[queryElem.prop]; + const op = sequelizeOps[queryElem.propOperator]; + var filter:any = {}; + filter[op] = queryElem.propOperand; + var where:any = {}; + where[prop] = filter; + and.push(where); + } + if (queryElem.childrenOperator && queryElem.children) { + const children = queryElem.children.map((child: api.SongQueryElem) => getSequelizeWhere(child)); + const op = sequelizeOps[queryElem.childrenOperator]; + var where:any = {}; + where[op] = children; + and.push(where) + } + + return { + [Op.and]: and + }; +} + export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res: any) => { if (!api.checkQuerySongsRequest(req)) { const e: EndpointError = { @@ -10,7 +49,11 @@ export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res: }; throw e; } - await models.Song.findAll() + const reqObject: api.QuerySongsRequest = req.body; + + await models.Song.findAll({ + where: getSequelizeWhere(reqObject.query) + }) .then((songs: any[]) => { const response: api.QuerySongsResponse = { ids: songs.map((song: any) => { diff --git a/server/test/integration/flows/ModifyArtistFlow.js b/server/test/integration/flows/ModifyArtistFlow.js index f2a7fdd..8b65594 100644 --- a/server/test/integration/flows/ModifyArtistFlow.js +++ b/server/test/integration/flows/ModifyArtistFlow.js @@ -73,8 +73,7 @@ describe('PUT /artist with an existing artist', () => { var req = chai.request(app).keepOpen(); - init() - .then(() => createArtist(req)) + createArtist(req) .then(() => modifyArtist(req)) .then(() => checkArtist(req)) .then(() => req.close()) diff --git a/server/test/integration/flows/QuerySongsFlow.js b/server/test/integration/flows/QuerySongsFlow.js new file mode 100644 index 0000000..8aef61f --- /dev/null +++ b/server/test/integration/flows/QuerySongsFlow.js @@ -0,0 +1,73 @@ +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 /song/query with no songs', () => { + it('should give empty list', done => { + init().then((app) => { + chai + .request(app) + .post('/song/query') + .send({ + 'query': {} + }) + .end((err, res) => { + expect(err).to.be.null; + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + ids: [] + }); + done(); + }); + }); + }); +}); + +describe('POST /song/query with several songs', () => { + it('should give empty list', done => { + init().then((app) => { + async function createSong(req) { + await req + .post('/song') + .send({ + title: "Song" + }) + .then((res) => { + expect(res).to.have.status(200); + }); + } + + async function checkSongs(req) { + await req + .post('/song/query') + .send({ "query": {} }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + ids: [1, 2, 3] + }); + }); + } + + var req = chai.request(app).keepOpen(); + + createSong(req) + .then(() => createSong(req)) + .then(() => createSong(req)) + .then(() => checkSongs(req)) + .then(() => req.close()) + .then(done) + }); + }); +}); \ No newline at end of file