import Knex from "knex"; import { AlbumBaseWithRefs, AlbumWithDetails, AlbumWithRefs } from "../../client/src/api/api"; import * as api from '../../client/src/api/api'; import asJson from "../lib/asJson"; import { DBError, DBErrorKind } from "../endpoints/types"; import { makeNotFoundError } from "./common"; var _ = require('lodash'); // Returns an album with details, or null if not found. export async function getAlbum(id: number, userId: number, knex: Knex): Promise { // Start transfers for tracks, tags and artists. // Also request the album itself. const tagsPromise: Promise = knex.select('tagId') .from('albums_tags') .where({ 'albumId': id }) .then((tags: any) => tags.map((tag: any) => tag['tagId'])) .then((ids: number[]) => knex.select(['id', 'name', 'parentId']) .from('tags') .whereIn('id', ids) ); const tracksPromise: Promise = knex.select('trackId') .from('tracks_albums') .where({ 'albumId': id }) .then((tracks: any) => tracks.map((track: any) => track['trackId'])) .then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) .from('tracks') .whereIn('id', ids) ); const artistsPromise: Promise = knex.select('artistId') .from('artists_albums') .where({ 'albumId': id }) .then((artists: any) => artists.map((artist: any) => artist['artistId'])) .then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) .from('artists') .whereIn('id', ids) ); const albumPromise: Promise = knex.select('name', 'storeLinks') .from('albums') .where({ 'user': userId }) .where({ id: id }) .then((albums: any) => albums[0]); // Wait for the requests to finish. const [album, tags, tracks, artists] = await Promise.all([albumPromise, tagsPromise, tracksPromise, artistsPromise]); if (album) { return { mbApi_typename: 'album', name: album['name'], artists: artists as api.ArtistWithId[], tags: tags as api.TagWithId[], tracks: tracks as api.TrackWithId[], storeLinks: asJson(album['storeLinks'] || []), }; } throw makeNotFoundError(); } // Returns the id of the created album. export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Knex): Promise { return await knex.transaction(async (trx) => { // Start retrieving artists. const artistIdsPromise: Promise = trx.select('id') .from('artists') .where({ 'user': userId }) .whereIn('id', album.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', album.tagIds || []) .then((as: any) => as.map((a: any) => a['id'])); // Start retrieving tracks. const trackIdsPromise: Promise = trx.select('id') .from('tracks') .where({ 'user': userId }) .whereIn('id', album.trackIds || []) .then((as: any) => as.map((a: any) => a['id'])); // Wait for the requests to finish. var [artists, tags, tracks] = await Promise.all([artistIdsPromise, tagIdsPromise, trackIdsPromise]);; // Check that we found all artists and tags we need. if ((!_.isEqual(artists.sort(), (album.artistIds || []).sort())) || (!_.isEqual(tags.sort(), (album.tagIds || []).sort())) || (!_.isEqual(tracks.sort(), (album.trackIds || []).sort()))) { throw makeNotFoundError(); } // Create the album. const albumId = (await trx('albums') .insert({ name: album.name, storeLinks: JSON.stringify(album.storeLinks || []), user: userId, }) .returning('id') // Needed for Postgres )[0]; // Link the artists via the linking table. if (artists && artists.length) { await trx('artists_albums').insert( artists.map((artistId: number) => { return { artistId: artistId, albumId: albumId, } }) ) } // Link the tags via the linking table. if (tags && tags.length) { await trx('albums_tags').insert( tags.map((tagId: number) => { return { albumId: albumId, tagId: tagId, } }) ) } // Link the tracks via the linking table. if (tracks && tracks.length) { await trx('tracks_albums').insert( tracks.map((trackId: number) => { return { albumId: albumId, trackId: trackId, } }) ) } console.log('created album', album, ', ID ', albumId); return albumId; }) } export async function modifyAlbum(userId: number, albumId: number, album: AlbumBaseWithRefs, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the album itself. const albumIdPromise: Promise = trx.select('id') .from('albums') .where({ 'user': userId }) .where({ id: albumId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); // Start retrieving artists if we are modifying those. const artistIdsPromise: Promise = album.artistIds ? trx.select('artistId') .from('artists_albums') .whereIn('artistId', album.artistIds) .then((as: any) => as.map((a: any) => a['artistId'])) : (async () => undefined)(); // Start retrieving tracks if we are modifying those. const trackIdsPromise: Promise = album.trackIds ? trx.select('artistId') .from('tracks_albums') .whereIn('albumId', album.trackIds) .then((as: any) => as.map((a: any) => a['trackId'])) : (async () => undefined)(); // Start retrieving tags if we are modifying those. const tagIdsPromise = album.tagIds ? trx.select('id') .from('albums_tags') .whereIn('tagId', album.tagIds) .then((ts: any) => ts.map((t: any) => t['tagId'])) : (async () => undefined)(); // Wait for the requests to finish. var [oldAlbum, artists, tags, tracks] = await Promise.all([albumIdPromise, artistIdsPromise, tagIdsPromise, trackIdsPromise]);; // Check that we found all objects we need. if ((!artists || !_.isEqual(artists.sort(), (album.artistIds || []).sort())) || (!tags || !_.isEqual(tags.sort(), (album.tagIds || []).sort())) || (!tracks || !_.isEqual(tracks.sort(), (album.trackIds || []).sort())) || !oldAlbum) { throw makeNotFoundError(); } // Modify the album. var update: any = {}; if ("name" in album) { update["name"] = album.name; } if ("storeLinks" in album) { update["storeLinks"] = JSON.stringify(album.storeLinks || []); } const modifyAlbumPromise = trx('albums') .where({ 'user': userId }) .where({ 'id': albumId }) .update(update) // Remove unlinked artists. const removeUnlinkedArtists = artists ? trx('artists_albums') .where({ 'albumId': albumId }) .whereNotIn('artistId', album.artistIds || []) .delete() : undefined; // Remove unlinked tags. const removeUnlinkedTags = tags ? trx('albums_tags') .where({ 'albumId': albumId }) .whereNotIn('tagId', album.tagIds || []) .delete() : undefined; // Remove unlinked tracks. const removeUnlinkedTracks = tracks ? trx('tracks_albums') .where({ 'albumId': albumId }) .whereNotIn('trackId', album.trackIds || []) .delete() : undefined; // Link new artists. const addArtists = artists ? trx('artists_albums') .where({ 'albumId': albumId }) .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, albumId: albumId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('artists_albums').insert(obj) ) ); }) : undefined; // Link new tracks. const addTracks = tracks ? trx('tracks_albums') .where({ 'albumId': albumId }) .then((as: any) => as.map((a: any) => a['trackId'])) .then((doneTrackIds: number[]) => { // Get the set of artists that are not yet linked const toLink = (tracks || []).filter((id: number) => { return !doneTrackIds.includes(id); }); const insertObjects = toLink.map((trackId: number) => { return { trackId: trackId, albumId: albumId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('tracks_albums').insert(obj) ) ); }) : undefined; // Link new tags. const addTags = tags ? trx('albums_tags') .where({ 'albumId': albumId }) .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, albumId: albumId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('albums_tags').insert(obj) ) ); }) : undefined; // Wait for all operations to finish. await Promise.all([ modifyAlbumPromise, removeUnlinkedArtists, removeUnlinkedTags, removeUnlinkedTracks, addArtists, addTags, addTracks, ]); return; }) const e: DBError = { kind: DBErrorKind.Unknown, message: "Reached the unreachable.", name: "DBError" } throw e; } export async function deleteAlbum(userId: number, albumId: number, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start by retrieving the album itself for sanity. const confirmAlbumId: number | undefined = await trx.select('id') .from('albums') .where({ 'user': userId }) .where({ id: albumId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); if (!confirmAlbumId) { throw makeNotFoundError(); } // Start deleting artist associations with the album. const deleteArtistsPromise: Promise = trx.delete() .from('artists_albums') .where({ 'albumId': albumId }); // Start deleting tag associations with the album. const deleteTagsPromise: Promise = trx.delete() .from('albums_tags') .where({ 'albumId': albumId }); // Start deleting track associations with the album. const deleteTracksPromise: Promise = trx.delete() .from('tracks_albums') .where({ 'albumId': albumId }); // Start deleting the album. const deleteAlbumPromise: Promise = trx.delete() .from('albums') .where({ id: albumId }); // Wait for the requests to finish. await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTracksPromise, deleteAlbumPromise]); }) }