diff --git a/client/src/api.ts b/client/src/api.ts index 8499339..748c80c 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -75,8 +75,6 @@ export enum QueryElemProperty { } export enum OrderByType { Name = 0, - ArtistRanking, - TagRanking } export interface QueryElem { prop?: QueryElemProperty, @@ -88,7 +86,6 @@ export interface QueryElem { export interface Ordering { orderBy: { type: OrderByType, - itemId?: number, } ascending: boolean, } diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index a3281d5..4d26a00 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -80,8 +80,13 @@ function addJoin(knexQuery: any, base: ObjectType, other: ObjectType) { .join(otherTable, { [otherTable + '.id']: linkTable + '.' + linkingTableIdNames[other] }); } -function addLeafWhere(knexQuery: any, queryElem: api.QueryElem) { - const simpleLeafOps = { +enum WhereType { + And = 0, + Or, +}; + +function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) { + const simpleLeafOps: Record = { [api.QueryFilterOp.Eq]: "=", [api.QueryFilterOp.Ne]: "!=", [api.QueryFilterOp.Like]: "like", @@ -101,21 +106,61 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem) { const b = queryElem.propOperand || ""; if (Object.keys(simpleLeafOps).includes(operator)) { - return knexQuery.where(a, operator, b); + if (type == WhereType.And) { + return knexQuery.andWhere(a, simpleLeafOps[operator], b); + } else if (type == WhereType.Or) { + return knexQuery.orWhere(a, simpleLeafOps[operator], b); + } + } else if (operator == api.QueryFilterOp.In) { + if (type == WhereType.And) { + return knexQuery.whereIn(a, b); + } else if (type == WhereType.Or) { + return knexQuery.orWhereIn(a, b); + } + } else if (operator == api.QueryFilterOp.NotIn) { + if (type == WhereType.And) { + return knexQuery.whereNotIn(a, b); + } else if (type == WhereType.Or) { + return knexQuery.orWhereNotIn(a, b); + } } throw "Query filter not implemented"; } -function addWhere(knexQuery: any, queryElem: api.QueryElem) { +function addBranchWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) { + if (queryElem.children && queryElem.childrenOperator === api.QueryElemOp.And) { + var q = knexQuery; + queryElem.children.forEach((child: api.QueryElem) => { + q = addWhere(q, child, type); + }) + return q; + } else if (queryElem.children && queryElem.childrenOperator === api.QueryElemOp.Or) { + var q = knexQuery; + const c = queryElem.children; + const f = function (this: any) { + for (var i = 0; i < c.length; i++) { + addWhere(this, c[i], WhereType.Or); + } + } + if (type == WhereType.And) { + return q.where(f); + } else if (type == WhereType.Or) { + return q.orWhere(f); + } + } +} + +function addWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) { if (queryElem.prop) { // Leaf node. - return addLeafWhere(knexQuery, queryElem); + return addLeafWhere(knexQuery, queryElem, type); } else if (queryElem.children) { // Branch node. + return addBranchWhere(knexQuery, queryElem, type); } - throw "Query filter not implemented."; + return knexQuery; } const objectColumns = { @@ -125,7 +170,8 @@ const objectColumns = { [ObjectType.Tag]: ['tags.id as tags.id', 'tags.name as tags.name', 'tags.parentId as tags.parentId'] }; -function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem) { +function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering, + offset: number, limit: number) { const joinObjects = getRequiredDatabaseObjects(queryElem); joinObjects.delete(queryFor); // We are already querying this object in the base query. @@ -145,9 +191,17 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl }) // Apply filtering. - q = addWhere(q, queryElem); + q = addWhere(q, queryElem, WhereType.And); + + // Apply ordering + const orderKeys = { + [api.OrderByType.Name]: objectTables[queryFor] + '.' + ((queryFor === ObjectType.Song) ? 'title' : 'name') + }; + q = q.orderBy(orderKeys[ordering.orderBy.type], + (ordering.ascending ? 'asc' : 'desc')); - // TODO: add ordering, limiting + // Apply limiting. + q = q.limit(limit).offset(offset); return q; } @@ -167,40 +221,6 @@ async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType return result; } -// // 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.type], ascstring ] -// ]; -// } - export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { if (!api.checkQueryRequest(req.body)) { const e: EndpointError = { @@ -210,6 +230,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, throw e; } const reqObject: api.QueryRequest = req.body; + console.log("Query: ", reqObject); try { const songLimit = reqObject.offsetsLimits.songLimit; @@ -220,18 +241,33 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const artistOffset = reqObject.offsetsLimits.artistOffset; const artistsPromise: Promise = (artistLimit && artistLimit > 0) ? - constructQuery(knex, ObjectType.Artist, reqObject.query) - .offset(artistOffset || 0).limit(artistLimit) : + constructQuery(knex, + ObjectType.Artist, + reqObject.query, + reqObject.ordering, + artistOffset || 0, + artistLimit + ) : (async () => [])(); const songsPromise: Promise = (songLimit && songLimit > 0) ? - constructQuery(knex, ObjectType.Song, reqObject.query) - .offset(songOffset || 0).limit(songLimit) : + constructQuery(knex, + ObjectType.Song, + reqObject.query, + reqObject.ordering, + songOffset || 0, + songLimit + ) : (async () => [])(); const tagsPromise: Promise = (tagLimit && tagLimit > 0) ? - constructQuery(knex, ObjectType.Tag, reqObject.query) - .offset(tagOffset || 0).limit(tagLimit) : + constructQuery(knex, + ObjectType.Tag, + reqObject.query, + reqObject.ordering, + tagOffset || 0, + tagLimit + ) : (async () => [])(); // For some objects, we want to return linked information as well. diff --git a/server/test/integration/flows/QueryFlow.js b/server/test/integration/flows/QueryFlow.js index 3781034..a36e42b 100644 --- a/server/test/integration/flows/QueryFlow.js +++ b/server/test/integration/flows/QueryFlow.js @@ -54,11 +54,12 @@ describe('POST /query with several songs and filters', () => { artists: [ { artistId: 1, - name: 'Artist1' + name: 'Artist1', + storeLinks: [], } ], tags: [], - rankings: [] + albums: [] }; const song2 = { songId: 2, @@ -67,11 +68,12 @@ describe('POST /query with several songs and filters', () => { artists: [ { artistId: 1, - name: 'Artist1' + name: 'Artist1', + storeLinks: [], } ], tags: [], - rankings: [] + albums: [] }; const song3 = { songId: 3, @@ -80,11 +82,12 @@ describe('POST /query with several songs and filters', () => { artists: [ { artistId: 2, - name: 'Artist2' + name: 'Artist2', + storeLinks: [], } ], tags: [], - rankings: [] + albums: [] }; async function checkAllSongs(req) {