You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

251 lines
8.1 KiB

import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostAlbumRequest, PostArtistRequest, PostTagRequest, PostTrackRequest, TagWithRefsWithId, 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<number, DBDataFormat>
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.
// The general procedure for this is:
// - 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 function createObject(
userId: number,
object: any,
objectType: ObjectsType,
singularReverseRefs: ReferencingField[],
pluralReverseRefs: ReferencingField[],
db: ReferenceDatabase
): { id: number } {
// Existence checks
if (!(userId in db)) { throw makeNotFoundError() }
singularReverseRefs.forEach((f: ReferencingField) => {
if (!checkExists(db[userId][f.otherObjectType], object[f.field] ? [object[f.field]] : [])) {
throw makeNotFoundError();
}
});
pluralReverseRefs.forEach((f: ReferencingField) => {
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
singularReverseRefs.forEach((f: ReferencingField) => {
ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, db[userId]);
});
pluralReverseRefs.forEach((f: ReferencingField) => {
ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, db[userId]);
});
return { id: id };
}
// Delete an object.
// The general procedure for this is:
// - check that the to-be-deleted object exists
// - remove any references that exist to the object in other objects
// - delete the object
export function deleteObject(
userId: number,
objectId: number,
objectType: ObjectsType,
singularRefsToThisObject: ReferencingField[],
pluralRefsToThisObject: ReferencingField[],
db: ReferenceDatabase
): void {
// Existence checks
if (!(userId in db)) { throw makeNotFoundError() }
// Find the object to delete.
let idx = db[userId][objectType].findIndex((o: any) => 'id' in o && o.id === objectId);
if (idx < 0) {
// Not found
throw makeNotFoundError();
}
// Remove references to this object
pluralRefsToThisObject.forEach((f: ReferencingField) => {
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; } })
});
// Delete the object
db[userId][objectType].splice(idx, 1);
}
// 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 {
return deleteObject(
userId,
id,
'tracks',
[],
[
{ field: 'trackIds', otherObjectType: 'albums' },
{ field: 'trackIds', otherObjectType: 'artists' },
],
db
);
}
// Delete an artist.
export function deleteArtist(userId: number, id: number, db: ReferenceDatabase): void {
return deleteObject(
userId,
id,
'artists',
[],
[
{ field: 'artistIds', otherObjectType: 'tracks' },
{ field: 'artistIds', otherObjectType: 'albums' },
],
db
);
}
// Delete a track.
export function deleteAlbum(userId: number, id: number, db: ReferenceDatabase): void {
return deleteObject(
userId,
id,
'albums',
[{ field: 'albumId', otherObjectType: 'tracks' }],
[{ field: 'albumIds', otherObjectType: 'artists' },],
db
);
}
// Delete a tag.
export function deleteTag(userId: number, id: number, db: ReferenceDatabase): void {
// Tags are special in that deleting them also deletes their children. Implement that here
// with recursive calls.
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) })
// Do the actual deletion of this tag.
return deleteObject(
userId,
id,
'tags',
[],
[
{ field: 'tagIds', otherObjectType: 'albums' },
{ field: 'tagIds', otherObjectType: 'artists' },
{ field: 'tagIds', otherObjectType: 'tracks' },
],
db
);
}