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