From 4471c27b4c396652cb1416adb5c5649705043743 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Tue, 1 Sep 2020 12:23:46 +0200 Subject: [PATCH] Query endpoint is up again, but no filtering yet. --- server/endpoints/QueryEndpointHandler.ts | 151 ++++++++++++++++++++++- server/knex/knex.ts | 4 +- server/knexfile.ts | 2 +- server/server.ts | 13 +- 4 files changed, 156 insertions(+), 14 deletions(-) diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index f0f06a1..5eb4a8f 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -1,10 +1,98 @@ import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +import Knex from 'knex'; -enum QueryType { +enum ObjectType { Song = 0, Artist, Tag, + Album, +} + +// To keep track of which database objects are needed to filter on +// certain properties. +const propertyObjects: Record = { + [api.QueryElemProperty.albumName]: ObjectType.Album, + [api.QueryElemProperty.artistId]: ObjectType.Artist, + [api.QueryElemProperty.artistName]: ObjectType.Artist, + [api.QueryElemProperty.songId]: ObjectType.Song, + [api.QueryElemProperty.songTitle]: ObjectType.Song, +} + +// To keep track of the tables in which objects are stored. +const objectTables: Record = { + [ObjectType.Album]: 'albums', + [ObjectType.Artist]: 'artists', + [ObjectType.Song]: 'songs', + [ObjectType.Tag]: 'tags', +} + +// To keep track of linking tables between objects. +const linkingTables: any = [ + [[ObjectType.Song, ObjectType.Album], 'songs_albums'], + [[ObjectType.Song, ObjectType.Artist], 'songs_artists'], + [[ObjectType.Song, ObjectType.Tag], 'songs_tags'], + [[ObjectType.Artist, ObjectType.Album], 'artists_albums'], + [[ObjectType.Artist, ObjectType.Tag], 'artists_tags'], + [[ObjectType.Album, ObjectType.Tag], 'albums_tags'], +] +function getLinkingTable(a: ObjectType, b: ObjectType): string { + var res: string | undefined = undefined; + linkingTables.forEach((row: any) => { + if (row[0].includes(a) && row[0].includes(b)) { + res = row[1]; + } + }) + if (res) return res; + + throw "Could not find linking table for objects: " + JSON.stringify(a) + ", " + JSON.stringify(b); +} + +// To keep track of ID fields used in linking tables. +const linkingTableIdNames: Record = { + [ObjectType.Album]: 'albumId', + [ObjectType.Artist]: 'artistId', + [ObjectType.Song]: 'songId', + [ObjectType.Tag]: 'tagId', +} + +function getRequiredDatabaseObjects(queryElem: api.QueryElem): Set { + if (queryElem.prop) { + // Leaf node. + return new Set([propertyObjects[queryElem.prop]]); + } else if (queryElem.children) { + // Branch node. + var r = new Set(); + queryElem.children.forEach((child: api.QueryElem) => { + getRequiredDatabaseObjects(child).forEach(object => r.add(object)); + }); + return r; + } + return new Set([]); +} + +function addJoin(knexQuery: any, base: ObjectType, other: ObjectType) { + const linkTable = getLinkingTable(base, other); + const baseTable = objectTables[base]; + const otherTable = objectTables[other]; + return knexQuery + .join(linkTable, { [baseTable + '.id']: linkTable + '.' + linkingTableIdNames[base] }) + .join(otherTable, { [otherTable + '.id']: linkTable + '.' + linkingTableIdNames[other] }); +} + +function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem) { + // First, we create a base query for the type of object we need to yield. + var q = knex.select('*').distinct().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) => { + q = addJoin(q, queryFor, object); + }) + + // TODO: apply filters. + return q; } // const sequelizeOps: any = { @@ -89,7 +177,7 @@ enum QueryType { // ]; // } -export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) => { +export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { if (!api.checkQueryRequest(req.body)) { const e: EndpointError = { internalMessage: 'Invalid Query request: ' + JSON.stringify(req.body), @@ -99,6 +187,63 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) } const reqObject: api.QueryRequest = req.body; + try { + const songLimit = reqObject.offsetsLimits.songLimit; + const songOffset = reqObject.offsetsLimits.songOffset; + const tagLimit = reqObject.offsetsLimits.tagLimit; + const tagOffset = reqObject.offsetsLimits.tagOffset; + const artistLimit = reqObject.offsetsLimits.artistLimit; + const artistOffset = reqObject.offsetsLimits.artistOffset; + + const artistsPromise: Promise = (artistLimit && artistLimit > 0) ? + constructQuery(knex, ObjectType.Artist, reqObject.query) + .offset(artistOffset || 0).limit(artistLimit) : + (async () => [])(); + + const songsPromise: Promise = (songLimit && songLimit > 0) ? + constructQuery(knex, ObjectType.Song, reqObject.query) + .offset(songOffset || 0).limit(songLimit) : + (async () => [])(); + + const tagsPromise: Promise = (tagLimit && tagLimit > 0) ? + constructQuery(knex, ObjectType.Tag, reqObject.query) + .offset(tagOffset || 0).limit(tagLimit) : + (async () => [])(); + + const [songs, artists, tags] = await Promise.all([songsPromise, artistsPromise, tagsPromise]); + + 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 + albums: [], //FIXME + } + }), + artists: artists.map((artist: any) => { + return { + artistId: artist['id'], + name: artist['name'], + storeLinks: JSON.parse(artist['storeLinks']), + } + }), + tags: tags.map((tag: any) => { + return { + tagId: tag['id'], + name: tag['name'], + parentId: tag['parentId'], + } + }), + } + + res.send(response); + } catch (e) { + catchUnhandledErrors(e); + } + // try { // const songLimit = reqObject.offsetsLimits.songLimit; // const songOffset = reqObject.offsetsLimits.songOffset; @@ -188,6 +333,4 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) // } catch (e) { // catchUnhandledErrors(e); // } - - throw "NOTIMPLEMENTED"; } \ No newline at end of file diff --git a/server/knex/knex.ts b/server/knex/knex.ts index a1af280..e7655d8 100644 --- a/server/knex/knex.ts +++ b/server/knex/knex.ts @@ -1,3 +1,3 @@ const environment = process.env.ENVIRONMENT || 'development' -const config = require('../knexfile.js')[environment]; -export default require('knex')(config); \ No newline at end of file +import config from '../knexfile'; +export default require('knex')(config[environment]); \ No newline at end of file diff --git a/server/knexfile.ts b/server/knexfile.ts index ca655ca..7ed1d05 100644 --- a/server/knexfile.ts +++ b/server/knexfile.ts @@ -1,6 +1,6 @@ // Update with your config settings. -export default { +export default > { development: { client: "sqlite3", diff --git a/server/server.ts b/server/server.ts index 4485977..452b0fc 100644 --- a/server/server.ts +++ b/server/server.ts @@ -1,16 +1,15 @@ const express = require('express'); -const environment = process.env.ENVIRONMENT || 'development' -const knexConfig = require('knexfile.js')[environment]; -const knex = require('knex')(knexConfig); - +import knex from './knex/knex'; import { SetupApp } from './app'; const app = express(); -SetupApp(app, knex); +knex.migrate.latest().then(() => { + SetupApp(app, knex); -const port = process.env.PORT || 5000; -app.listen(port, () => console.log(`Listening on port ${port}`)); + const port = process.env.PORT || 5000; + app.listen(port, () => console.log(`Listening on port ${port}`)); +}) export { } \ No newline at end of file