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.
 
 
 
 

379 lines
14 KiB

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.
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, exact: boolean, data: DBDataFormat) {
if (toObjectsType === 'tracks') {
(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) }
});
}
}
// 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, doReverseReference: boolean };
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) => {
f.doReverseReference &&
ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, true, db[userId]);
});
pluralReverseRefs.forEach((f: ReferencingField) => {
f.doReverseReference &&
ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, true, 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) => {
f.doReverseReference &&
db[userId][f.otherObjectType].forEach((other: any) => { filterInPlace(other[f.field], (oid: number) => oid !== objectId) })
});
singularRefsToThisObject.forEach((f: ReferencingField) => {
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', doReverseReference: true }],
[
{ field: 'artistIds', otherObjectType: 'artists', doReverseReference: true },
{ field: 'tagIds', otherObjectType: 'tags', doReverseReference: false },
],
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', doReverseReference: true },
{ field: 'trackIds', otherObjectType: 'tracks', doReverseReference: true },
{ field: 'tagIds', otherObjectType: 'tags', doReverseReference: false },
],
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', doReverseReference: true },
{ field: 'trackIds', otherObjectType: 'tracks', doReverseReference: true },
{ field: 'tagIds', otherObjectType: 'tags', doReverseReference: false },
],
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', doReverseReference: false },],
[],
db
);
}
// Delete a track.
export function deleteTrack(userId: number, id: number, db: ReferenceDatabase): void {
return deleteObject(
userId,
id,
'tracks',
[],
[
{ field: 'trackIds', otherObjectType: 'albums', doReverseReference: true },
{ field: 'trackIds', otherObjectType: 'artists', doReverseReference: true },
],
db
);
}
// Delete an artist.
export function deleteArtist(userId: number, id: number, db: ReferenceDatabase): void {
return deleteObject(
userId,
id,
'artists',
[],
[
{ field: 'artistIds', otherObjectType: 'tracks', doReverseReference: true },
{ field: 'artistIds', otherObjectType: 'albums', doReverseReference: true },
],
db
);
}
// Delete a track.
export function deleteAlbum(userId: number, id: number, db: ReferenceDatabase): void {
return deleteObject(
userId,
id,
'albums',
[{ 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, 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) => {
// Prevent cyclic dependencies.
if (!_recursiveIdsSoFar.includes(child.id)) {
deleteTag(userId, child.id, db, _recursiveIdsSoFar)
}
})
// Do the actual deletion of this tag.
return deleteObject(
userId,
id,
'tags',
[],
[
{ 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);
}