From 40e3ba22031c7980a9000818abcc1bb75873d0fa Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Thu, 17 Dec 2020 01:19:13 +0100 Subject: [PATCH] Stuck in some recursivity problems. --- server/db/Album.ts | 63 ++--- server/endpoints/Album.ts | 4 +- server/endpoints/Tag.ts | 2 +- server/endpoints/Track.ts | 4 +- server/test/integration/flows/ResourceFlow.ts | 132 ++++++++++- server/test/integration/helpers.ts | 71 +++++- .../test/reference_model/DBReferenceModel.ts | 192 +++++++++++++--- server/test/reference_model/randomGen.ts | 217 +++++++++++++++++- 8 files changed, 584 insertions(+), 101 deletions(-) diff --git a/server/db/Album.ts b/server/db/Album.ts index 012d1ba..008e691 100644 --- a/server/db/Album.ts +++ b/server/db/Album.ts @@ -4,6 +4,7 @@ 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. @@ -25,15 +26,10 @@ export async function getAlbum(id: number, userId: number, knex: Knex): ); 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) - ); + knex.select(['id', 'name', 'storeLinks']) + .from('tracks') + .where({ 'album': id }) + .then((tracks: any) => tracks.map((track: any) => track['id'])) const artistsPromise: Promise = knex.select('artistId') @@ -142,16 +138,11 @@ export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Kn ) } - // Link the tracks via the linking table. + // Link the tracks via direct links. if (tracks && tracks.length) { - await trx('tracks_albums').insert( - tracks.map((trackId: number) => { - return { - albumId: albumId, - trackId: trackId, - } - }) - ) + await trx('tracks') + .update({ album: albumId }) + .whereIn('id', tracks); } console.log('created album', album, ', ID ', albumId); @@ -183,7 +174,7 @@ export async function modifyAlbum(userId: number, albumId: number, album: AlbumB album.trackIds ? trx.select('id') .from('tracks') - .whereIn('albumId', album.trackIds) + .whereIn('album', album.trackIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => undefined)(); @@ -231,9 +222,9 @@ export async function modifyAlbum(userId: number, albumId: number, album: AlbumB // Remove unlinked tracks by setting their references to null. const removeUnlinkedTracks = tracks ? trx('tracks') - .where({ 'albumId': albumId }) + .where({ 'album': albumId }) .whereNotIn('id', album.trackIds || []) - .update({ 'albumId': null }) : undefined; + .update({ 'album': null }) : undefined; // Link new artists. const addArtists = artists ? trx('artists_albums') @@ -260,27 +251,18 @@ export async function modifyAlbum(userId: number, albumId: number, album: AlbumB }) : undefined; // Link new tracks. - const addTracks = tracks ? trx('tracks_albums') - .where({ 'albumId': albumId }) - .then((as: any) => as.map((a: any) => a['trackId'])) + const addTracks = tracks ? trx('tracks') + .where({ 'album': albumId }) + .then((as: any) => as.map((a: any) => a['id'])) .then((doneTrackIds: number[]) => { - // Get the set of artists that are not yet linked + // Get the set of tracks 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) - ) - ); + return trx('tracks') + .update({ album: albumId }) + .whereIn('id', toLink); }) : undefined; // Link new tags. @@ -320,13 +302,6 @@ export async function modifyAlbum(userId: number, albumId: number, album: AlbumB 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 { diff --git a/server/endpoints/Album.ts b/server/endpoints/Album.ts index 7ef0329..8113390 100644 --- a/server/endpoints/Album.ts +++ b/server/endpoints/Album.ts @@ -61,7 +61,7 @@ export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) console.log("User ", userId, ": Put Album ", reqObject); try { - modifyAlbum(userId, req.params.id, reqObject, knex); + await modifyAlbum(userId, req.params.id, reqObject, knex); res.status(200).send(); } catch (e) { handleErrorsInEndpoint(e); @@ -83,7 +83,7 @@ export const PatchAlbum: EndpointHandler = async (req: any, res: any, knex: Knex console.log("User ", userId, ": Patch Album ", reqObject); try { - modifyAlbum(userId, req.params.id, reqObject, knex); + await modifyAlbum(userId, req.params.id, reqObject, knex); res.status(200).send(); } catch (e) { handleErrorsInEndpoint(e); diff --git a/server/endpoints/Tag.ts b/server/endpoints/Tag.ts index a44ab6c..2201c6a 100644 --- a/server/endpoints/Tag.ts +++ b/server/endpoints/Tag.ts @@ -107,7 +107,7 @@ export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) const toId = req.params.toId; try { - mergeTag(userId, fromId, toId, knex); + await mergeTag(userId, fromId, toId, knex); res.status(200).send(); } catch (e) { diff --git a/server/endpoints/Track.ts b/server/endpoints/Track.ts index c9a8a6e..2246621 100644 --- a/server/endpoints/Track.ts +++ b/server/endpoints/Track.ts @@ -55,7 +55,7 @@ export const PutTrack: EndpointHandler = async (req: any, res: any, knex: Knex) console.log("User ", userId, ": Put Track ", reqObject); try { - modifyTrack(userId, req.params.id, reqObject, knex); + await modifyTrack(userId, req.params.id, reqObject, knex); res.status(200).send(); } catch (e) { handleErrorsInEndpoint(e); @@ -77,7 +77,7 @@ export const PatchTrack: EndpointHandler = async (req: any, res: any, knex: Knex console.log("User ", userId, ": Patch Track ", reqObject); try { - modifyTrack(userId, req.params.id, reqObject, knex); + await modifyTrack(userId, req.params.id, reqObject, knex); res.status(200).send(); } catch (e) { handleErrorsInEndpoint(e); diff --git a/server/test/integration/flows/ResourceFlow.ts b/server/test/integration/flows/ResourceFlow.ts index 367544e..069653d 100644 --- a/server/test/integration/flows/ResourceFlow.ts +++ b/server/test/integration/flows/ResourceFlow.ts @@ -218,14 +218,22 @@ describe('Randomized model-based DB back-end tests', () => { // be generated. let dist: RandomDBActionDistribution = { type: new Map([ - [DBActionType.CreateTrack, 0.125], - [DBActionType.CreateArtist, 0.125], - [DBActionType.CreateAlbum, 0.125], - [DBActionType.CreateTag, 0.125], - [DBActionType.DeleteTrack, 0.125], - [DBActionType.DeleteArtist, 0.125], - [DBActionType.DeleteAlbum, 0.125], - [DBActionType.DeleteTag, 0.125], + [DBActionType.CreateTrack, 0.0625], + [DBActionType.CreateArtist, 0.0625], + [DBActionType.CreateAlbum, 0.0625], + [DBActionType.CreateTag, 0.0625], + [DBActionType.PutTrack, 0.0625], + [DBActionType.PutArtist, 0.0625], + [DBActionType.PutAlbum, 0.0625], + [DBActionType.PutTag, 0.0625], + [DBActionType.PatchTrack, 0.0625], + [DBActionType.PatchArtist, 0.0625], + [DBActionType.PatchAlbum, 0.0625], + [DBActionType.PatchTag, 0.0625], + [DBActionType.DeleteTrack, 0.0625], + [DBActionType.DeleteArtist, 0.0625], + [DBActionType.DeleteAlbum, 0.0625], + [DBActionType.DeleteTag, 0.0625], ]), userId: new Map([[1, 1.0]]), createTrackParams: { @@ -271,6 +279,112 @@ describe('Randomized model-based DB back-end tests', () => { linkParent: new Map([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), }, + putTrackParams: { + linkAlbum: new Map([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkArtists: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + putAlbumParams: { + linkTracks: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkArtists: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + putArtistParams: { + linkTracks: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkAlbums: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + putTagParams: { + linkParent: new Map([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + patchTrackParams: { + linkAlbum: new Map([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkArtists: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + replaceName: new Map([[false, 0.5], [true, 0.5]]), + replaceTags: new Map([[false, 0.5], [true, 0.5]]), + replaceAlbum: new Map([[false, 0.5], [true, 0.5]]), + replaceArtists: new Map([[false, 0.5], [true, 0.5]]), + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + patchAlbumParams: { + linkTracks: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkArtists: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + replaceName: new Map([[false, 0.5], [true, 0.5]]), + replaceTags: new Map([[false, 0.5], [true, 0.5]]), + replaceTracks: new Map([[false, 0.5], [true, 0.5]]), + replaceArtists: new Map([[false, 0.5], [true, 0.5]]), + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + patchArtistParams: { + linkTracks: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkAlbums: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + replaceName: new Map([[false, 0.5], [true, 0.5]]), + replaceTags: new Map([[false, 0.5], [true, 0.5]]), + replaceTracks: new Map([[false, 0.5], [true, 0.5]]), + replaceAlbums: new Map([[false, 0.5], [true, 0.5]]), + validId: new Map([[false, 0.3], [true, 0.7]]), + }, + patchTagParams: { + linkParent: new Map([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), + replaceName: new Map([[false, 0.5], [true, 0.5]]), + replaceParent: new Map([[false, 0.5], [true, 0.5]]), + validId: new Map([[false, 0.3], [true, 0.7]]), + }, deleteTrackParams: { validId: new Map([[false, 0.2], [true, 0.8]]) }, @@ -306,7 +420,7 @@ describe('Randomized model-based DB back-end tests', () => { // If this was an object creation action, we need to update the mappings. if (refStatus === 200 && realStatus === 200) { - switch(refAction.type) { + switch (refAction.type) { case DBActionType.CreateTrack: { idMappingsRefToReal.tracks[refResponse.id] = realResponse.id; break; } case DBActionType.CreateArtist: { idMappingsRefToReal.artists[refResponse.id] = realResponse.id; break; } case DBActionType.CreateAlbum: { idMappingsRefToReal.albums[refResponse.id] = realResponse.id; break; } diff --git a/server/test/integration/helpers.ts b/server/test/integration/helpers.ts index 8813dbc..a0d3ae2 100644 --- a/server/test/integration/helpers.ts +++ b/server/test/integration/helpers.ts @@ -38,7 +38,7 @@ export async function createTrack( }); } -export async function modifyTrack( +export async function putTrack( req: any, id = 1, props = { name: "NewTrack" }, @@ -53,6 +53,21 @@ export async function modifyTrack( }); } +export async function patchTrack( + req: any, + id = 1, + props = { name: "NewTrack" }, + expectStatus: number | undefined = undefined, +) { + return await req + .patch('/track/' + id) + .send(props) + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + return res; + }); +} + export async function deleteTrack( req: any, id = 1, @@ -98,7 +113,7 @@ export async function createArtist( }); } -export async function modifyArtist( +export async function putArtist( req: any, id = 1, props = { name: "NewArtist" }, @@ -113,6 +128,21 @@ export async function modifyArtist( }); } +export async function patchArtist( + req: any, + id = 1, + props = { name: "NewArtist" }, + expectStatus: number | undefined = undefined, +) { + return await req + .patch('/artist/' + id) + .send(props) + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + return res; + }); +} + export async function checkArtist( req: any, id: any, @@ -158,7 +188,7 @@ export async function createTag( }); } -export async function modifyTag( +export async function putTag( req: any, id = 1, props = { name: "NewTag" }, @@ -173,6 +203,22 @@ export async function modifyTag( }); } +export async function patchTag( + req: any, + id = 1, + props = { name: "NewTag" }, + expectStatus: number | undefined = undefined, +) { + return await req + .patch('/tag/' + id) + .send(props) + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + return res; + }); +} + + export async function checkTag( req: any, id: any, @@ -218,7 +264,7 @@ export async function createAlbum( }); } -export async function modifyAlbum( +export async function putAlbum( req: any, id = 1, props = { name: "NewAlbum" }, @@ -233,6 +279,21 @@ export async function modifyAlbum( }); } +export async function patchAlbum( + req: any, + id = 1, + props = { name: "NewAlbum" }, + expectStatus: number | undefined = undefined, +) { + return await req + .patch('/album/' + id) + .send(props) + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + return res; + }); +} + export async function checkAlbum( req: any, id: any, @@ -324,7 +385,7 @@ export async function createIntegration( }); } -export async function modifyIntegration( +export async function putIntegration( req: any, id = 1, props = { name: "NewIntegration", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, diff --git a/server/test/reference_model/DBReferenceModel.ts b/server/test/reference_model/DBReferenceModel.ts index 8786f31..a53a5db 100644 --- a/server/test/reference_model/DBReferenceModel.ts +++ b/server/test/reference_model/DBReferenceModel.ts @@ -1,6 +1,7 @@ -import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostAlbumRequest, PostArtistRequest, PostTagRequest, PostTrackRequest, TagWithRefsWithId, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api"; +import { AlbumBaseWithRefs, AlbumWithRefsWithId, ArtistBaseWithRefs, ArtistWithRefsWithId, DBDataFormat, PostAlbumRequest, PostArtistRequest, PostTagRequest, PostTrackRequest, TagBaseWithRefs, TagWithRefsWithId, TrackBaseWithRefs, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api"; import { makeNotFoundError } from "../../db/common"; import filterInPlace from "../../lib/filterInPlace"; +let _ = require('lodash'); // The mock reference database is in the same format as // the JSON import/export format, for multiple users. @@ -33,12 +34,40 @@ function ensureInSet(n: number, s: number[]) { // For a set of objects, ensure they point to another object. function ensureLinked(fromObjects: number[], fromObjectsType: ObjectsType, - toId: number, toObjectsType: ObjectsType, data: DBDataFormat) { + toId: number, toObjectsType: ObjectsType, exact: boolean, data: DBDataFormat) { + if (toObjectsType === 'tracks') { - fromObjects.forEach((fromId: number) => { - let fromObject = (data[fromObjectsType] as any).find((o: any) => o.id === fromId); - ensureInSet(toId, (fromObject as AlbumWithRefsWithId | ArtistWithRefsWithId).trackIds) - }) + (data[fromObjectsType] as any).forEach((fromObject: AlbumWithRefsWithId | ArtistWithRefsWithId) => { + if (fromObjects.includes(fromObject.id)) { ensureInSet(toId, fromObject.trackIds); } + else if (exact) { fromObject.trackIds = fromObject.trackIds.filter((id: number) => id !== toId) } + }); + } else if (toObjectsType === 'artists') { + (data[fromObjectsType] as any).forEach((fromObject: AlbumWithRefsWithId | TrackWithRefsWithId) => { + if (fromObjects.includes(fromObject.id)) { ensureInSet(toId, fromObject.artistIds); } + else if (exact) { + fromObject.artistIds = fromObject.artistIds.filter((id: number) => id !== toId) + } + }); + } else if (toObjectsType === 'albums' && fromObjectsType === 'artists') { + (data[fromObjectsType] as any).forEach((fromObject: ArtistWithRefsWithId) => { + if (fromObjects.includes(fromObject.id)) { ensureInSet(toId, fromObject.albumIds); } + else if (exact) { fromObject.albumIds = fromObject.albumIds.filter((id: number) => id !== toId) } + }); + } else if (toObjectsType === 'albums' && fromObjectsType === 'tracks') { + (data[fromObjectsType] as any).forEach((fromObject: TrackWithRefsWithId) => { + if (fromObjects.includes(fromObject.id)) { fromObject.albumId = toId; } + else if (exact && fromObject.albumId === toId) { fromObject.albumId = null; } + }); + } else if (toObjectsType === 'tags' && fromObjectsType === 'tags') { + (data[fromObjectsType] as any).forEach((fromObject: TagWithRefsWithId) => { + if (fromObjects.includes(fromObject.id)) { fromObject.parentId = toId; } + else if (exact && fromObject.parentId === toId) { fromObject.parentId = null; } + }); + } else if (toObjectsType === 'tags') { + (data[fromObjectsType] as any).forEach((fromObject: AlbumWithRefsWithId | TrackWithRefsWithId | ArtistWithRefsWithId) => { + if (fromObjects.includes(fromObject.id)) { ensureInSet(toId, fromObject.tagIds); } + else if (exact) { fromObject.tagIds = fromObject.tagIds.filter((id: number) => id !== toId) } + }); } } @@ -47,7 +76,7 @@ function ensureLinked(fromObjects: number[], fromObjectsType: ObjectsType, // - check that any existing objects referenced in the new object actually exist // - generate a new ID and insert the object // - add reverse references into any existing object referenced by the new object. -export interface ReferencingField { field: string, otherObjectType: ObjectsType }; +export interface ReferencingField { field: string, otherObjectType: ObjectsType, doReverseReference: boolean }; export function createObject( userId: number, object: any, @@ -78,10 +107,12 @@ export function createObject( // reverse links singularReverseRefs.forEach((f: ReferencingField) => { - ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, db[userId]); + f.doReverseReference && + ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, true, db[userId]); }); pluralReverseRefs.forEach((f: ReferencingField) => { - ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, db[userId]); + f.doReverseReference && + ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, true, db[userId]); }); return { id: id }; @@ -112,26 +143,73 @@ export function deleteObject( // Remove references to this object pluralRefsToThisObject.forEach((f: ReferencingField) => { - db[userId][f.otherObjectType].forEach((other: any) => { filterInPlace(other[f.field], (oid: number) => oid !== objectId) }) + f.doReverseReference && + db[userId][f.otherObjectType].forEach((other: any) => { filterInPlace(other[f.field], (oid: number) => oid !== objectId) }) }); singularRefsToThisObject.forEach((f: ReferencingField) => { - db[userId][f.otherObjectType].forEach((other: any) => { if (other[f.field] === objectId) { other[f.field] = null; } }) + f.doReverseReference && + db[userId][f.otherObjectType].forEach((other: any) => { if (other[f.field] === objectId) { other[f.field] = null; } }) }); // Delete the object db[userId][objectType].splice(idx, 1); } +// Modify an existing object. +// This can be a complete replacement or a partial change. +export function modifyObject( + userId: number, + objectId: number, + objectUpdates: any, + objectType: ObjectsType, + singularReverseRefs: ReferencingField[], + pluralReverseRefs: ReferencingField[], + db: ReferenceDatabase +): void { + // Existence checks + if (!(userId in db)) { + throw makeNotFoundError() + } + let object = (db[userId][objectType] as any[]).find((o: any) => 'id' in o && o.id === objectId); + if (!object) { + // Not found + throw makeNotFoundError(); + } + singularReverseRefs.forEach((f: ReferencingField) => { + if (f.field in objectUpdates && !checkExists(db[userId][f.otherObjectType], objectUpdates[f.field] ? [objectUpdates[f.field]] : [])) { + throw makeNotFoundError(); + } + }); + pluralReverseRefs.forEach((f: ReferencingField) => { + if (f.field in objectUpdates && !checkExists(db[userId][f.otherObjectType], objectUpdates[f.field] || [])) { + throw makeNotFoundError(); + } + }); + + // Update the object + _.extend(object, objectUpdates); + + // reverse links + singularReverseRefs.forEach((f: ReferencingField) => { + f.doReverseReference && + ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, objectId, objectType, true, db[userId]); + }); + pluralReverseRefs.forEach((f: ReferencingField) => { + f.doReverseReference && + ensureLinked(object[f.field] || [], f.otherObjectType, objectId, objectType, true, db[userId]); + }); +} + // Create a new track. export function createTrack(userId: number, track: PostTrackRequest, db: ReferenceDatabase): { id: number } { return createObject( userId, track, 'tracks', - [{ field: 'albumId', otherObjectType: 'albums' }], + [{ field: 'albumId', otherObjectType: 'albums', doReverseReference: true }], [ - { field: 'artistIds', otherObjectType: 'artists' }, - { field: 'tagIds', otherObjectType: 'tags' }, + { field: 'artistIds', otherObjectType: 'artists', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tags', doReverseReference: false }, ], db ); @@ -145,9 +223,9 @@ export function createAlbum(userId: number, album: PostAlbumRequest, db: Referen 'albums', [], [ - { field: 'artistIds', otherObjectType: 'artists' }, - { field: 'trackIds', otherObjectType: 'tracks' }, - { field: 'tagIds', otherObjectType: 'tags' }, + { field: 'artistIds', otherObjectType: 'artists', doReverseReference: true }, + { field: 'trackIds', otherObjectType: 'tracks', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tags', doReverseReference: false }, ], db ); @@ -161,9 +239,9 @@ export function createArtist(userId: number, artist: PostArtistRequest, db: Refe 'artists', [], [ - { field: 'albumIds', otherObjectType: 'albums' }, - { field: 'trackIds', otherObjectType: 'tracks' }, - { field: 'tagIds', otherObjectType: 'tags' }, + { field: 'albumIds', otherObjectType: 'albums', doReverseReference: true }, + { field: 'trackIds', otherObjectType: 'tracks', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tags', doReverseReference: false }, ], db ); @@ -175,7 +253,7 @@ export function createTag(userId: number, tag: PostTagRequest, db: ReferenceData userId, tag, 'tags', - [{ field: 'parentId', otherObjectType: 'tags' }], + [{ field: 'parentId', otherObjectType: 'tags', doReverseReference: false },], [], db ); @@ -189,8 +267,8 @@ export function deleteTrack(userId: number, id: number, db: ReferenceDatabase): 'tracks', [], [ - { field: 'trackIds', otherObjectType: 'albums' }, - { field: 'trackIds', otherObjectType: 'artists' }, + { field: 'trackIds', otherObjectType: 'albums', doReverseReference: true }, + { field: 'trackIds', otherObjectType: 'artists', doReverseReference: true }, ], db ); @@ -204,8 +282,8 @@ export function deleteArtist(userId: number, id: number, db: ReferenceDatabase): 'artists', [], [ - { field: 'artistIds', otherObjectType: 'tracks' }, - { field: 'artistIds', otherObjectType: 'albums' }, + { field: 'artistIds', otherObjectType: 'tracks', doReverseReference: true }, + { field: 'artistIds', otherObjectType: 'albums', doReverseReference: true }, ], db ); @@ -217,23 +295,30 @@ export function deleteAlbum(userId: number, id: number, db: ReferenceDatabase): userId, id, 'albums', - [{ field: 'albumId', otherObjectType: 'tracks' }], - [{ field: 'albumIds', otherObjectType: 'artists' },], + [{ field: 'albumId', otherObjectType: 'tracks', doReverseReference: true }], + [{ field: 'albumIds', otherObjectType: 'artists', doReverseReference: true },], db ); } // Delete a tag. -export function deleteTag(userId: number, id: number, db: ReferenceDatabase): void { +export function deleteTag(userId: number, id: number, db: ReferenceDatabase, recursiveIdsSoFar: number[] = []): void { // Tags are special in that deleting them also deletes their children. Implement that here // with recursive calls. + let _recursiveIdsSoFar = [...recursiveIdsSoFar, id] + if (!(userId in db)) { throw makeNotFoundError() } let tag = db[userId].tags.find((o: any) => 'id' in o && o.id === id); if (!tag) { throw makeNotFoundError(); } let children = db[userId].tags.filter((t: TagWithRefsWithId) => t.parentId === id); - children.forEach((child: TagWithRefsWithId) => { deleteTag(userId, child.id, db) }) + children.forEach((child: TagWithRefsWithId) => { + // Prevent cyclic dependencies. + if (!_recursiveIdsSoFar.includes(child.id)) { + deleteTag(userId, child.id, db, _recursiveIdsSoFar) + } + }) // Do the actual deletion of this tag. return deleteObject( @@ -242,10 +327,53 @@ export function deleteTag(userId: number, id: number, db: ReferenceDatabase): vo 'tags', [], [ - { field: 'tagIds', otherObjectType: 'albums' }, - { field: 'tagIds', otherObjectType: 'artists' }, - { field: 'tagIds', otherObjectType: 'tracks' }, + { field: 'tagIds', otherObjectType: 'albums', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'artists', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tracks', doReverseReference: true }, ], db ); +} + +// Modify a track. +export function modifyTrack(userId: number, id: number, updates: TrackBaseWithRefs, db: ReferenceDatabase): void { + return modifyObject(userId, id, updates, 'tracks', + [{ field: 'albumId', otherObjectType: 'albums', doReverseReference: true }], + [ + { field: 'artistIds', otherObjectType: 'artists', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tags', doReverseReference: false }, + ], + db); +} + +// Modify an artist. +export function modifyArtist(userId: number, id: number, updates: ArtistBaseWithRefs, db: ReferenceDatabase): void { + return modifyObject(userId, id, updates, 'artists', + [], + [ + { field: 'albumIds', otherObjectType: 'albums', doReverseReference: true }, + { field: 'trackIds', otherObjectType: 'tracks', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tags', doReverseReference: false }, + ], + db); +} + +// Modify an album. +export function modifyAlbum(userId: number, id: number, updates: AlbumBaseWithRefs, db: ReferenceDatabase): void { + return modifyObject(userId, id, updates, 'albums', + [], + [ + { field: 'artistIds', otherObjectType: 'artists', doReverseReference: true }, + { field: 'trackIds', otherObjectType: 'tracks', doReverseReference: true }, + { field: 'tagIds', otherObjectType: 'tags', doReverseReference: false }, + ], + db); +} + +// Modify a tag. +export function modifyTag(userId: number, id: number, updates: TagBaseWithRefs, db: ReferenceDatabase): void { + return modifyObject(userId, id, updates, 'tags', + [{ field: 'parentId', otherObjectType: 'tags', doReverseReference: false },], + [], + db); } \ No newline at end of file diff --git a/server/test/reference_model/randomGen.ts b/server/test/reference_model/randomGen.ts index 57705a4..14c23c2 100644 --- a/server/test/reference_model/randomGen.ts +++ b/server/test/reference_model/randomGen.ts @@ -1,8 +1,9 @@ -import { AlbumWithRefs, AlbumWithRefsWithId, ArtistWithRefs, ArtistWithRefsWithId, TagWithRefs, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from "../../../client/src/api/api"; +import { AlbumBaseWithRefs, AlbumWithRefs, AlbumWithRefsWithId, ArtistBaseWithRefs, ArtistWithRefs, ArtistWithRefsWithId, TagBaseWithRefs, TagWithRefs, TagWithRefsWithId, TrackBaseWithRefs, TrackWithRefs, TrackWithRefsWithId } from "../../../client/src/api/api"; import { userEndpoints } from "../../endpoints/User"; -import { createAlbum, createArtist, createTag, createTrack, deleteAlbum, deleteArtist, deleteTag, deleteTrack, ReferenceDatabase } from "./DBReferenceModel"; +import { createAlbum, createArtist, createTag, createTrack, deleteAlbum, deleteArtist, deleteTag, deleteTrack, modifyAlbum, modifyArtist, modifyTag, modifyTrack, ReferenceDatabase } from "./DBReferenceModel"; import * as helpers from '../integration/helpers'; import { DBErrorKind, isDBError } from "../../endpoints/types"; +let _ = require('lodash'); export enum DBActionType { CreateTrack = "CreateTrack", @@ -13,6 +14,14 @@ export enum DBActionType { DeleteAlbum = "DeleteAlbum", DeleteArtist = "DeleteArtist", DeleteTag = "DeleteTag", + PutTrack = "PutTrack", + PutAlbum = "PutAlbum", + PutArtist = "PutArtist", + PutTag = "PutTag", + PatchTrack = "PatchTrack", + PatchAlbum = "PatchAlbum", + PatchArtist = "PatchArtist", + PatchTag = "PatchTag", } export interface DBAction { @@ -34,6 +43,14 @@ export interface RandomDBActionDistribution { deleteArtistParams: RandomDeleteObjectDistribution, deleteAlbumParams: RandomDeleteObjectDistribution, deleteTagParams: RandomDeleteObjectDistribution, + patchTrackParams: RandomPatchTrackDistribution, + patchAlbumParams: RandomPatchAlbumDistribution, + patchArtistParams: RandomPatchArtistDistribution, + patchTagParams: RandomPatchTagDistribution, + putTrackParams: RandomPutTrackDistribution, + putAlbumParams: RandomPutAlbumDistribution, + putArtistParams: RandomPutArtistDistribution, + putTagParams: RandomPutTagDistribution, } export interface RandomCreateTrackDistribution { @@ -47,6 +64,15 @@ export interface RandomCreateTrackDistribution { } linkAlbum: Distribution, } +export interface RandomPutTrackDistribution extends RandomCreateTrackDistribution { + validId: Distribution, +} +export interface RandomPatchTrackDistribution extends RandomPutTrackDistribution { + replaceName: Distribution, + replaceArtists: Distribution, + replaceTags: Distribution, + replaceAlbum: Distribution, +} export interface RandomCreateAlbumDistribution { linkArtists: { @@ -62,6 +88,15 @@ export interface RandomCreateAlbumDistribution { numInvalid: Distribution, } } +export interface RandomPutAlbumDistribution extends RandomCreateAlbumDistribution { + validId: Distribution, +} +export interface RandomPatchAlbumDistribution extends RandomPutAlbumDistribution { + replaceName: Distribution, + replaceArtists: Distribution, + replaceTags: Distribution, + replaceTracks: Distribution, +} export interface RandomCreateArtistDistribution { linkAlbums: { @@ -77,10 +112,27 @@ export interface RandomCreateArtistDistribution { numInvalid: Distribution, } } +export interface RandomPutArtistDistribution extends RandomCreateArtistDistribution { + validId: Distribution, +} +export interface RandomPatchArtistDistribution extends RandomPutArtistDistribution { + replaceName: Distribution, + replaceAlbums: Distribution, + replaceTags: Distribution, + replaceTracks: Distribution, +} export interface RandomCreateTagDistribution { linkParent: Distribution, } +export interface RandomPutTagDistribution extends RandomCreateTagDistribution { + validId: Distribution, +} +export interface RandomPatchTagDistribution extends RandomPutTagDistribution { + replaceName: Distribution, + replaceParent: Distribution, + validId: Distribution, +} export interface RandomDeleteObjectDistribution { validId: Distribution, @@ -169,6 +221,30 @@ export function applyReferenceDBAction( status = 200; break; } + case DBActionType.PutTrack: + case DBActionType.PutAlbum: + case DBActionType.PutArtist: + case DBActionType.PutTag: + case DBActionType.PatchTrack: + case DBActionType.PatchAlbum: + case DBActionType.PatchArtist: + case DBActionType.PatchTag: + { + let funcs = { + [DBActionType.PutTrack]: modifyTrack, + [DBActionType.PutAlbum]: modifyAlbum, + [DBActionType.PutArtist]: modifyArtist, + [DBActionType.PutTag]: modifyTag, + [DBActionType.PatchTrack]: modifyTrack, + [DBActionType.PatchAlbum]: modifyAlbum, + [DBActionType.PatchArtist]: modifyArtist, + [DBActionType.PatchTag]: modifyTag, + } + funcs[action.type](action.userId, action.payload.id, _.omit(action.payload, ['id']), db); + response = {}; + status = 200; + break; + } } } catch (e) { if (isDBError(e)) { @@ -228,6 +304,30 @@ export async function applyRealDBAction( response = res.body; break; } + case DBActionType.PutTrack: + case DBActionType.PutAlbum: + case DBActionType.PutArtist: + case DBActionType.PutTag: + case DBActionType.PatchTrack: + case DBActionType.PatchAlbum: + case DBActionType.PatchArtist: + case DBActionType.PatchTag: + { + let funcs = { + [DBActionType.PutTrack]: helpers.putTrack, + [DBActionType.PutAlbum]: helpers.putAlbum, + [DBActionType.PutArtist]: helpers.putArtist, + [DBActionType.PutTag]: helpers.putTag, + [DBActionType.PatchTrack]: helpers.patchTrack, + [DBActionType.PatchAlbum]: helpers.patchAlbum, + [DBActionType.PatchArtist]: helpers.patchArtist, + [DBActionType.PatchTag]: helpers.patchTag, + } + let res = await funcs[action.type](req, action.payload.id, _.omit(action.payload, ['id'])); + status = res.status; + response = res.body; + break; + } } return { response: response, status: status }; @@ -252,20 +352,54 @@ export function randomDBAction( case DBActionType.CreateArtist: case DBActionType.CreateAlbum: case DBActionType.CreateTag: + case DBActionType.PutTrack: + case DBActionType.PutAlbum: + case DBActionType.PutArtist: + case DBActionType.PutTag: + case DBActionType.PatchTrack: + case DBActionType.PatchAlbum: + case DBActionType.PatchArtist: + case DBActionType.PatchTag: { let funcs = { [DBActionType.CreateTrack]: createRandomTrack, [DBActionType.CreateArtist]: createRandomArtist, [DBActionType.CreateAlbum]: createRandomAlbum, [DBActionType.CreateTag]: createRandomTag, + [DBActionType.PutTrack]: createRandomTrack, + [DBActionType.PutArtist]: createRandomArtist, + [DBActionType.PutAlbum]: createRandomAlbum, + [DBActionType.PutTag]: createRandomTag, + [DBActionType.PatchTrack]: createPartialRandomTrack, + [DBActionType.PatchArtist]: createPartialRandomArtist, + [DBActionType.PatchAlbum]: createPartialRandomAlbum, + [DBActionType.PatchTag]: createPartialRandomTag, } let params = { [DBActionType.CreateTrack]: distribution.createTrackParams, [DBActionType.CreateArtist]: distribution.createArtistParams, [DBActionType.CreateAlbum]: distribution.createAlbumParams, [DBActionType.CreateTag]: distribution.createTagParams, + [DBActionType.PutTrack]: distribution.putTrackParams, + [DBActionType.PutArtist]: distribution.putArtistParams, + [DBActionType.PutAlbum]: distribution.putAlbumParams, + [DBActionType.PutTag]: distribution.putTagParams, + [DBActionType.PatchTrack]: distribution.patchTrackParams, + [DBActionType.PatchArtist]: distribution.patchArtistParams, + [DBActionType.PatchAlbum]: distribution.patchAlbumParams, + [DBActionType.PatchTag]: distribution.patchTagParams, + } + let objectArrays = { + [DBActionType.PatchTrack]: db[userId].tracks, + [DBActionType.PatchArtist]: db[userId].artists, + [DBActionType.PatchAlbum]: db[userId].albums, + [DBActionType.PatchTag]: db[userId].tags, + [DBActionType.PutTrack]: db[userId].tracks, + [DBActionType.PutArtist]: db[userId].artists, + [DBActionType.PutAlbum]: db[userId].albums, + [DBActionType.PutTag]: db[userId].tags, } - return { + let object: any = { type: type, payload: funcs[type]( db, @@ -275,6 +409,23 @@ export function randomDBAction( ), userId: userId, }; + // For patch and put operations, we have to include an ID. + if ([DBActionType.PatchTrack, + DBActionType.PatchTag, + DBActionType.PatchAlbum, + DBActionType.PatchArtist, + DBActionType.PutTrack, + DBActionType.PutTag, + DBActionType.PutAlbum, + DBActionType.PutArtist].includes(type)) { + let _type = type as + DBActionType.PatchTrack | DBActionType.PatchTag | DBActionType.PatchAlbum | DBActionType.PatchArtist + | DBActionType.PutTrack | DBActionType.PutTag | DBActionType.PutAlbum | DBActionType.PutArtist + let validIdx = Math.floor(randomNumGen() * objectArrays[_type].length); + let validId = objectArrays[_type][validIdx].id; + object.payload.id = applyDistribution(params[_type].validId, randomNumGen) ? validId : validId + 99999; + } + return object; } case DBActionType.DeleteTrack: case DBActionType.DeleteArtist: @@ -292,11 +443,11 @@ export function randomDBAction( [DBActionType.DeleteAlbum]: db[userId].albums, [DBActionType.DeleteTag]: db[userId].tags, } + let validIdx = Math.floor(randomNumGen() * objectArrays[type].length); + let validId = objectArrays[type][validIdx].id; return { type: type, - payload: applyDistribution(params[type].validId, randomNumGen) ? - Math.floor(randomNumGen() * objectArrays[type].length) + 1 : - Math.floor(randomNumGen() * objectArrays[type].length) + 1 + objectArrays[type].length, + payload: applyDistribution(params[type].validId, randomNumGen) ? validId : validId + 99999, userId: userId, } } @@ -381,6 +532,20 @@ export function createRandomTrack( } } +export function createPartialRandomTrack( + db: ReferenceDatabase, + userId: number, + dist: RandomPatchTrackDistribution, + randomNumGen: any, +): TrackBaseWithRefs { + let track: TrackBaseWithRefs = createRandomTrack(db, userId, dist, randomNumGen) + if (!applyDistribution(dist.replaceName, randomNumGen)) { delete track.name } + if (!applyDistribution(dist.replaceTags, randomNumGen)) { delete track.tagIds } + if (!applyDistribution(dist.replaceAlbum, randomNumGen)) { delete track.albumId } + if (!applyDistribution(dist.replaceArtists, randomNumGen)) { delete track.artistIds } + return track; +} + export function createRandomArtist( db: ReferenceDatabase, userId: number, @@ -415,6 +580,20 @@ export function createRandomArtist( } } +export function createPartialRandomArtist( + db: ReferenceDatabase, + userId: number, + dist: RandomPatchArtistDistribution, + randomNumGen: any, +): ArtistBaseWithRefs { + let artist: ArtistBaseWithRefs = createRandomArtist(db, userId, dist, randomNumGen) + if (!applyDistribution(dist.replaceName, randomNumGen)) { delete artist.name } + if (!applyDistribution(dist.replaceTags, randomNumGen)) { delete artist.tagIds } + if (!applyDistribution(dist.replaceAlbums, randomNumGen)) { delete artist.albumIds } + if (!applyDistribution(dist.replaceTracks, randomNumGen)) { delete artist.trackIds } + return artist; +} + export function createRandomAlbum( db: ReferenceDatabase, userId: number, @@ -449,6 +628,20 @@ export function createRandomAlbum( } } +export function createPartialRandomAlbum( + db: ReferenceDatabase, + userId: number, + dist: RandomPatchAlbumDistribution, + randomNumGen: any, +): AlbumBaseWithRefs { + let album: AlbumBaseWithRefs = createRandomAlbum(db, userId, dist, randomNumGen) + if (!applyDistribution(dist.replaceName, randomNumGen)) { delete album.name } + if (!applyDistribution(dist.replaceTags, randomNumGen)) { delete album.tagIds } + if (!applyDistribution(dist.replaceArtists, randomNumGen)) { delete album.artistIds } + if (!applyDistribution(dist.replaceTracks, randomNumGen)) { delete album.trackIds } + return album; +} + export function createRandomTag( db: ReferenceDatabase, userId: number, @@ -466,4 +659,16 @@ export function createRandomTag( name: randomString(randomNumGen, 20), parentId: maybeParent, } +} + +export function createPartialRandomTag( + db: ReferenceDatabase, + userId: number, + dist: RandomPatchTagDistribution, + randomNumGen: any, +): TagBaseWithRefs { + let tag: TagBaseWithRefs = createRandomTag(db, userId, dist, randomNumGen) + if (!applyDistribution(dist.replaceName, randomNumGen)) { delete tag.name } + if (!applyDistribution(dist.replaceParent, randomNumGen)) { delete tag.parentId } + return tag; } \ No newline at end of file