import Knex from "knex"; import { ArtistBaseWithRefs, ArtistWithDetails, ArtistWithRefs } 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 artist with details, or null if not found. export async function getArtist(id: number, userId: number, knex: Knex): Promise { // Start transfers for tags and albums. // Also request the artist itself. const tagsPromise: Promise = knex.select('tagId') .from('artists_tags') .where({ 'artistId': id }) .then((tags: any) => tags.map((tag: any) => tag['tagId'])) .then((ids: number[]) => knex.select(['id', 'name', 'parentId']) .from('tags') .whereIn('id', ids) ); const albumsPromise: Promise = knex.select('albumId') .from('artists_albums') .where({ 'artistId': id }) .then((albums: any) => albums.map((album: any) => album['albumId'])) .then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) .from('album') .whereIn('id', ids) ); const artistPromise: Promise = knex.select('name', 'storeLinks') .from('artists') .where({ 'user': userId }) .where({ id: id }) .then((artists: any) => artists[0]); // Wait for the requests to finish. const [artist, tags, albums] = await Promise.all([artistPromise, tagsPromise, albumsPromise]); if (artist) { return { mbApi_typename: 'artist', name: artist['name'], albums: albums as api.AlbumWithId[], tags: tags as api.TagWithId[], storeLinks: asJson(artist['storeLinks'] || []), }; } const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } // Returns the id of the created artist. export async function createArtist(userId: number, artist: ArtistWithRefs, knex: Knex): Promise { return await knex.transaction(async (trx) => { try { // Start retrieving albums. const albumIdsPromise: Promise = trx.select('id') .from('albums') .where({ 'user': userId }) .whereIn('id', artist.albumIds) .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', artist.tagIds) .then((as: any) => as.map((a: any) => a['id'])); // Wait for the requests to finish. var [albums, tags] = await Promise.all([albumIdsPromise, tagIdsPromise]);; // Check that we found all artists and tags we need. if ((new Set(albums.map((a: any) => a['id'])) !== new Set(artist.albumIds)) || (new Set(tags.map((a: any) => a['id'])) !== new Set(artist.tagIds))) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all to-be-linked resources were found.', }; throw e; } // Create the artist. const artistId = (await trx('artists') .insert({ name: artist.name, storeLinks: JSON.stringify(artist.storeLinks || []), user: userId, }) .returning('id') // Needed for Postgres )[0]; // Link the albums via the linking table. if (albums && albums.length) { await trx('artists_albums').insert( albums.map((albumId: number) => { return { albumId: albumId, artistId: artistId, } }) ) } // Link the tags via the linking table. if (tags && tags.length) { await trx('artists_tags').insert( tags.map((tagId: number) => { return { artistId: artistId, tagId: tagId, } }) ) } return artistId; } catch (e) { trx.rollback(); throw e; } }) } export async function modifyArtist(userId: number, artistId: number, artist: ArtistBaseWithRefs, knex: Knex): Promise { await knex.transaction(async (trx) => { try { // Start retrieving the artist itself. const artistIdPromise: Promise = trx.select('id') .from('artists') .where({ 'user': userId }) .where({ id: artistId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); // Start retrieving albums if we are modifying those. const albumIdsPromise: Promise = artist.albumIds ? trx.select('albumId') .from('artists_albums') .whereIn('id', artist.albumIds) .then((as: any) => as.map((a: any) => a['albumId'])) : (async () => undefined)(); // Start retrieving tags if we are modifying those. const tagIdsPromise = artist.tagIds ? trx.select('id') .from('artists_tags') .whereIn('id', artist.tagIds) .then((ts: any) => ts.map((t: any) => t['tagId'])) : (async () => undefined)(); // Wait for the requests to finish. var [oldArtist, albums, tags] = await Promise.all([artistIdPromise, albumIdsPromise, tagIdsPromise]);; // Check that we found all objects we need. if ((!albums || new Set(albums.map((a: any) => a['id'])) !== new Set(artist.albumIds)) || (!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(artist.tagIds)) || !oldArtist) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all to-be-linked resources were found.', }; throw e; } // Modify the artist. var update: any = {}; if ("name" in artist) { update["name"] = artist.name; } if ("storeLinks" in artist) { update["storeLinks"] = JSON.stringify(artist.storeLinks || []); } const modifyArtistPromise = trx('artists') .where({ 'user': userId }) .where({ 'id': artistId }) .update(update) // Remove unlinked albums. const removeUnlinkedAlbums = albums ? trx('artists_albums') .where({ 'artistId': artistId }) .whereNotIn('albumId', artist.albumIds || []) .delete() : undefined; // Remove unlinked tags. const removeUnlinkedTags = tags ? trx('artists_tags') .where({ 'artistId': artistId }) .whereNotIn('tagId', artist.tagIds || []) .delete() : undefined; // Link new albums. const addAlbums = albums ? trx('artists_albums') .where({ 'artistId': artistId }) .then((as: any) => as.map((a: any) => a['albumId'])) .then((doneAlbumIds: number[]) => { // Get the set of artists that are not yet linked const toLink = (albums || []).filter((id: number) => { return !doneAlbumIds.includes(id); }); const insertObjects = toLink.map((albumId: number) => { return { artistId: artistId, albumId: albumId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('artists_artists').insert(obj) ) ); }) : undefined; // Link new tags. const addTags = tags ? trx('artists_tags') .where({ 'artistId': artistId }) .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, artistId: artistId, } }) // Link them return Promise.all( insertObjects.map((obj: any) => trx('artists_tags').insert(obj) ) ); }) : undefined; // Wait for all operations to finish. await Promise.all([ modifyArtistPromise, removeUnlinkedAlbums, removeUnlinkedTags, addAlbums, addTags ]); return; } catch (e) { trx.rollback(); throw e; } }) } export async function deleteArtist(userId: number, artistId: number, knex: Knex): Promise { await knex.transaction(async (trx) => { try { // Start by retrieving the artist itself for sanity. const confirmArtistId: number | undefined = await trx.select('id') .from('artists') .where({ 'user': userId }) .where({ id: artistId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); if (!confirmArtistId) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } // Start deleting artist associations with the artist. const deleteAlbumsPromise: Promise = trx.delete() .from('artists_albums') .where({ 'artistId': artistId }); // Start deleting tag associations with the artist. const deleteTagsPromise: Promise = trx.delete() .from('artists_tags') .where({ 'artistId': artistId }); // Start deleting track associations with the artist. const deleteTracksPromise: Promise = trx.delete() .from('tracks_artists') .where({ 'artistId': artistId }); // Start deleting the artist. const deleteArtistPromise: Promise = trx.delete() .from('artists') .where({ id: artistId }); // Wait for the requests to finish. await Promise.all([deleteAlbumsPromise, deleteTagsPromise, deleteTracksPromise, deleteArtistPromise]); } catch (e) { trx.rollback(); throw e; } }) }