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, 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", CreateAlbum = "CreateAlbum", CreateTag = "CreateTag", CreateArtist = "CreateArtist", DeleteTrack = "DeleteTrack", 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 { type: DBActionType, userId: number, payload: any, } export type Distribution = Map; export interface RandomDBActionDistribution { type: Distribution, userId: Distribution, createTrackParams: RandomCreateTrackDistribution, createAlbumParams: RandomCreateAlbumDistribution, createArtistParams: RandomCreateArtistDistribution, createTagParams: RandomCreateTagDistribution, deleteTrackParams: RandomDeleteObjectDistribution, 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 { linkArtists: { numValid: Distribution, numInvalid: Distribution, } linkTags: { numValid: Distribution, numInvalid: Distribution, } 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: { numValid: Distribution, numInvalid: Distribution, } linkTags: { numValid: Distribution, numInvalid: Distribution, } linkTracks: { numValid: Distribution, 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: { numValid: Distribution, numInvalid: Distribution, } linkTags: { numValid: Distribution, numInvalid: Distribution, } linkTracks: { numValid: Distribution, 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, } export function applyDistribution( dist: Map, randomNumGen: any, ): T { let n = randomNumGen(); let r: T | undefined = undefined; dist.forEach((value: number, key: T) => { if (r) { return; } if (n <= value) { r = key; } else { n -= value; } }) if (r === undefined) { throw new Error(`Invalid distribution: n=${n}, dist ${JSON.stringify(dist.entries())}`); } return r; } export function randomString(randomNumGen: any, length: number) { let chars = 'abcdefghijklmnopqrstuvwxyz'; let retval = ''; for (let i = 0; i < length; i++) { retval += chars[Math.floor(randomNumGen() * 26)]; } return retval; } export function pickNFromArray( array: T[], randomNumGen: any, N: number) : T[] { let r: T[] = []; for (let i = 0; i < N; i++) { let idx = Math.floor(randomNumGen() * array.length); r.push(array[idx]); array.splice(idx); } return r; } export function applyReferenceDBAction( action: DBAction, db: ReferenceDatabase ): { response: any, status: number, } { let response: any = undefined; let status: number = 0; try { switch (action.type) { case DBActionType.CreateTrack: case DBActionType.CreateAlbum: case DBActionType.CreateArtist: case DBActionType.CreateTag: { let funcs = { [DBActionType.CreateTrack]: createTrack, [DBActionType.CreateAlbum]: createAlbum, [DBActionType.CreateArtist]: createArtist, [DBActionType.CreateTag]: createTag, } response = funcs[action.type](action.userId, action.payload, db); status = 200; break; } case DBActionType.DeleteTrack: case DBActionType.DeleteAlbum: case DBActionType.DeleteArtist: case DBActionType.DeleteTag: { let funcs = { [DBActionType.DeleteTrack]: deleteTrack, [DBActionType.DeleteAlbum]: deleteAlbum, [DBActionType.DeleteArtist]: deleteArtist, [DBActionType.DeleteTag]: deleteTag, } funcs[action.type](action.userId, action.payload, db); response = {}; 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)) { if (e.kind === DBErrorKind.ResourceNotFound) { status = 404; response = {}; } } else { throw e; } } return { response: response, status: status }; } export async function applyRealDBAction( action: DBAction, req: any, ): Promise<{ response: any, status: number, }> { let response: any = undefined; let status: number = 0; switch (action.type) { case DBActionType.CreateTrack: case DBActionType.CreateAlbum: case DBActionType.CreateArtist: case DBActionType.CreateTag: { let funcs = { [DBActionType.CreateTrack]: helpers.createTrack, [DBActionType.CreateAlbum]: helpers.createAlbum, [DBActionType.CreateArtist]: helpers.createArtist, [DBActionType.CreateTag]: helpers.createTag, } let res = await funcs[action.type](req, action.payload); status = res.status; response = res.body; break; } case DBActionType.DeleteTrack: case DBActionType.DeleteAlbum: case DBActionType.DeleteArtist: case DBActionType.DeleteTag: { let funcs = { [DBActionType.DeleteTrack]: helpers.deleteTrack, [DBActionType.DeleteAlbum]: helpers.deleteAlbum, [DBActionType.DeleteArtist]: helpers.deleteArtist, [DBActionType.DeleteTag]: helpers.deleteTag, } let res = await funcs[action.type](req, action.payload); status = res.status; 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 }; } export function randomDBAction( db: ReferenceDatabase, randomNumGen: any, distribution: RandomDBActionDistribution, ): DBAction { let type = applyDistribution( distribution.type, randomNumGen ); let userId = applyDistribution( distribution.userId, randomNumGen ); switch (type) { case DBActionType.CreateTrack: 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, } let object: any = { type: type, payload: funcs[type]( db, userId, params[type] as any, randomNumGen ), 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: case DBActionType.DeleteAlbum: case DBActionType.DeleteTag: { let params = { [DBActionType.DeleteTrack]: distribution.deleteTrackParams, [DBActionType.DeleteArtist]: distribution.deleteArtistParams, [DBActionType.DeleteAlbum]: distribution.deleteAlbumParams, [DBActionType.DeleteTag]: distribution.deleteTagParams, } let objectArrays = { [DBActionType.DeleteTrack]: db[userId].tracks, [DBActionType.DeleteArtist]: db[userId].artists, [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) ? validId : validId + 99999, userId: userId, } } } } function pickArtists(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { let allValidArtistIds: number[] = db[userId] && db[userId].artists ? db[userId].artists.map((a: ArtistWithRefsWithId) => a.id) : []; let validArtists: number[] = pickNFromArray(allValidArtistIds, rng, nValid); let invalidArtists: number[] = []; for (let i = 0; i < nInvalid; i++) { invalidArtists.push(Math.round(rng() * 100) + allValidArtistIds.length); } return [...validArtists, ...invalidArtists]; } function pickAlbums(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { let allValidAlbumIds: number[] = db[userId] && db[userId].albums ? db[userId].albums.map((a: AlbumWithRefsWithId) => a.id) : []; let validAlbums: number[] = pickNFromArray(allValidAlbumIds, rng, nValid); let invalidAlbums: number[] = []; for (let i = 0; i < nInvalid; i++) { invalidAlbums.push(Math.round(rng() * 100) + allValidAlbumIds.length); } return [...validAlbums, ...invalidAlbums]; } function pickTracks(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { let allValidTrackIds: number[] = db[userId] && db[userId].tracks ? db[userId].tracks.map((a: TrackWithRefsWithId) => a.id) : []; let validTracks: number[] = pickNFromArray(allValidTrackIds, rng, nValid); let invalidTracks: number[] = []; for (let i = 0; i < nInvalid; i++) { invalidTracks.push(Math.round(rng() * 100) + allValidTrackIds.length); } return [...validTracks, ...invalidTracks]; } function pickTags(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { let allValidTagIds: number[] = db[userId] && db[userId].tags ? db[userId].tags.map((a: TagWithRefsWithId) => a.id) : []; let validTags: number[] = pickNFromArray(allValidTagIds, rng, nValid); let invalidTags: number[] = []; for (let i = 0; i < nInvalid; i++) { invalidTags.push(Math.round(rng() * 100) + allValidTagIds.length); } return [...validTags, ...invalidTags]; } export function createRandomTrack( db: ReferenceDatabase, userId: number, dist: RandomCreateTrackDistribution, randomNumGen: any, ): TrackWithRefs { let artists: number[] = pickArtists(userId, applyDistribution(dist.linkArtists.numValid, randomNumGen), applyDistribution(dist.linkArtists.numInvalid, randomNumGen), randomNumGen, db); let tags: number[] = pickTags(userId, applyDistribution(dist.linkTags.numValid, randomNumGen), applyDistribution(dist.linkTags.numInvalid, randomNumGen), randomNumGen, db); let maybeAlbum: number | null = (() => { let r: boolean | null | 'nonexistent' = applyDistribution(dist.linkAlbum, randomNumGen); let albums = pickAlbums(userId, r === true ? 1 : 0, r === 'nonexistent' ? 1 : 0, randomNumGen, db); return albums.length ? albums[0] : null; })(); return { mbApi_typename: 'track', albumId: maybeAlbum, artistIds: artists, tagIds: tags, name: randomString(randomNumGen, 20), storeLinks: [], // TODO } } 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, dist: RandomCreateArtistDistribution, randomNumGen: any, ): ArtistWithRefs { let tracks: number[] = pickTracks(userId, applyDistribution(dist.linkTracks.numValid, randomNumGen), applyDistribution(dist.linkTracks.numInvalid, randomNumGen), randomNumGen, db); let tags: number[] = pickTags(userId, applyDistribution(dist.linkTags.numValid, randomNumGen), applyDistribution(dist.linkTags.numInvalid, randomNumGen), randomNumGen, db); let albums: number[] = pickAlbums(userId, applyDistribution(dist.linkAlbums.numValid, randomNumGen), applyDistribution(dist.linkAlbums.numInvalid, randomNumGen), randomNumGen, db); return { mbApi_typename: 'artist', albumIds: albums, trackIds: tracks, tagIds: tags, name: randomString(randomNumGen, 20), storeLinks: [], // TODO } } 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, dist: RandomCreateAlbumDistribution, randomNumGen: any, ): AlbumWithRefs { let tracks: number[] = pickTracks(userId, applyDistribution(dist.linkTracks.numValid, randomNumGen), applyDistribution(dist.linkTracks.numInvalid, randomNumGen), randomNumGen, db); let tags: number[] = pickTags(userId, applyDistribution(dist.linkTags.numValid, randomNumGen), applyDistribution(dist.linkTags.numInvalid, randomNumGen), randomNumGen, db); let artists: number[] = pickArtists(userId, applyDistribution(dist.linkArtists.numValid, randomNumGen), applyDistribution(dist.linkArtists.numInvalid, randomNumGen), randomNumGen, db); return { mbApi_typename: 'album', artistIds: artists, trackIds: tracks, tagIds: tags, name: randomString(randomNumGen, 20), storeLinks: [], // TODO } } 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, dist: RandomCreateTagDistribution, randomNumGen: any, ): TagWithRefs { let maybeParent: number | null = (() => { let r: boolean | null | 'nonexistent' = applyDistribution(dist.linkParent, randomNumGen); let tags = pickTags(userId, r === true ? 1 : 0, r === 'nonexistent' ? 1 : 0, randomNumGen, db); return tags.length ? tags[0] : null; })(); return { mbApi_typename: 'tag', 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; }