import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import Knex from 'knex'; import asJson from '../lib/asJson'; export const PostSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { if (!api.checkCreateSongRequest(req)) { const e: EndpointError = { internalMessage: 'Invalid PostSong request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; } const reqObject: api.CreateSongRequest = req.body; const { id: userId } = req.user; console.log("User ", userId, ": Post Song ", reqObject); await knex.transaction(async (trx) => { try { // Start retrieving artists. const artistIdsPromise = reqObject.artistIds ? trx.select('id') .from('artists') .where({ 'user': userId }) .whereIn('id', reqObject.artistIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); // Start retrieving tags. const tagIdsPromise = reqObject.tagIds ? trx.select('id') .from('tags') .where({ 'user': userId }) .whereIn('id', reqObject.tagIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); // Start retrieving albums. const albumIdsPromise = reqObject.albumIds ? trx.select('id') .from('albums') .where({ 'user': userId }) .whereIn('id', reqObject.albumIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); // Wait for the requests to finish. var [artists, tags, albums] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdsPromise]);; // Check that we found all objects we need. if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || (reqObject.tagIds && tags.length !== reqObject.tagIds.length) || (reqObject.albumIds && albums.length !== reqObject.albumIds.length)) { const e: EndpointError = { internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; } // Create the song. const songId = (await trx('songs') .insert({ title: reqObject.title, storeLinks: JSON.stringify(reqObject.storeLinks || []), user: userId, }) .returning('id') // Needed for Postgres )[0]; // Link the artists via the linking table. if (artists && artists.length) { await Promise.all( artists.map((artistId: number) => { return trx('songs_artists').insert({ artistId: artistId, songId: songId, }) }) ) } // Link the tags via the linking table. if (tags && tags.length) { await Promise.all( tags.map((tagId: number) => { return trx('songs_tags').insert({ songId: songId, tagId: tagId, }) }) ) } // Link the albums via the linking table. if (albums && albums.length) { await Promise.all( albums.map((albumId: number) => { return trx('songs_albums').insert({ songId: songId, albumId: albumId, }) }) ) } // Respond to the request. const responseObject: api.CreateSongResponse = { id: songId }; res.status(200).send(responseObject); } catch (e) { catchUnhandledErrors(e); trx.rollback(); } }) } export const GetSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { if (!api.checkSongDetailsRequest(req)) { const e: EndpointError = { internalMessage: 'Invalid GetSong request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; } const { id: userId } = req.user; try { const tagIdsPromise: Promise = knex.select('tagId') .from('songs_tags') .where({ 'songId': req.params.id }) .then((ts: any) => { return Array.from(new Set( ts.map((tag: any) => tag['tagId']) )); }) const albumIdsPromise: Promise = knex.select('albumId') .from('songs_albums') .where({ 'songId': req.params.id }) .then((as: any) => { return Array.from(new Set( as.map((album: any) => album['albumId']) )); }) const artistIdsPromise: Promise = knex.select('artistId') .from('songs_artists') .where({ 'songId': req.params.id }) .then((as: any) => { return Array.from(new Set( as.map((artist: any) => artist['artistId']) )); }) const songPromise = await knex.select(['id', 'title', 'storeLinks']) .from('songs') .where({ 'user': userId }) .where({ 'id': req.params.id }) .then((ss: any) => ss[0]) const [tags, albums, artists, song] = await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]); if (song) { const response: api.SongDetailsResponse = { title: song.title, tagIds: tags, artistIds: artists, albumIds: albums, storeLinks: asJson(song.storeLinks), } await res.send(response); } else { await res.status(404).send({}); } } catch (e) { catchUnhandledErrors(e) } } export const PutSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { if (!api.checkModifySongRequest(req)) { const e: EndpointError = { internalMessage: 'Invalid PutSong request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; } const reqObject: api.ModifySongRequest = req.body; const { id: userId } = req.user; console.log("User ", userId, ": Put Song ", reqObject); await knex.transaction(async (trx) => { try { // Retrieve the song to be modified itself. const songPromise = trx.select('id') .from('songs') .where({ 'user': userId }) .where({ id: req.params.id }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) // Start retrieving artists. const artistIdsPromise = reqObject.artistIds ? trx.select('artistId') .from('songs_artists') .whereIn('id', reqObject.artistIds) .then((as: any) => as.map((a: any) => a['artistId'])) : (async () => { return undefined })(); // Start retrieving tags. const tagIdsPromise = reqObject.tagIds ? trx.select('id') .from('songs_tags') .whereIn('id', reqObject.tagIds) .then((ts: any) => ts.map((t: any) => t['tagId'])) : (async () => { return undefined })(); // Start retrieving albums. const albumIdsPromise = reqObject.albumIds ? trx.select('id') .from('songs_albums') .whereIn('id', reqObject.albumIds) .then((as: any) => as.map((a: any) => a['albumId'])) : (async () => { return undefined })(); // Wait for the requests to finish. var [song, artists, tags, albums] = await Promise.all([songPromise, artistIdsPromise, tagIdsPromise, albumIdsPromise]);; // Check that we found all objects we need. if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || (reqObject.tagIds && tags.length !== reqObject.tagIds.length) || (reqObject.albumIds && albums.length !== reqObject.albumIds.length) || !song) { const e: EndpointError = { internalMessage: 'Not all albums and/or artists and/or tags exist for ModifySong request: ' + JSON.stringify(req.body), httpStatus: 400 }; throw e; } // Modify the song. var update: any = {}; if ("title" in reqObject) { update["title"] = reqObject.title; } if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } const modifySongPromise = trx('songs') .where({ 'user': userId }) .where({ 'id': req.params.id }) .update(update) // Remove unlinked artists. // TODO: test this! const removeUnlinkedArtists = artists ? trx('songs_artists') .where({ 'songId': req.params.id }) .whereNotIn('artistId', reqObject.artistIds || []) .delete() : undefined; // Remove unlinked tags. // TODO: test this! const removeUnlinkedTags = tags ? trx('songs_tags') .where({ 'songId': req.params.id }) .whereNotIn('tagId', reqObject.tagIds || []) .delete() : undefined; // Remove unlinked albums. // TODO: test this! const removeUnlinkedAlbums = albums ? trx('songs_albums') .where({ 'songId': req.params.id }) .whereNotIn('albumId', reqObject.albumIds || []) .delete() : undefined; // Link new artists. // TODO: test this! const addArtists = artists ? trx('songs_artists') .where({ 'songId': req.params.id }) .then((as: any) => as.map((a: any) => a['artistId'])) .then((doneArtistIds: number[]) => { // Get the set of artists that are not yet linked const toLink = artists.filter((id: number) => { return !doneArtistIds.includes(id); }); const insertObjects = toLink.map((artistId: number) => { return { artistId: artistId, songId: req.params.id, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('songs_artists').insert(obj) ) ); }) : undefined; // Link new tags. // TODO: test this! const addTags = tags ? trx('songs_tags') .where({ 'songId': req.params.id }) .then((ts: any) => ts.map((t: any) => t['tagId'])) .then((doneTagIds: number[]) => { // Get the set of tags that are not yet linked const toLink = tags.filter((id: number) => { return !doneTagIds.includes(id); }); const insertObjects = toLink.map((tagId: number) => { return { tagId: tagId, songId: req.params.id, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('songs_tags').insert(obj) ) ); }) : undefined; // Link new albums. // TODO: test this! const addAlbums = albums ? trx('songs_albums') .where({ 'albumId': req.params.id }) .then((as: any) => as.map((a: any) => a['albumId'])) .then((doneAlbumIds: number[]) => { // Get the set of albums that are not yet linked const toLink = albums.filter((id: number) => { return !doneAlbumIds.includes(id); }); const insertObjects = toLink.map((albumId: number) => { return { albumId: albumId, songId: req.params.id, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('songs_albums').insert(obj) ) ); }) : undefined; // Wait for all operations to finish. await Promise.all([ modifySongPromise, removeUnlinkedArtists, removeUnlinkedTags, removeUnlinkedAlbums, addArtists, addTags, addAlbums, ]); // Respond to the request. res.status(200).send(); } catch (e) { catchUnhandledErrors(e); trx.rollback(); } }) }