From 1d5df9b43b46bdac8c2e00f271df1e2c44539bdd Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Tue, 1 Sep 2020 14:35:43 +0200 Subject: [PATCH] Got some basic filtering working. Not finished with queries yet. --- server/endpoints/QueryEndpointHandler.ts | 195 +++++++++++++++-------- 1 file changed, 131 insertions(+), 64 deletions(-) diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 5eb4a8f..a3281d5 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -80,68 +80,92 @@ function addJoin(knexQuery: any, base: ObjectType, other: ObjectType) { .join(otherTable, { [otherTable + '.id']: linkTable + '.' + linkingTableIdNames[other] }); } +function addLeafWhere(knexQuery: any, queryElem: api.QueryElem) { + const simpleLeafOps = { + [api.QueryFilterOp.Eq]: "=", + [api.QueryFilterOp.Ne]: "!=", + [api.QueryFilterOp.Like]: "like", + } + + const propertyKeys = { + [api.QueryElemProperty.songTitle]: 'songs.title', + [api.QueryElemProperty.songId]: 'songs.id', + [api.QueryElemProperty.artistName]: 'artists.name', + [api.QueryElemProperty.artistId]: 'artists.id', + [api.QueryElemProperty.albumName]: 'albums.name', + } + + if (!queryElem.propOperator) throw "Cannot create where clause without an operator."; + const operator = queryElem.propOperator || api.QueryFilterOp.Eq; + const a = queryElem.prop && propertyKeys[queryElem.prop]; + const b = queryElem.propOperand || ""; + + if (Object.keys(simpleLeafOps).includes(operator)) { + return knexQuery.where(a, operator, b); + } + + throw "Query filter not implemented"; +} + +function addWhere(knexQuery: any, queryElem: api.QueryElem) { + if (queryElem.prop) { + // Leaf node. + return addLeafWhere(knexQuery, queryElem); + } else if (queryElem.children) { + // Branch node. + } + + throw "Query filter not implemented."; +} + +const objectColumns = { + [ObjectType.Song]: ['songs.id as songs.id', 'songs.title as songs.title', 'songs.storeLinks as songs.storeLinks'], + [ObjectType.Artist]: ['artists.id as artists.id', 'artists.name as artists.name', 'artists.storeLinks as artists.storeLinks'], + [ObjectType.Album]: ['albums.id as albums.id', 'albums.name as albums.name', 'albums.storeLinks as albums.storeLinks'], + [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) { + const joinObjects = getRequiredDatabaseObjects(queryElem); + joinObjects.delete(queryFor); // We are already querying this object in the base query. + + // Figure out what data we want to select from the results. + var columns: string[] = []; + joinObjects.forEach((obj: ObjectType) => columns.push(...objectColumns[obj])); + columns.push(...objectColumns[queryFor]); + // First, we create a base query for the type of object we need to yield. - var q = knex.select('*').distinct().from(objectTables[queryFor]); + var q = knex.select(columns) + .distinct(objectTables[queryFor] + '.' + 'id') + .from(objectTables[queryFor]); // Now, we need to add join statements for other objects we want to filter on. - const allObjects = getRequiredDatabaseObjects(queryElem); - allObjects.delete(queryFor); // We are already querying this object in the base query. - allObjects.forEach((object: ObjectType) => { + joinObjects.forEach((object: ObjectType) => { q = addJoin(q, queryFor, object); }) - // TODO: apply filters. + // Apply filtering. + q = addWhere(q, queryElem); + + // TODO: add ordering, limiting + return q; } -// 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.artistId]: "$Artists.id$", -// [api.QueryElemProperty.albumName]: "$Albums.name$", -// }, -// [QueryType.Artist]: { -// [api.QueryElemProperty.songTitle]: "$Songs.title$", -// [api.QueryElemProperty.songId]: "$Songs.id$", -// [api.QueryElemProperty.artistName]: "name", -// [api.QueryElemProperty.artistId]: "id", -// [api.QueryElemProperty.albumName]: "$Albums.name$", -// }, -// [QueryType.Tag]: { -// [api.QueryElemProperty.songTitle]: "$Songs.title$", -// [api.QueryElemProperty.songId]: "$Songs.id$", -// [api.QueryElemProperty.artistName]: "$Artists.name$", -// [api.QueryElemProperty.artistId]: "$Artists.id$", -// [api.QueryElemProperty.albumName]: "$Albums.name$", -// } -// }; - -// const sequelizeOrderColumns: any = { -// [QueryType.Song]: { -// [api.OrderByType.Name]: 'title', -// [api.OrderByType.ArtistRanking]: '$Rankings.rank$', -// [api.OrderByType.TagRanking]: '$Rankings.rank$', -// }, -// [QueryType.Artist]: { -// [api.OrderByType.Name]: 'name' -// }, -// [QueryType.Tag]: { -// [api.OrderByType.Name]: 'name' -// }, -// } +async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType, baseIds: number[]) { + var result: Record = {}; + const otherTable = objectTables[linked]; + const linkingTable = getLinkingTable(base, linked); + const columns = objectColumns[linked]; + + await Promise.all(baseIds.map((baseId: number) => { + return knex.select(columns).distinct(otherTable + '.id').from(otherTable) + .join(linkingTable, { [linkingTable + '.' + linkingTableIdNames[linked]]: otherTable + '.id' }) + .where({ [linkingTable + '.' + linkingTableIdNames[base]]: baseId }) + .then((others: any) => { result[baseId] = others; }) + })) + return result; +} // // Returns the "where" clauses for Sequelize, per object type. // const getSequelizeWhere = (queryElem: api.QueryElem, type: QueryType) => { @@ -210,31 +234,74 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, .offset(tagOffset || 0).limit(tagLimit) : (async () => [])(); - const [songs, artists, tags] = await Promise.all([songsPromise, artistsPromise, tagsPromise]); + // For some objects, we want to return linked information as well. + // For that we need to do further queries. + const songIdsPromise = (async () => { + const songs = await songsPromise; + console.log("Found songs:", songs); + const ids = songs.map((song: any) => song['songs.id']); + return ids; + })(); + const songsArtistsPromise: Promise> = (songLimit && songLimit > 0) ? + (async () => { + return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Artist, await songIdsPromise); + })() : + (async () => { return {}; })(); + const songsTagsPromise: Promise> = (songLimit && songLimit > 0) ? + (async () => { + return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); + })() : + (async () => { return {}; })(); + + const [ + songs, + artists, + tags, + songsArtists, + songsTags + ] = + await Promise.all([ + songsPromise, + artistsPromise, + tagsPromise, + songsArtistsPromise, + songsTagsPromise, + ]); const response: api.QueryResponse = { songs: songs.map((song: any) => { return { - songId: song['id'], - title: song['title'], - storeLinks: JSON.parse(song['storeLinks']), - artists: [], //FIXME - tags: [], //FIXME + songId: song['songs.id'], + title: song['songs.title'], + storeLinks: JSON.parse(song['songs.storeLinks']), + artists: songsArtists[song['songs.id']].map((artist: any) => { + return { + artistId: artist['artists.id'], + name: artist['artists.name'], + storeLinks: JSON.parse(artist['artists.storeLinks']), + }; + }), + tags: songsTags[song['songs.id']].map((tag: any) => { + return { + tagId: tag['tags.id'], + name: tag['tags.name'], + }; + }), albums: [], //FIXME } }), artists: artists.map((artist: any) => { return { - artistId: artist['id'], - name: artist['name'], - storeLinks: JSON.parse(artist['storeLinks']), + artistId: artist['artists.id'], + name: artist['artists.name'], + storeLinks: JSON.parse(artist['artists.storeLinks']), } }), tags: tags.map((tag: any) => { return { - tagId: tag['id'], - name: tag['name'], - parentId: tag['parentId'], + tagId: tag['tags.id'], + name: tag['tags.name'], + parentId: tag['tags.parentId'], } }), }