import Knex from "knex"; import { TrackBaseWithRefs, TrackWithDetails, TrackWithRefs } from "../../client/src/api/api"; import * as api from '../../client/src/api/api'; import asJson from "../lib/asJson"; import { DBError, DBErrorKind } from "../endpoints/types"; // Returns an track with details, or null if not found. export async function getTrack(id: number, userId: number, knex: Knex): Promise { // Start transfers for tracks, tags and artists. // Also request the track itself. const tagsPromise: Promise = knex.select('tagId') .from('tracks_tags') .where({ 'trackId': id }) .then((tags: any) => tags.map((tag: any) => tag['tagId'])) .then((ids: number[]) => knex.select(['id', 'name', 'parentId']) .from('tags') .whereIn('id', ids) ); const artistsPromise: Promise = knex.select('artistId') .from('artists_tracks') .where({ 'trackId': id }) .then((artists: any) => artists.map((artist: any) => artist['artistId'])) .then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) .from('artists') .whereIn('id', ids) ); const trackPromise: Promise = knex.select('name', 'storeLinks') .from('tracks') .where({ 'user': userId }) .where({ id: id }) .then((tracks: any) => tracks[0]); const albumPromise: Promise = trackPromise .then((t: api.Track | undefined) => t ? knex.select('id', 'name', 'storeLinks') .from('albums') .where({ 'user': userId }) .where({ id: t.albumId }) .then((albums: any) => albums.length > 0 ? albums[0] : null) : (() => null)() ) // Wait for the requests to finish. const [track, tags, album, artists] = await Promise.all([trackPromise, tagsPromise, albumPromise, artistsPromise]); if (track) { return { mbApi_typename: 'track', name: track['name'], artists: artists as api.ArtistWithId[], tags: tags as api.TagWithId[], album: album as api.AlbumWithId | null, storeLinks: asJson(track['storeLinks'] || []), }; } else { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } } // Returns the id of the created track. export async function createTrack(userId: number, track: TrackWithRefs, knex: Knex): Promise { return await knex.transaction(async (trx) => { try { // Start retrieving artists. const artistIdsPromise: Promise = trx.select('id') .from('artists') .where({ 'user': userId }) .whereIn('id', track.artistIds) .then((as: any) => as.map((a: any) => a['id'])); // Start retrieving tags. const tagIdsPromise: Promise = trx.select('id') .from('tags') .where({ 'user': userId }) .whereIn('id', track.tagIds) .then((as: any) => as.map((a: any) => a['id'])); // Start retrieving album. const albumIdPromise: Promise = knex.select('id') .from('albums') .where({ 'user': userId, 'albumId': track.albumId }) .then((albums: any) => albums.map((album: any) => album['albumId'])) .then((ids: number[]) => ids.length > 0 ? ids[0] : (() => null)() ); // Wait for the requests to finish. var [artists, tags, album] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdPromise]);; // Check that we found all artists and tags we need. if ((new Set((artists as number[]).map((a: any) => a['id'])) !== new Set(track.artistIds)) || (new Set((tags as number[]).map((a: any) => a['id'])) !== new Set(track.tagIds)) || (album === null)) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all to-be-linked resources were found.', }; throw e; } // Create the track. const trackId = (await trx('tracks') .insert({ name: track.name, storeLinks: JSON.stringify(track.storeLinks || []), user: userId, albumId: album, }) .returning('id') // Needed for Postgres )[0]; // Link the artists via the linking table. if (artists && artists.length) { await trx('artists_tracks').insert( artists.map((artistId: number) => { return { artistId: artistId, trackId: trackId, } }) ) } // Link the tags via the linking table. if (tags && tags.length) { await trx('tracks_tags').insert( tags.map((tagId: number) => { return { trackId: trackId, tagId: tagId, } }) ) } return trackId; } catch (e) { trx.rollback(); throw e; } }) } export async function modifyTrack(userId: number, trackId: number, track: TrackBaseWithRefs, knex: Knex): Promise { await knex.transaction(async (trx) => { try { // Start retrieving the track itself. const trackIdPromise: Promise = trx.select('id') .from('tracks') .where({ 'user': userId }) .where({ id: trackId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); // Start retrieving artists if we are modifying those. const artistIdsPromise: Promise = track.artistIds ? trx.select('artistId') .from('artists_tracks') .whereIn('artistId', track.artistIds) .then((as: any) => as.map((a: any) => a['artistId'])) : (async () => undefined)(); // Start retrieving tags if we are modifying those. const tagIdsPromise = track.tagIds ? trx.select('id') .from('tracks_tags') .whereIn('tagId', track.tagIds) .then((ts: any) => ts.map((t: any) => t['tagId'])) : (async () => undefined)(); // Wait for the requests to finish. var [oldTrack, artists, tags] = await Promise.all([trackIdPromise, artistIdsPromise, tagIdsPromise]);; // Check that we found all objects we need. if ((!artists || new Set(artists.map((a: any) => a['id'])) !== new Set(track.artistIds)) || (!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(track.tagIds)) || !oldTrack) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all to-be-linked resources were found.', }; throw e; } // Modify the track. var update: any = {}; if ("name" in track) { update["name"] = track.name; } if ("storeLinks" in track) { update["storeLinks"] = JSON.stringify(track.storeLinks || []); } if ("albumId" in track) { update["albumId"] = track.albumId; } const modifyTrackPromise = trx('tracks') .where({ 'user': userId }) .where({ 'id': trackId }) .update(update) // Remove unlinked artists. const removeUnlinkedArtists = artists ? trx('artists_tracks') .where({ 'trackId': trackId }) .whereNotIn('artistId', track.artistIds || []) .delete() : undefined; // Remove unlinked tags. const removeUnlinkedTags = tags ? trx('tracks_tags') .where({ 'trackId': trackId }) .whereNotIn('tagId', track.tagIds || []) .delete() : undefined; // Link new artists. const addArtists = artists ? trx('artists_tracks') .where({ 'trackId': trackId }) .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, trackId: trackId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('artists_tracks').insert(obj) ) ); }) : undefined; // Link new tags. const addTags = tags ? trx('tracks_tags') .where({ 'trackId': trackId }) .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, trackId: trackId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('tracks_tags').insert(obj) ) ); }) : undefined; // Wait for all operations to finish. await Promise.all([ modifyTrackPromise, removeUnlinkedArtists, removeUnlinkedTags, addArtists, addTags, ]); return; } catch (e) { trx.rollback(); throw e; } }) } export async function deleteTrack(userId: number, trackId: number, knex: Knex): Promise { await knex.transaction(async (trx) => { try { // Start by retrieving the track itself for sanity. const confirmTrackId: number | undefined = await trx.select('id') .from('tracks') .where({ 'user': userId }) .where({ id: trackId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); if (!confirmTrackId) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } // Start deleting artist associations with the track. const deleteArtistsPromise: Promise = trx.delete() .from('artists_tracks') .where({ 'trackId': trackId }); // Start deleting tag associations with the track. const deleteTagsPromise: Promise = trx.delete() .from('tracks_tags') .where({ 'trackId': trackId }); // Start deleting the track. const deleteTrackPromise: Promise = trx.delete() .from('tracks') .where({ id: trackId }); // Wait for the requests to finish. await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTrackPromise]); } catch (e) { trx.rollback(); throw e; } }) }