import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostAlbumRequest, PostArtistRequest, PostTagRequest, PostTrackRequest, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api"; import { makeNotFoundError } from "../../db/common"; import filterInPlace from "../../lib/filterInPlace"; // The mock reference database is in the same format as // the JSON import/export format, for multiple users. export type ReferenceDatabase = Record type ObjectsType = "tracks" | "artists" | "tags" | "albums"; // Get a fresh ID for a new object. function getNewId(db: ReferenceDatabase, objectsType: ObjectsType): number { let highest: number = 1; for (const data of Object.values(db)) { data[objectsType].forEach((obj: any) => highest = Math.max(highest, obj.id)); } return highest + 1; } // Check a (set of) IDs for presence in the objects array. // All have to exist for it to return true. function checkExists(objects: any[], ids: number[]) { return ids.reduce((prev: boolean, id: number) => { return prev && objects.find((object: any) => object.id === id); }, true); } // If not in the array, put the number in the array. function ensureInSet(n: number, s: number[]) { if (!(n in s)) { s.push(n); } } // For a set of objects, ensure they point to another object. function ensureLinked(fromObjects: number[], fromObjectsType: ObjectsType, toId: number, toObjectsType: ObjectsType, 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) }) } } // Create a new object. export interface LinkField { field: string, otherObjectType: ObjectsType }; export function createObject( userId: number, object: any, objectType: ObjectsType, singularLinkFields: LinkField[], pluralLinkFields: LinkField[], db: ReferenceDatabase ): { id: number } { // Existence checks if (!(userId in db)) { throw makeNotFoundError() } singularLinkFields.forEach((f: LinkField) => { if (!checkExists(db[userId][f.otherObjectType], object[f.field] ? [object[f.field]] : [])) { throw makeNotFoundError(); } }); pluralLinkFields.forEach((f: LinkField) => { if (!checkExists(db[userId][f.otherObjectType], object[f.field] || [])) { throw makeNotFoundError(); } }); // Create an ID and the object let id = getNewId(db, objectType); db[userId][objectType].push({ ...object, id: id, }) // reverse links singularLinkFields.forEach((f: LinkField) => { ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, db[userId]); }); pluralLinkFields.forEach((f: LinkField) => { ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, db[userId]); }); return { id: id }; } // 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: 'artistIds', otherObjectType: 'artists' }, { field: 'tagIds', otherObjectType: 'tags' }, ], db ); } // Create a new album. export function createAlbum(userId: number, album: PostAlbumRequest, db: ReferenceDatabase): { id: number } { return createObject( userId, album, 'albums', [], [ { field: 'artistIds', otherObjectType: 'artists' }, { field: 'trackIds', otherObjectType: 'tracks' }, { field: 'tagIds', otherObjectType: 'tags' }, ], db ); } // Create a new artist. export function createArtist(userId: number, artist: PostArtistRequest, db: ReferenceDatabase): { id: number } { return createObject( userId, artist, 'artists', [], [ { field: 'albumIds', otherObjectType: 'albums' }, { field: 'trackIds', otherObjectType: 'tracks' }, { field: 'tagIds', otherObjectType: 'tags' }, ], db ); } // Create a new tag. export function createTag(userId: number, tag: PostTagRequest, db: ReferenceDatabase): { id: number } { return createObject( userId, tag, 'tags', [{ field: 'parentId', otherObjectType: 'tags' }], [], db ); } // Delete a track. export function deleteTrack(userId: number, id: number, db: ReferenceDatabase): void { // Existence checks if (!(userId in db)) { throw makeNotFoundError() } // Find the object to delete. let idx = db[userId].tracks.findIndex((track: TrackWithRefsWithId) => track.id === id); if (idx < 0) { // Not found throw makeNotFoundError(); } // Remove references db[userId].albums.forEach((x: AlbumWithRefsWithId) => { filterInPlace(x.trackIds, (tid: number) => tid !== id); }) db[userId].artists.forEach((x: ArtistWithRefsWithId) => { filterInPlace(x.trackIds, (tid: number) => tid !== id); }) // Delete the object db[userId].tracks.splice(idx, 1); }