import Knex from "knex"; import { isConstructorDeclaration } from "typescript"; import * as api from '../../client/src/api/api'; import { TagBaseWithRefs, TagWithDetails, TagWithId, TagWithRefs, TagWithRefsWithId } from "../../client/src/api/api"; import { DBError, DBErrorKind } from "../endpoints/types"; export async function getTagChildrenRecursive(id: number, userId: number, trx: any): Promise { const directChildren = (await trx.select('id') .from('tags') .where({ 'user': userId }) .where({ 'parentId': id })).map((r: any) => r.id); const indirectChildrenPromises = directChildren.map( (child: number) => getTagChildrenRecursive(child, userId, trx) ); const indirectChildrenNested = await Promise.all(indirectChildrenPromises); const indirectChildren = indirectChildrenNested.flat(); return [ ...directChildren, ...indirectChildren, ] } // Returns the id of the created tag. export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): Promise { return await knex.transaction(async (trx) => { // If applicable, retrieve the parent tag. const maybeParent: number | null = tag.parentId ? (await trx.select('id') .from('tags') .where({ 'user': userId }) .where({ 'id': tag.parentId }))[0]['id'] : null; // Check if the parent was found, if applicable. if (tag.parentId && maybeParent !== tag.parentId) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all to-be-linked resources were found.', }; throw e; } // Create the new tag. var newTag: any = { name: tag.name, user: userId, }; if (maybeParent) { newTag['parentId'] = maybeParent; } const tagId = (await trx('tags') .insert(newTag) .returning('id') // Needed for Postgres )[0]; return tagId; }) } export async function deleteTag(userId: number, tagId: number, knex: Knex) { await knex.transaction(async (trx) => { // Start retrieving any child tags. const childTagsPromise = getTagChildrenRecursive(tagId, userId, trx); // Start retrieving the tag itself. const tagPromise = trx.select('id') .from('tags') .where({ 'user': userId }) .where({ id: tagId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) // Wait for the requests to finish. var [tag, children] = await Promise.all([tagPromise, childTagsPromise]); // Merge all IDs. const toDelete = [tag, ...children]; // Check that we found all objects we need. if (!tag) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all to-be-linked resources were found.', }; throw e; } // Start deleting artist associations with the tag. const deleteArtistsPromise: Promise = trx.delete() .from('artists_tags') .whereIn('tagId', toDelete); // Start deleting album associations with the tag. const deleteAlbumsPromise: Promise = trx.delete() .from('albums_tags') .whereIn('tagId', toDelete); // Start deleting track associations with the tag. const deleteTracksPromise: Promise = trx.delete() .from('tracks_tags') .whereIn('tagId', toDelete); // Start deleting the tag and its children. const deleteTags: Promise = trx('tags') .where({ 'user': userId }) .whereIn('id', toDelete) .del(); await Promise.all([deleteArtistsPromise, deleteAlbumsPromise, deleteTracksPromise, deleteTags]) }) } export async function getTag(userId: number, tagId: number, knex: Knex): Promise { const tagPromise: Promise = knex.select(['id', 'name', 'parentId']) .from('tags') .where({ 'user': userId }) .where({ 'id': tagId }) .then((r: TagWithRefsWithId[] | undefined) => r ? r[0] : undefined); const parentPromise: Promise = tagPromise .then((r: TagWithRefsWithId | undefined) => (r && r.parentId) ? ( getTag(userId, r.parentId, knex) .then((rr: TagWithDetails | null) => rr ? { ...rr, id: r.parentId || 0 } : null) ) : null ) const [maybeTag, maybeParent] = await Promise.all([tagPromise, parentPromise]); if (maybeTag) { let result: TagWithDetails = { mbApi_typename: "tag", name: maybeTag.name, parent: maybeParent, } return result; } else { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } } export async function modifyTag(userId: number, tagId: number, tag: TagBaseWithRefs, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the parent tag. const parentTagIdPromise: Promise = tag.parentId ? trx.select('id') .from('tags') .where({ 'user': userId }) .where({ 'id': tag.parentId }) .then((ts: any) => ts.map((t: any) => t['tagId'])) : (async () => { return null })(); // Start retrieving the tag itself. const tagPromise = trx.select('id') .from('tags') .where({ 'user': userId }) .where({ id: tagId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) // Wait for the requests to finish. var [dbTag, parent] = await Promise.all([tagPromise, parentTagIdPromise]); // Check that we found all objects we need. if ((tag.parentId && !parent) || !dbTag) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } // Modify the tag. await trx('tags') .where({ 'user': userId }) .where({ 'id': tagId }) .update({ name: tag.name, parentId: tag.parentId || null, }) }) } export async function mergeTag(userId: number, fromId: number, toId: number, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the "from" tag. const fromTagIdPromise = trx.select('id') .from('tags') .where({ 'user': userId }) .where({ id: fromId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) // Start retrieving the "to" tag. const toTagIdPromise = trx.select('id') .from('tags') .where({ 'user': userId }) .where({ id: toId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) // Wait for the requests to finish. var [fromTagId, toTagId] = await Promise.all([fromTagIdPromise, toTagIdPromise]); // Check that we found all objects we need. if (!fromTagId || !toTagId) { const e: DBError = { name: "DBError", kind: DBErrorKind.ResourceNotFound, message: 'Not all resources were found.', }; throw e; } // Assign new tag ID to any objects referencing the to-be-merged tag. const cPromise = trx('tags') .where({ 'user': userId }) .where({ 'parentId': fromId }) .update({ 'parentId': toId }); const sPromise = trx('songs_tags') .where({ 'tagId': fromId }) .update({ 'tagId': toId }); const arPromise = trx('artists_tags') .where({ 'tagId': fromId }) .update({ 'tagId': toId }); const alPromise = trx('albums_tags') .where({ 'tagId': fromId }) .update({ 'tagId': toId }); await Promise.all([sPromise, arPromise, alPromise, cPromise]); // Delete the original tag. await trx('tags') .where({ 'user': userId }) .where({ 'id': fromId }) .del(); }) }