You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

359 lines
14 KiB

import Knex from "knex";
import { Album, AlbumRefs, Id, Name, AlbumDetails, StoreLinks, Tag, TagParentId, Track, Artist } 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";
import { transform } from "typescript";
var _ = require('lodash');
// Returns an album with details, or null if not found.
export async function getAlbum(id: number, userId: number, knex: Knex):
Promise<(Album & AlbumDetails & StoreLinks & Name)> {
// Start transfers for tracks, tags and artists.
// Also request the album itself.
const tagsPromise: Promise<(Tag & Id & Name & TagParentId)[]> =
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)
.then((tags: (Id & Name & TagParentId)[]) =>
tags.map((tag : (Id & Name & TagParentId)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
);
const tracksPromise: Promise<(Track & Id)[]> =
knex.select(['id', 'name', 'storeLinks'])
.from('tracks')
.where({ 'album': id })
.then((tracks: any) => tracks.map((track: any) => {
return { id: track['id'], mbApi_typename: "track" }
}))
const artistsPromise: Promise<(Artist & Id & Name & StoreLinks)[]> =
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)
.then((artists: (Id & Name & StoreLinks)[]) =>
artists.map((artist : (Id & Name & StoreLinks)) =>
{ return {...artist, mbApi_typename: "artist"}}
))
);
const albumPromise: Promise<(Album & Name & StoreLinks) | undefined> =
knex.select('name', 'storeLinks')
.from('albums')
.where({ 'user': userId })
.where({ id: id })
.then((albums: any) => { return { ...albums[0], mbApi_typename: 'album' }});
// 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 || [],
tags: tags || [],
tracks: tracks || [],
storeLinks: asJson(album['storeLinks'] || []),
};
}
throw makeNotFoundError();
}
// Returns the id of the created album.
export async function createAlbum(userId: number, album: (Album & Name & AlbumRefs), knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => {
// Start retrieving artists.
const artistIdsPromise: Promise<number[]> =
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<number[]> =
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<number[]> =
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 direct links.
if (tracks && tracks.length) {
await trx('tracks')
.update({ album: albumId })
.whereIn('id', tracks);
}
console.log('created album', album, ', ID ', albumId);
return albumId;
})
}
export async function modifyAlbum(userId: number, albumId: number, album: Album, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => {
// Start retrieving the album itself.
const albumIdPromise: Promise<number | undefined> =
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<number[] | undefined> =
album.artistIds ?
trx.select('artistId')
.from('artists')
.whereIn('id', album.artistIds)
.then((as: any) => as.map((a: any) => a['id']))
: (async () => undefined)();
// Start retrieving tracks if we are modifying those.
const trackIdsPromise: Promise<number[] | undefined> =
album.trackIds ?
trx.select('id')
.from('tracks')
.whereIn('album', album.trackIds)
.then((as: any) => as.map((a: any) => a['id']))
: (async () => undefined)();
// Start retrieving tags if we are modifying those.
const tagIdsPromise =
album.tagIds ?
trx.select('id')
.from('tags')
.whereIn('id', album.tagIds)
.then((ts: any) => ts.map((t: any) => t['id'])) :
(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 ((album.artistIds && (!artists || !_.isEqual(artists.sort(), (album.artistIds || []).sort()))) ||
(album.tagIds && (!tags || !_.isEqual(tags.sort(), (album.tagIds || []).sort()))) ||
(album.trackIds && (!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 by setting their references to null.
const removeUnlinkedTracks = tracks ? trx('tracks')
.where({ 'album': albumId })
.whereNotIn('id', album.trackIds || [])
.update({ 'album': null }) : 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')
.where({ 'album': albumId })
.then((as: any) => as.map((a: any) => a['id']))
.then((doneTrackIds: number[]) => {
// Get the set of tracks that are not yet linked
const toLink = (tracks || []).filter((id: number) => {
return !doneTrackIds.includes(id);
});
// Link them
return trx('tracks')
.update({ album: albumId })
.whereIn('id', toLink);
}) : 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;
})
}
export async function deleteAlbum(userId: number, albumId: number, knex: Knex): Promise<void> {
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<any> =
trx.delete()
.from('artists_albums')
.where({ 'albumId': albumId });
// Start deleting tag associations with the album.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('albums_tags')
.where({ 'albumId': albumId });
// Start deleting track associations with the album by setting their references to null.
const deleteTracksPromise: Promise<any> =
trx.update({ 'album': null })
.from('tracks')
.where({ 'album': albumId });
// Start deleting the album.
const deleteAlbumPromise: Promise<any> =
trx.delete()
.from('albums')
.where({ id: albumId });
// Wait for the requests to finish.
await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTracksPromise, deleteAlbumPromise]);
})
}