Stuck in some recursivity problems.

editsong
Sander Vocke 5 years ago
parent cd26f79e4b
commit 40e3ba2203
  1. 63
      server/db/Album.ts
  2. 4
      server/endpoints/Album.ts
  3. 2
      server/endpoints/Tag.ts
  4. 4
      server/endpoints/Track.ts
  5. 132
      server/test/integration/flows/ResourceFlow.ts
  6. 71
      server/test/integration/helpers.ts
  7. 192
      server/test/reference_model/DBReferenceModel.ts
  8. 217
      server/test/reference_model/randomGen.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<api.TrackWithId[]> =
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<api.ArtistWithId[]> =
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<void> {

@ -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);

@ -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) {

@ -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);

@ -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<boolean | 'nonexistent', number>([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]),
},
putTrackParams: {
linkAlbum: new Map<boolean | 'nonexistent', number>([[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<boolean | 'nonexistent', number>([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]),
validId: new Map([[false, 0.3], [true, 0.7]]),
},
patchTrackParams: {
linkAlbum: new Map<boolean | 'nonexistent', number>([[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<boolean | 'nonexistent', number>([[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; }

@ -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: {} },

@ -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);
}

@ -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<boolean | 'nonexistent'>,
}
export interface RandomPutTrackDistribution extends RandomCreateTrackDistribution {
validId: Distribution<boolean>,
}
export interface RandomPatchTrackDistribution extends RandomPutTrackDistribution {
replaceName: Distribution<boolean>,
replaceArtists: Distribution<boolean>,
replaceTags: Distribution<boolean>,
replaceAlbum: Distribution<boolean>,
}
export interface RandomCreateAlbumDistribution {
linkArtists: {
@ -62,6 +88,15 @@ export interface RandomCreateAlbumDistribution {
numInvalid: Distribution<number>,
}
}
export interface RandomPutAlbumDistribution extends RandomCreateAlbumDistribution {
validId: Distribution<boolean>,
}
export interface RandomPatchAlbumDistribution extends RandomPutAlbumDistribution {
replaceName: Distribution<boolean>,
replaceArtists: Distribution<boolean>,
replaceTags: Distribution<boolean>,
replaceTracks: Distribution<boolean>,
}
export interface RandomCreateArtistDistribution {
linkAlbums: {
@ -77,10 +112,27 @@ export interface RandomCreateArtistDistribution {
numInvalid: Distribution<number>,
}
}
export interface RandomPutArtistDistribution extends RandomCreateArtistDistribution {
validId: Distribution<boolean>,
}
export interface RandomPatchArtistDistribution extends RandomPutArtistDistribution {
replaceName: Distribution<boolean>,
replaceAlbums: Distribution<boolean>,
replaceTags: Distribution<boolean>,
replaceTracks: Distribution<boolean>,
}
export interface RandomCreateTagDistribution {
linkParent: Distribution<boolean | 'nonexistent'>,
}
export interface RandomPutTagDistribution extends RandomCreateTagDistribution {
validId: Distribution<boolean>,
}
export interface RandomPatchTagDistribution extends RandomPutTagDistribution {
replaceName: Distribution<boolean>,
replaceParent: Distribution<boolean>,
validId: Distribution<boolean>,
}
export interface RandomDeleteObjectDistribution {
validId: Distribution<boolean>,
@ -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;
}
Loading…
Cancel
Save