const models = require('../models'); const { Op } = require("sequelize"); import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; enum QueryType { Song = 0, Artist, Tag, } const sequelizeOps: any = { [api.QueryFilterOp.Eq]: Op.eq, [api.QueryFilterOp.Ne]: Op.ne, [api.QueryFilterOp.In]: Op.in, [api.QueryFilterOp.NotIn]: Op.notIn, [api.QueryFilterOp.Like]: Op.like, [api.QueryElemOp.And]: Op.and, [api.QueryElemOp.Or]: Op.or, }; const sequelizeProps: any = { [QueryType.Song]: { [api.QueryElemProperty.songTitle]: "title", [api.QueryElemProperty.songId]: "id", [api.QueryElemProperty.artistName]: "$Artists.name$", [api.QueryElemProperty.albumName]: "$Albums.name$", }, [QueryType.Artist]: { [api.QueryElemProperty.songTitle]: "$Songs.title$", [api.QueryElemProperty.songId]: "$Songs.id$", [api.QueryElemProperty.artistName]: "name", [api.QueryElemProperty.albumName]: "$Albums.name$", }, [QueryType.Tag]: { [api.QueryElemProperty.songTitle]: "$Songs.title$", [api.QueryElemProperty.songId]: "$Songs.id$", [api.QueryElemProperty.artistName]: "$Artists.name$", [api.QueryElemProperty.albumName]: "$Albums.name$", } }; const sequelizeOrderColumns: any = { [QueryType.Song]: { [api.OrderBy.Name]: 'title' }, [QueryType.Artist]: { [api.OrderBy.Name]: 'name' }, [QueryType.Tag]: { [api.OrderBy.Name]: 'name' }, } // Returns the "where" clauses for Sequelize, per object type. const getSequelizeWhere = (queryElem: api.QueryElem, type: QueryType) => { var where: any = { [Op.and]: [] }; if (queryElem.prop && queryElem.propOperator && queryElem.propOperand) { // Visit a filter-like subquery leaf. where[Op.and].push({ [sequelizeProps[type][queryElem.prop]]: { [sequelizeOps[queryElem.propOperator]]: queryElem.propOperand } }); } if (queryElem.childrenOperator && queryElem.children) { // Recursively visit a nested subquery. const children = queryElem.children.map((child: api.QueryElem) => getSequelizeWhere(child, type)); where[Op.and].push({ [sequelizeOps[queryElem.childrenOperator]]: children }); } return where; } function getSequelizeOrder(order: api.Ordering, type: QueryType) { const ascstring = order.ascending ? 'ASC' : 'DESC'; return [ [ sequelizeOrderColumns[type][order.orderBy], ascstring ] ]; } export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) => { if (!api.checkQueryRequest(req.body)) { const e: EndpointError = { internalMessage: 'Invalid Query request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; } const reqObject: api.QueryRequest = req.body; try { const songs = (reqObject.songLimit > 0) && await models.Song.findAll({ // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938. // Custom pagination is implemented before responding. where: getSequelizeWhere(reqObject.query, QueryType.Song), order: getSequelizeOrder(reqObject.ordering, QueryType.Song), include: [models.Artist, models.Album, models.Tag, models.Ranking], //limit: reqObject.limit, //offset: reqObject.offset, }) const artists = (reqObject.artistLimit > 0) && await models.Artist.findAll({ // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938. // Custom pagination is implemented before responding. where: getSequelizeWhere(reqObject.query, QueryType.Artist), order: getSequelizeOrder(reqObject.ordering, QueryType.Artist), include: [models.Song, models.Album, models.Tag], //limit: reqObject.limit, //offset: reqObject.offset, }) const tags = (reqObject.tagLimit > 0) && await models.Tag.findAll({ // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938. // Custom pagination is implemented before responding. where: getSequelizeWhere(reqObject.query, QueryType.Tag), order: getSequelizeOrder(reqObject.ordering, QueryType.Tag), include: [models.Song, models.Album, models.Artist], //limit: reqObject.limit, //offset: reqObject.offset, }) const response: api.QueryResponse = { songs: (reqObject.songLimit <= 0) ? [] : await Promise.all(songs.map(async (song: any) => { const artists = song.getArtists(); const tags = song.getTags(); const rankings = song.getRankings(); return { songId: song.id, title: song.title, storeLinks: song.storeLinks, artists: (await artists).map((artist: any) => { return { artistId: artist.id, name: artist.name, } }), tags: (await tags).map((tag: any) => { return { tagId: tag.id, name: tag.name, } }), rankings: await (await rankings).map(async (ranking: any) => { const maybeTagContext: api.TagDetails | undefined = await ranking.getTagContext(); const maybeArtistContext: api.ArtistDetails | undefined = await ranking.getArtistContext(); const maybeContext = maybeTagContext || maybeArtistContext; return { rankingId: ranking.id, type: api.ItemType.Song, rankedId: song.id, context: maybeContext, value: ranking.value, } }) }; }).slice(reqObject.songOffset, reqObject.songOffset + reqObject.songLimit)), // TODO: custom pagination due to bug mentioned above artists: (reqObject.artistLimit <= 0) ? [] : await Promise.all(artists.map(async (artist: any) => { return { artistId: artist.id, name: artist.name, }; }).slice(reqObject.artistOffset, reqObject.artistOffset + reqObject.artistLimit)), tags: (reqObject.tagLimit <= 0) ? [] : await Promise.all(tags.map(async (tag: any) => { return { tagId: tag.id, name: tag.name, }; }).slice(reqObject.tagOffset, reqObject.tagOffset + reqObject.tagLimit)), }; res.send(response); } catch (e) { catchUnhandledErrors(e); } }