parent
b4c747d6f0
commit
bb9a1bdfa6
67 changed files with 3853 additions and 3100 deletions
@ -1,476 +0,0 @@ |
|||||||
// TODO: this file is located in the client src folder because
|
|
||||||
// otherwise, Create React App will refuse to compile it.
|
|
||||||
// Putting it in the server folder or in its own folder makes more sense.
|
|
||||||
|
|
||||||
// This file represents the API interface for Mudbase's back-end.
|
|
||||||
// Each endpoint is described by its endpoint address,
|
|
||||||
// a request structure, a response structure and
|
|
||||||
// a checking function which determines request validity.
|
|
||||||
|
|
||||||
export enum ItemType { |
|
||||||
Song = 0, |
|
||||||
Artist, |
|
||||||
Album, |
|
||||||
Tag |
|
||||||
} |
|
||||||
|
|
||||||
export interface ArtistDetails { |
|
||||||
artistId: number, |
|
||||||
name: string, |
|
||||||
storeLinks?: string[], |
|
||||||
} |
|
||||||
export function isArtistDetails(q: any): q is ArtistDetails { |
|
||||||
return 'artistId' in q; |
|
||||||
} |
|
||||||
export interface AlbumDetails { |
|
||||||
albumId: number, |
|
||||||
name: string, |
|
||||||
storeLinks?: string[], |
|
||||||
} |
|
||||||
export function isAlbumDetails(q: any): q is ArtistDetails { |
|
||||||
return 'albumId' in q; |
|
||||||
} |
|
||||||
export interface TagDetails { |
|
||||||
tagId: number, |
|
||||||
name: string, |
|
||||||
parent?: TagDetails, |
|
||||||
storeLinks?: string[], |
|
||||||
} |
|
||||||
export function isTagDetails(q: any): q is TagDetails { |
|
||||||
return 'tagId' in q; |
|
||||||
} |
|
||||||
export interface RankingDetails { |
|
||||||
rankingId: number, |
|
||||||
type: ItemType, // The item type being ranked
|
|
||||||
rankedId: number, // The item being ranked
|
|
||||||
context: ArtistDetails | TagDetails, |
|
||||||
value: number, // The ranking (higher = better)
|
|
||||||
} |
|
||||||
export function isRankingDetails(q: any): q is RankingDetails { |
|
||||||
return 'rankingId' in q; |
|
||||||
} |
|
||||||
export interface SongDetails { |
|
||||||
songId: number, |
|
||||||
title: string, |
|
||||||
artists?: ArtistDetails[], |
|
||||||
albums?: AlbumDetails[], |
|
||||||
tags?: TagDetails[], |
|
||||||
storeLinks?: string[], |
|
||||||
rankings?: RankingDetails[], |
|
||||||
} |
|
||||||
export function isSongDetails(q: any): q is SongDetails { |
|
||||||
return 'songId' in q; |
|
||||||
} |
|
||||||
|
|
||||||
// Query for items (POST).
|
|
||||||
export const QueryEndpoint = '/query'; |
|
||||||
export enum QueryElemOp { |
|
||||||
And = "AND", |
|
||||||
Or = "OR", |
|
||||||
Not = "NOT", |
|
||||||
} |
|
||||||
export enum QueryFilterOp { |
|
||||||
Eq = "EQ", |
|
||||||
Ne = "NE", |
|
||||||
In = "IN", |
|
||||||
NotIn = "NOTIN", |
|
||||||
Like = "LIKE", |
|
||||||
} |
|
||||||
export enum QueryElemProperty { |
|
||||||
songTitle = "songTitle", |
|
||||||
songId = "songId", |
|
||||||
artistName = "artistName", |
|
||||||
artistId = "artistId", |
|
||||||
albumName = "albumName", |
|
||||||
albumId = "albumId", |
|
||||||
tagId = "tagId", |
|
||||||
songStoreLinks = "songStoreLinks", //Note: treated as a JSON string for filter operations
|
|
||||||
artistStoreLinks = "artistStoreLinks", //Note: treated as a JSON string for filter operations
|
|
||||||
albumStoreLinks = "albumStoreLinks", //Note: treated as a JSON string for filter operations
|
|
||||||
} |
|
||||||
export enum OrderByType { |
|
||||||
Name = 'name', |
|
||||||
} |
|
||||||
export enum QueryResponseType { |
|
||||||
Details = 'details', // Returns detailed result items.
|
|
||||||
Ids = 'ids', // Returns IDs only.
|
|
||||||
Count = 'count', // Returns an item count only.
|
|
||||||
} |
|
||||||
export interface QueryElem { |
|
||||||
prop?: QueryElemProperty, |
|
||||||
propOperand?: any, |
|
||||||
propOperator?: QueryFilterOp, |
|
||||||
children?: QueryElem[] |
|
||||||
childrenOperator?: QueryElemOp, |
|
||||||
} |
|
||||||
export interface Ordering { |
|
||||||
orderBy: { |
|
||||||
type: OrderByType, |
|
||||||
} |
|
||||||
ascending: boolean, |
|
||||||
} |
|
||||||
export interface Query extends QueryElem { } |
|
||||||
export interface QueryRequest { |
|
||||||
query: Query, |
|
||||||
offsetsLimits: OffsetsLimits, |
|
||||||
ordering: Ordering, |
|
||||||
responseType: QueryResponseType |
|
||||||
} |
|
||||||
export interface QueryResponse { |
|
||||||
songs: SongDetails[] | number[] | number, // Details | IDs | count, depending on QueryResponseType
|
|
||||||
artists: ArtistDetails[] | number[] | number, |
|
||||||
tags: TagDetails[] | number[] | number, |
|
||||||
albums: AlbumDetails[] | number[] | number, |
|
||||||
} |
|
||||||
// Note: use -1 as an infinity limit.
|
|
||||||
export interface OffsetsLimits { |
|
||||||
songOffset?: number, |
|
||||||
songLimit?: number, |
|
||||||
artistOffset?: number, |
|
||||||
artistLimit?: number, |
|
||||||
tagOffset?: number, |
|
||||||
tagLimit?: number, |
|
||||||
albumOffset?: number, |
|
||||||
albumLimit?: number, |
|
||||||
} |
|
||||||
export function checkQueryElem(elem: any): boolean { |
|
||||||
if (elem.childrenOperator && elem.children) { |
|
||||||
elem.children.forEach((child: any) => { |
|
||||||
if (!checkQueryElem(child)) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
return (elem.childrenOperator && elem.children) || |
|
||||||
("prop" in elem && "propOperand" in elem && "propOperator" in elem) || |
|
||||||
Object.keys(elem).length === 0; |
|
||||||
} |
|
||||||
export function checkQueryRequest(req: any): boolean { |
|
||||||
return 'query' in req |
|
||||||
&& 'offsetsLimits' in req |
|
||||||
&& 'ordering' in req |
|
||||||
&& 'responseType' in req |
|
||||||
&& checkQueryElem(req.query); |
|
||||||
} |
|
||||||
|
|
||||||
// Get song details (GET).
|
|
||||||
export const SongDetailsEndpoint = '/song/:id'; |
|
||||||
export interface SongDetailsRequest { } |
|
||||||
export interface SongDetailsResponse { |
|
||||||
title: string, |
|
||||||
storeLinks: string[], |
|
||||||
artists?: ArtistDetailsResponseWithId[], |
|
||||||
albums?: AlbumDetailsResponseWithId[], |
|
||||||
tags?: TagDetailsResponseWithId[], |
|
||||||
} |
|
||||||
export function checkSongDetailsRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Get artist details (GET).
|
|
||||||
export const ArtistDetailsEndpoint = '/artist/:id'; |
|
||||||
export interface ArtistDetailsRequest { } |
|
||||||
export interface ArtistDetailsResponse { |
|
||||||
name: string, |
|
||||||
tags?: TagDetailsResponseWithId[], |
|
||||||
storeLinks: string[], |
|
||||||
} |
|
||||||
export function checkArtistDetailsRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new song (POST).
|
|
||||||
export const CreateSongEndpoint = '/song'; |
|
||||||
export interface CreateSongRequest { |
|
||||||
title: string; |
|
||||||
artistIds?: number[]; |
|
||||||
albumIds?: number[]; |
|
||||||
tagIds?: number[]; |
|
||||||
storeLinks?: string[]; |
|
||||||
} |
|
||||||
export interface CreateSongResponse { |
|
||||||
id: number; |
|
||||||
} |
|
||||||
export function checkCreateSongRequest(req: any): boolean { |
|
||||||
return "body" in req && |
|
||||||
"title" in req.body; |
|
||||||
} |
|
||||||
|
|
||||||
// Modify an existing song (PUT).
|
|
||||||
export const ModifySongEndpoint = '/song/:id'; |
|
||||||
export interface ModifySongRequest { |
|
||||||
title?: string; |
|
||||||
artistIds?: number[]; |
|
||||||
albumIds?: number[]; |
|
||||||
tagIds?: number[]; |
|
||||||
storeLinks?: string[]; |
|
||||||
} |
|
||||||
export interface ModifySongResponse { } |
|
||||||
export function checkModifySongRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new album (POST).
|
|
||||||
export const CreateAlbumEndpoint = '/album'; |
|
||||||
export interface CreateAlbumRequest { |
|
||||||
name: string; |
|
||||||
tagIds?: number[]; |
|
||||||
artistIds?: number[]; |
|
||||||
storeLinks?: string[]; |
|
||||||
} |
|
||||||
export interface CreateAlbumResponse { |
|
||||||
id: number; |
|
||||||
} |
|
||||||
export function checkCreateAlbumRequest(req: any): boolean { |
|
||||||
return "body" in req && |
|
||||||
"name" in req.body; |
|
||||||
} |
|
||||||
|
|
||||||
// Modify an existing album (PUT).
|
|
||||||
export const ModifyAlbumEndpoint = '/album/:id'; |
|
||||||
export interface ModifyAlbumRequest { |
|
||||||
name?: string; |
|
||||||
tagIds?: number[]; |
|
||||||
artistIds?: number[]; |
|
||||||
storeLinks?: string[]; |
|
||||||
} |
|
||||||
export interface ModifyAlbumResponse { } |
|
||||||
export function checkModifyAlbumRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Get album details (GET).
|
|
||||||
export const AlbumDetailsEndpoint = '/album/:id'; |
|
||||||
export interface AlbumDetailsRequest { } |
|
||||||
export interface AlbumDetailsResponse { |
|
||||||
name: string; |
|
||||||
artists?: ArtistDetailsResponseWithId[], |
|
||||||
songs?: SongDetailsResponseWithId[], |
|
||||||
tags?: TagDetailsResponseWithId[], |
|
||||||
storeLinks: string[]; |
|
||||||
} |
|
||||||
export function checkAlbumDetailsRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new artist (POST).
|
|
||||||
export const CreateArtistEndpoint = '/artist'; |
|
||||||
export interface CreateArtistRequest { |
|
||||||
name: string; |
|
||||||
tagIds?: number[]; |
|
||||||
storeLinks?: string[]; |
|
||||||
} |
|
||||||
export interface CreateArtistResponse { |
|
||||||
id: number; |
|
||||||
} |
|
||||||
export function checkCreateArtistRequest(req: any): boolean { |
|
||||||
return "body" in req && |
|
||||||
"name" in req.body; |
|
||||||
} |
|
||||||
|
|
||||||
// Modify an existing artist (PUT).
|
|
||||||
export const ModifyArtistEndpoint = '/artist/:id'; |
|
||||||
export interface ModifyArtistRequest { |
|
||||||
name?: string, |
|
||||||
tagIds?: number[]; |
|
||||||
storeLinks?: string[], |
|
||||||
} |
|
||||||
export interface ModifyArtistResponse { } |
|
||||||
export function checkModifyArtistRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new tag (POST).
|
|
||||||
export const CreateTagEndpoint = '/tag'; |
|
||||||
export interface CreateTagRequest { |
|
||||||
name: string; |
|
||||||
parentId?: number; |
|
||||||
} |
|
||||||
export interface CreateTagResponse { |
|
||||||
id: number; |
|
||||||
} |
|
||||||
export function checkCreateTagRequest(req: any): boolean { |
|
||||||
return "body" in req && |
|
||||||
"name" in req.body; |
|
||||||
} |
|
||||||
|
|
||||||
// Modify an existing tag (PUT).
|
|
||||||
export const ModifyTagEndpoint = '/tag/:id'; |
|
||||||
export interface ModifyTagRequest { |
|
||||||
name?: string, |
|
||||||
parentId?: number; |
|
||||||
} |
|
||||||
export interface ModifyTagResponse { } |
|
||||||
export function checkModifyTagRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Get tag details (GET).
|
|
||||||
export const TagDetailsEndpoint = '/tag/:id'; |
|
||||||
export interface TagDetailsRequest { } |
|
||||||
export interface TagDetailsResponse { |
|
||||||
name: string, |
|
||||||
parentId?: number, |
|
||||||
} |
|
||||||
export function checkTagDetailsRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Delete tag (DELETE).
|
|
||||||
export const DeleteTagEndpoint = '/tag/:id'; |
|
||||||
export interface DeleteTagRequest { } |
|
||||||
export interface DeleteTagResponse { } |
|
||||||
export function checkDeleteTagRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Merge tag (POST).
|
|
||||||
export const MergeTagEndpoint = '/tag/:id/merge/:toId'; |
|
||||||
export interface MergeTagRequest { } |
|
||||||
export interface MergeTagResponse { } |
|
||||||
export function checkMergeTagRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Register a user (POST).
|
|
||||||
// TODO: add e-mail verification.
|
|
||||||
export const RegisterUserEndpoint = '/register'; |
|
||||||
export interface RegisterUserRequest { |
|
||||||
email: string, |
|
||||||
password: string, |
|
||||||
} |
|
||||||
export interface RegisterUserResponse { } |
|
||||||
export function checkPassword(password: string): boolean { |
|
||||||
const result = (password.length < 32) && |
|
||||||
(password.length >= 8) && |
|
||||||
password.split("").every(char => char.charCodeAt(0) <= 127) && // is ASCII
|
|
||||||
(/[a-z]/g.test(password)) && // has lowercase
|
|
||||||
(/[A-Z]/g.test(password)) && // has uppercase
|
|
||||||
(/[0-9]/g.test(password)) && // has number
|
|
||||||
(/[!@#$%^&*()_+/]/g.test(password)) // has special character;
|
|
||||||
|
|
||||||
console.log("Password check for ", password, ": ", result); |
|
||||||
return result; |
|
||||||
} |
|
||||||
export function checkEmail(email: string): boolean { |
|
||||||
const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; |
|
||||||
const result = re.test(String(email).toLowerCase()); |
|
||||||
console.log("Email check for ", email, ": ", result); |
|
||||||
return result; |
|
||||||
} |
|
||||||
export function checkRegisterUserRequest(req: any): boolean { |
|
||||||
return "body" in req && |
|
||||||
"email" in req.body && |
|
||||||
"password" in req.body && |
|
||||||
checkEmail(req.body.email) && |
|
||||||
checkPassword(req.body.password); |
|
||||||
} |
|
||||||
|
|
||||||
// Note: Login is handled by Passport.js, so it is not explicitly written here.
|
|
||||||
export const LoginEndpoint = "/login"; |
|
||||||
export const LogoutEndpoint = "/logout"; |
|
||||||
|
|
||||||
export enum IntegrationType { |
|
||||||
SpotifyClientCredentials = "SpotifyClientCredentials", |
|
||||||
YoutubeWebScraper = "YoutubeWebScraper", |
|
||||||
} |
|
||||||
|
|
||||||
export enum ExternalStore { |
|
||||||
GooglePlayMusic = "Google Play Music", |
|
||||||
Spotify = "Spotify", |
|
||||||
YoutubeMusic = "Youtube Music", |
|
||||||
} |
|
||||||
|
|
||||||
// Links to external stores are identified by their domain or some
|
|
||||||
// other unique substring. These unique substrings are stored here.
|
|
||||||
export const StoreURLIdentifiers: Record<ExternalStore, string> = { |
|
||||||
[ExternalStore.GooglePlayMusic]: 'play.google.com', |
|
||||||
[ExternalStore.Spotify]: 'spotify.com', |
|
||||||
[ExternalStore.YoutubeMusic]: 'music.youtube.com', |
|
||||||
} |
|
||||||
|
|
||||||
export const IntegrationStores: Record<IntegrationType, ExternalStore> = { |
|
||||||
[IntegrationType.SpotifyClientCredentials]: ExternalStore.Spotify, |
|
||||||
[IntegrationType.YoutubeWebScraper]: ExternalStore.YoutubeMusic, |
|
||||||
} |
|
||||||
|
|
||||||
export interface SpotifyClientCredentialsDetails { |
|
||||||
clientId: string, |
|
||||||
} |
|
||||||
export interface SpotifyClientCredentialsSecretDetails { |
|
||||||
clientSecret: string, |
|
||||||
} |
|
||||||
|
|
||||||
export interface YoutubeMusicWebScraperDetails {} |
|
||||||
export interface YoutubeMusicWebScraperSecretDetails {} |
|
||||||
|
|
||||||
export type IntegrationDetails = SpotifyClientCredentialsDetails | YoutubeMusicWebScraperDetails; |
|
||||||
export type IntegrationSecretDetails = SpotifyClientCredentialsSecretDetails | YoutubeMusicWebScraperSecretDetails; |
|
||||||
|
|
||||||
// Create a new integration (POST).
|
|
||||||
export const CreateIntegrationEndpoint = '/integration'; |
|
||||||
export interface CreateIntegrationRequest { |
|
||||||
name: string, |
|
||||||
type: IntegrationType, |
|
||||||
details: IntegrationDetails, |
|
||||||
secretDetails: IntegrationSecretDetails, |
|
||||||
} |
|
||||||
export interface CreateIntegrationResponse { |
|
||||||
id: number; |
|
||||||
} |
|
||||||
export function checkCreateIntegrationRequest(req: any): boolean { |
|
||||||
return "body" in req && |
|
||||||
"name" in req.body && |
|
||||||
"type" in req.body && |
|
||||||
"details" in req.body && |
|
||||||
"secretDetails" in req.body && |
|
||||||
(req.body.type in IntegrationType); |
|
||||||
} |
|
||||||
|
|
||||||
// Modify an existing integration (PUT).
|
|
||||||
export const ModifyIntegrationEndpoint = '/integration/:id'; |
|
||||||
export interface ModifyIntegrationRequest { |
|
||||||
name?: string, |
|
||||||
type?: IntegrationType, |
|
||||||
details?: IntegrationDetails, |
|
||||||
secretDetails?: IntegrationSecretDetails, |
|
||||||
} |
|
||||||
export interface ModifyIntegrationResponse { } |
|
||||||
export function checkModifyIntegrationRequest(req: any): boolean { |
|
||||||
if("type" in req.body && !(req.body.type in IntegrationType)) return false; |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Get integration details (GET).
|
|
||||||
export const IntegrationDetailsEndpoint = '/integration/:id'; |
|
||||||
export interface IntegrationDetailsRequest { } |
|
||||||
export interface IntegrationDetailsResponse { |
|
||||||
name: string, |
|
||||||
type: IntegrationType, |
|
||||||
details: IntegrationDetails, |
|
||||||
} |
|
||||||
export function checkIntegrationDetailsRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// List integrations (GET).
|
|
||||||
export const ListIntegrationsEndpoint = '/integration'; |
|
||||||
export interface ListIntegrationsRequest { } |
|
||||||
export interface ListIntegrationsItem extends IntegrationDetailsResponse { id: number } |
|
||||||
export type ListIntegrationsResponse = ListIntegrationsItem[]; |
|
||||||
export function checkListIntegrationsRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Delete integration (DELETE).
|
|
||||||
export const DeleteIntegrationEndpoint = '/integration/:id'; |
|
||||||
export interface DeleteIntegrationRequest { } |
|
||||||
export interface DeleteIntegrationResponse { } |
|
||||||
export function checkDeleteIntegrationRequest(req: any): boolean { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
export interface ArtistDetailsResponseWithId extends ArtistDetailsResponse { id: number } |
|
||||||
export interface AlbumDetailsResponseWithId extends AlbumDetailsResponse { id: number } |
|
||||||
export interface TagDetailsResponseWithId extends TagDetailsResponse { id: number } |
|
||||||
export interface SongDetailsResponseWithId extends SongDetailsResponse { id: number } |
|
||||||
@ -0,0 +1,13 @@ |
|||||||
|
// TODO: this file is located in the front-end src folder because
|
||||||
|
// otherwise, Create React App will refuse to compile it.
|
||||||
|
// Putting it in the server folder or in its own folder makes more sense.
|
||||||
|
|
||||||
|
// This file represents the API interface for Mudbase's back-end.
|
||||||
|
// Each endpoint is described by its endpoint address,
|
||||||
|
// a request structure, a response structure and
|
||||||
|
// a checking function which determines request validity.
|
||||||
|
|
||||||
|
export * from './types/resources'; |
||||||
|
export * from './endpoints/auth'; |
||||||
|
export * from './endpoints/resources'; |
||||||
|
export * from './endpoints/query'; |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
// Any endpoints which have to do with authentication, registration, e.d.
|
||||||
|
|
||||||
|
import { User } from "../types/resources"; |
||||||
|
|
||||||
|
// Register a user (POST).
|
||||||
|
// TODO: add e-mail verification.
|
||||||
|
// TODO: add descriptive reason for failure.
|
||||||
|
export const RegisterUserEndpoint = '/register'; |
||||||
|
export type RegisterUserRequest = User; |
||||||
|
export interface RegisterUserResponse { } |
||||||
|
|
||||||
|
export function checkPassword(password: string): boolean { |
||||||
|
const result = (password.length < 32) && |
||||||
|
(password.length >= 8) && |
||||||
|
password.split("").every(char => char.charCodeAt(0) <= 127) && // is ASCII
|
||||||
|
(/[a-z]/g.test(password)) && // has lowercase
|
||||||
|
(/[A-Z]/g.test(password)) && // has uppercase
|
||||||
|
(/[0-9]/g.test(password)) && // has number
|
||||||
|
(/[!@#$%^&*()_+/]/g.test(password)) // has special character;
|
||||||
|
|
||||||
|
console.log("Password check for ", password, ": ", result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
export function checkEmail(email: string): boolean { |
||||||
|
const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; |
||||||
|
const result = re.test(String(email).toLowerCase()); |
||||||
|
console.log("Email check for ", email, ": ", result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
export function checkRegisterUserRequest(req: any): boolean { |
||||||
|
return "body" in req && |
||||||
|
"email" in req.body && |
||||||
|
"password" in req.body && |
||||||
|
checkEmail(req.body.email) && |
||||||
|
checkPassword(req.body.password); |
||||||
|
} |
||||||
|
|
||||||
|
// Note: Login is handled by Passport.js, so it is not explicitly written here.
|
||||||
|
export const LoginEndpoint = "/login"; |
||||||
|
export const LogoutEndpoint = "/logout"; |
||||||
@ -0,0 +1,118 @@ |
|||||||
|
// Query for items (POST).
|
||||||
|
|
||||||
|
import { AlbumWithId, ArtistWithId, TagWithId, TrackWithId } from "../types/resources"; |
||||||
|
|
||||||
|
export const QueryEndpoint = '/query'; |
||||||
|
|
||||||
|
// Combinational query operations
|
||||||
|
export enum QueryNodeOp { |
||||||
|
And = "AND", |
||||||
|
Or = "OR", |
||||||
|
Not = "NOT", |
||||||
|
} |
||||||
|
|
||||||
|
// Leaf (filter) query operations
|
||||||
|
export enum QueryLeafOp { |
||||||
|
Eq = "EQ", |
||||||
|
Ne = "NE", |
||||||
|
In = "IN", |
||||||
|
NotIn = "NOTIN", |
||||||
|
Like = "LIKE", |
||||||
|
} |
||||||
|
|
||||||
|
// Resource properties that can be queried on
|
||||||
|
export enum QueryElemProperty { |
||||||
|
trackName = "trackName", |
||||||
|
trackId = "trackId", |
||||||
|
artistName = "artistName", |
||||||
|
artistId = "artistId", |
||||||
|
albumName = "albumName", |
||||||
|
albumId = "albumId", |
||||||
|
tagName = "tagName", |
||||||
|
tagId = "tagId", |
||||||
|
trackStoreLinks = "trackStoreLinks", //Note: treated as a JSON string for filter operations
|
||||||
|
artistStoreLinks = "artistStoreLinks", //Note: treated as a JSON string for filter operations
|
||||||
|
albumStoreLinks = "albumStoreLinks", //Note: treated as a JSON string for filter operations
|
||||||
|
} |
||||||
|
|
||||||
|
// Resource properties that can be ordered by
|
||||||
|
export enum OrderByType { |
||||||
|
Name = 'name', |
||||||
|
} |
||||||
|
|
||||||
|
// Levels of detail for the query response
|
||||||
|
export enum QueryResponseType { |
||||||
|
Details = 'details', // Returns detailed result items.
|
||||||
|
Ids = 'ids', // Returns IDs only.
|
||||||
|
Count = 'count', // Returns an item count only.
|
||||||
|
} |
||||||
|
|
||||||
|
// A single query element (can be node or leaf)
|
||||||
|
export interface QueryElem { |
||||||
|
// Leaf
|
||||||
|
prop?: QueryElemProperty, |
||||||
|
propOperand?: any, |
||||||
|
propOperator?: QueryLeafOp, |
||||||
|
|
||||||
|
// Node
|
||||||
|
children?: QueryElem[] |
||||||
|
childrenOperator?: QueryNodeOp, |
||||||
|
} |
||||||
|
|
||||||
|
// An ordering specification for a query.
|
||||||
|
export interface Ordering { |
||||||
|
orderBy: { |
||||||
|
type: OrderByType, |
||||||
|
} |
||||||
|
ascending: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
// Query request structure
|
||||||
|
export interface Query extends QueryElem { } |
||||||
|
export interface QueryRequest { |
||||||
|
query: Query, |
||||||
|
offsetsLimits: OffsetsLimits, |
||||||
|
ordering: Ordering, |
||||||
|
responseType: QueryResponseType |
||||||
|
} |
||||||
|
|
||||||
|
// Query response structure
|
||||||
|
export interface QueryResponse { |
||||||
|
tracks: TrackWithId[] | number[] | number, // Details | IDs | count, depending on QueryResponseType
|
||||||
|
artists: ArtistWithId[] | number[] | number, |
||||||
|
tags: TagWithId[] | number[] | number, |
||||||
|
albums: AlbumWithId[] | number[] | number, |
||||||
|
} |
||||||
|
|
||||||
|
// Note: use -1 as an infinity limit.
|
||||||
|
export interface OffsetsLimits { |
||||||
|
trackOffset?: number, |
||||||
|
trackLimit?: number, |
||||||
|
artistOffset?: number, |
||||||
|
artistLimit?: number, |
||||||
|
tagOffset?: number, |
||||||
|
tagLimit?: number, |
||||||
|
albumOffset?: number, |
||||||
|
albumLimit?: number, |
||||||
|
} |
||||||
|
|
||||||
|
// Checking functions for query requests.
|
||||||
|
export function checkQueryElem(elem: any): boolean { |
||||||
|
if (elem.childrenOperator && elem.children) { |
||||||
|
elem.children.forEach((child: any) => { |
||||||
|
if (!checkQueryElem(child)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
return (elem.childrenOperator && elem.children) || |
||||||
|
("prop" in elem && "propOperand" in elem && "propOperator" in elem) || |
||||||
|
Object.keys(elem).length === 0; |
||||||
|
} |
||||||
|
export function checkQueryRequest(req: any): boolean { |
||||||
|
return 'query' in req |
||||||
|
&& 'offsetsLimits' in req |
||||||
|
&& 'ordering' in req |
||||||
|
&& 'responseType' in req |
||||||
|
&& checkQueryElem(req.query); |
||||||
|
} |
||||||
@ -0,0 +1,186 @@ |
|||||||
|
import { |
||||||
|
Album, |
||||||
|
AlbumBaseWithRefs, |
||||||
|
AlbumWithRefs, |
||||||
|
Artist, |
||||||
|
ArtistBaseWithRefs, |
||||||
|
ArtistWithRefs, |
||||||
|
IntegrationData, |
||||||
|
IntegrationDataWithId, |
||||||
|
IntegrationDataWithSecret, |
||||||
|
isAlbumBaseWithRefs, |
||||||
|
isAlbumWithRefs, |
||||||
|
isArtistBaseWithRefs, |
||||||
|
isArtistWithRefs, |
||||||
|
isIntegrationData, |
||||||
|
isPartialIntegrationData, |
||||||
|
isTagBaseWithRefs, |
||||||
|
isTagWithRefs, |
||||||
|
isTrackBaseWithRefs, |
||||||
|
isTrackWithRefs, |
||||||
|
PartialIntegrationData, |
||||||
|
Tag, |
||||||
|
TagBaseWithRefs, |
||||||
|
TagWithRefs, |
||||||
|
TrackBaseWithRefs, |
||||||
|
TrackWithDetails, |
||||||
|
TrackWithRefs |
||||||
|
} from "../types/resources"; |
||||||
|
|
||||||
|
// The API supports RESTful access to single API resources:
|
||||||
|
// - GET for retrieving details about a single item
|
||||||
|
// - PUT for replacing a single item
|
||||||
|
// - POST for creating a new item
|
||||||
|
// - PATCH for modifying a single item
|
||||||
|
// - DELETE for deleting a single item
|
||||||
|
//
|
||||||
|
// The above are implemented for:
|
||||||
|
// - tracks
|
||||||
|
// - artists
|
||||||
|
// - albums
|
||||||
|
// - tags
|
||||||
|
// - integrations
|
||||||
|
//
|
||||||
|
// The following special requests exist in addition:
|
||||||
|
// - Merge a tag into another using a POST
|
||||||
|
// - List all integrations using a GET
|
||||||
|
|
||||||
|
// Get track details (GET).
|
||||||
|
export const GetTrackEndpoint = '/track/:id'; |
||||||
|
export type GetTrackResponse = TrackWithDetails; |
||||||
|
|
||||||
|
// Get artist details (GET).
|
||||||
|
export const GetArtistEndpoint = '/artist/:id'; |
||||||
|
export type GetArtistResponse = Artist; |
||||||
|
|
||||||
|
// Get album details (GET).
|
||||||
|
export const GetAlbumEndpoint = "/album/:id"; |
||||||
|
export type GetAlbumResponse = Album; |
||||||
|
|
||||||
|
// Get tag details (GET).
|
||||||
|
export const GetTagEndpoint = "/tag/:id"; |
||||||
|
export type GetTagResponse = Tag; |
||||||
|
|
||||||
|
// Get integration details (GET).
|
||||||
|
export const GetIntegrationEndpoint = "/integration/:id"; |
||||||
|
export type GetIntegrationResponse = IntegrationData; |
||||||
|
|
||||||
|
// Post new track (POST).
|
||||||
|
export const PostTrackEndpoint = "/track"; |
||||||
|
export type PostTrackRequest = TrackWithRefs; |
||||||
|
export interface PostTrackResponse { id: number }; |
||||||
|
export const checkPostTrackRequest: (v: any) => boolean = isTrackWithRefs; |
||||||
|
|
||||||
|
// Post new artist (POST).
|
||||||
|
export const PostArtistEndpoint = "/artist"; |
||||||
|
export type PostArtistRequest = ArtistWithRefs; |
||||||
|
export interface PostArtistResponse { id: number }; |
||||||
|
export const checkPostArtistRequest: (v: any) => boolean = isArtistWithRefs; |
||||||
|
|
||||||
|
// Post new album (POST).
|
||||||
|
export const PostAlbumEndpoint = "/album"; |
||||||
|
export type PostAlbumRequest = AlbumWithRefs; |
||||||
|
export interface PostAlbumResponse { id: number }; |
||||||
|
export const checkPostAlbumRequest: (v: any) => boolean = isAlbumWithRefs; |
||||||
|
|
||||||
|
// Post new tag (POST).
|
||||||
|
export const PostTagEndpoint = "/tag"; |
||||||
|
export type PostTagRequest = TagWithRefs; |
||||||
|
export interface PostTagResponse { id: number }; |
||||||
|
export const checkPostTagRequest: (v: any) => boolean = isTagWithRefs; |
||||||
|
|
||||||
|
// Post new integration (POST).
|
||||||
|
export const PostIntegrationEndpoint = "/integration"; |
||||||
|
export type PostIntegrationRequest = IntegrationDataWithSecret; |
||||||
|
export interface PostIntegrationResponse { id: number }; |
||||||
|
export const checkPostIntegrationRequest: (v: any) => boolean = isIntegrationData; |
||||||
|
|
||||||
|
// Replace track (PUT).
|
||||||
|
export const PutTrackEndpoint = "/track/:id"; |
||||||
|
export type PutTrackRequest = TrackWithRefs; |
||||||
|
export type PutTrackResponse = void; |
||||||
|
export const checkPutTrackRequest: (v: any) => boolean = isTrackWithRefs; |
||||||
|
|
||||||
|
// Replace artist (PUT).
|
||||||
|
export const PutArtistEndpoint = "/artist/:id"; |
||||||
|
export type PutArtistRequest = ArtistWithRefs; |
||||||
|
export type PutArtistResponse = void; |
||||||
|
export const checkPutArtistRequest: (v: any) => boolean = isArtistWithRefs; |
||||||
|
|
||||||
|
// Replace album (PUT).
|
||||||
|
export const PutAlbumEndpoint = "/album/:id"; |
||||||
|
export type PutAlbumRequest = AlbumWithRefs; |
||||||
|
export type PutAlbumResponse = void; |
||||||
|
export const checkPutAlbumRequest: (v: any) => boolean = isAlbumWithRefs; |
||||||
|
|
||||||
|
// Replace tag (PUT).
|
||||||
|
export const PutTagEndpoint = "/tag/:id"; |
||||||
|
export type PutTagRequest = TagWithRefs; |
||||||
|
export type PutTagResponse = void; |
||||||
|
export const checkPutTagRequest: (v: any) => boolean = isTagWithRefs; |
||||||
|
|
||||||
|
// Replace integration (PUT).
|
||||||
|
export const PutIntegrationEndpoint = "/integration/:id"; |
||||||
|
export type PutIntegrationRequest = IntegrationDataWithSecret; |
||||||
|
export type PutIntegrationResponse = void; |
||||||
|
export const checkPutIntegrationRequest: (v: any) => boolean = isIntegrationData; |
||||||
|
|
||||||
|
// Modify track (PATCH).
|
||||||
|
export const PatchTrackEndpoint = "/track/:id"; |
||||||
|
export type PatchTrackRequest = TrackBaseWithRefs; |
||||||
|
export type PatchTrackResponse = void; |
||||||
|
export const checkPatchTrackRequest: (v: any) => boolean = isTrackBaseWithRefs; |
||||||
|
|
||||||
|
// Modify artist (PATCH).
|
||||||
|
export const PatchArtistEndpoint = "/artist/:id"; |
||||||
|
export type PatchArtistRequest = ArtistBaseWithRefs; |
||||||
|
export type PatchArtistResponse = void; |
||||||
|
export const checkPatchArtistRequest: (v: any) => boolean = isArtistBaseWithRefs; |
||||||
|
|
||||||
|
// Modify album (PATCH).
|
||||||
|
export const PatchAlbumEndpoint = "/album/:id"; |
||||||
|
export type PatchAlbumRequest = AlbumBaseWithRefs; |
||||||
|
export type PatchAlbumResponse = void; |
||||||
|
export const checkPatchAlbumRequest: (v: any) => boolean = isAlbumBaseWithRefs; |
||||||
|
|
||||||
|
// Modify tag (PATCH).
|
||||||
|
export const PatchTagEndpoint = "/tag/:id"; |
||||||
|
export type PatchTagRequest = TagBaseWithRefs; |
||||||
|
export type PatchTagResponse = void; |
||||||
|
export const checkPatchTagRequest: (v: any) => boolean = isTagBaseWithRefs; |
||||||
|
|
||||||
|
// Modify integration (PATCH).
|
||||||
|
export const PatchIntegrationEndpoint = "/integration/:id"; |
||||||
|
export type PatchIntegrationRequest = PartialIntegrationData; |
||||||
|
export type PatchIntegrationResponse = void; |
||||||
|
export const checkPatchIntegrationRequest: (v: any) => boolean = isPartialIntegrationData; |
||||||
|
|
||||||
|
// DELETE track.
|
||||||
|
export const DeleteTrackEndpoint = '/track/:id'; |
||||||
|
export type DeleteTrackResponse = void |
||||||
|
|
||||||
|
// DELETE artist.
|
||||||
|
export const DeleteArtistEndpoint = '/artist/:id'; |
||||||
|
export type DeleteArtistResponse = void |
||||||
|
|
||||||
|
// DELETE album.
|
||||||
|
export const DeleteAlbumEndpoint = "/album/:id"; |
||||||
|
export type DeleteAlbumResponse = void |
||||||
|
|
||||||
|
// DELETE tag.
|
||||||
|
export const DeleteTagEndpoint = "/tag/:id"; |
||||||
|
export type DeleteTagResponse = void |
||||||
|
|
||||||
|
// DELETE integration.
|
||||||
|
export const DeleteIntegrationEndpoint = "/integration/:id"; |
||||||
|
export type DeleteIntegrationResponse = void |
||||||
|
|
||||||
|
// List integrations (GET).
|
||||||
|
export const ListIntegrationsEndpoint = "/integration"; |
||||||
|
export type ListIntegrationsResponse = IntegrationDataWithId[]; |
||||||
|
|
||||||
|
// Merge tag (POST).
|
||||||
|
// This will tag any items which are tagged by :id
|
||||||
|
// with :toId instead, and then delete tag :id.
|
||||||
|
export const MergeTagEndpoint = '/tag/:id/merge/:toId'; |
||||||
|
export type MergeTagResponse = void; |
||||||
@ -0,0 +1,278 @@ |
|||||||
|
// Enumerates the different kinds of resources dealt with by the API.
|
||||||
|
export enum ResourceType { |
||||||
|
Track = "track", |
||||||
|
Artist = "artist", |
||||||
|
Album = "album", |
||||||
|
Tag = "tag" |
||||||
|
} |
||||||
|
|
||||||
|
export interface TrackBase { |
||||||
|
mbApi_typename: "track", |
||||||
|
|
||||||
|
name?: string, |
||||||
|
storeLinks?: string[], |
||||||
|
albumId?: number | null, |
||||||
|
} |
||||||
|
export interface TrackBaseWithRefs extends TrackBase { |
||||||
|
artistIds?: number[], |
||||||
|
albumId?: number | null, |
||||||
|
tagIds?: number[], |
||||||
|
} |
||||||
|
export interface TrackBaseWithDetails extends TrackBase { |
||||||
|
artists: ArtistWithId[], |
||||||
|
album: AlbumWithId | null, |
||||||
|
tags: TagWithId[], |
||||||
|
} |
||||||
|
export interface TrackWithDetails extends TrackBaseWithDetails { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface TrackWithRefs extends TrackBaseWithRefs { |
||||||
|
name: string, |
||||||
|
artistIds: number[], |
||||||
|
albumId: number | null, |
||||||
|
tagIds: number[], |
||||||
|
} |
||||||
|
export interface Track extends TrackBase { |
||||||
|
name: string, |
||||||
|
album: AlbumWithId | null, |
||||||
|
} |
||||||
|
export interface TrackWithRefsWithId extends TrackWithRefs { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface TrackWithDetailsWithId extends TrackWithDetails { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface TrackWithId extends Track { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export function isTrackBase(q: any): q is TrackBase { |
||||||
|
return q.mbApi_typename && q.mbApi_typename === "track"; |
||||||
|
} |
||||||
|
export function isTrackBaseWithRefs(q: any): q is TrackBaseWithRefs { |
||||||
|
return isTrackBase(q) && "artistIds" in q && "tagIds" in q && "albumId" in q; |
||||||
|
} |
||||||
|
export function isTrackWithRefs(q: any): q is TrackWithRefs { |
||||||
|
return isTrackBaseWithRefs(q) && "name" in q; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export interface ArtistBase { |
||||||
|
mbApi_typename: "artist", |
||||||
|
|
||||||
|
name?: string, |
||||||
|
storeLinks?: string[], |
||||||
|
} |
||||||
|
export interface ArtistBaseWithRefs extends ArtistBase { |
||||||
|
albumIds?: number[], |
||||||
|
tagIds?: number[], |
||||||
|
} |
||||||
|
export interface ArtistBaseWithDetails extends ArtistBase { |
||||||
|
albums: AlbumWithId[], |
||||||
|
tags: TagWithId[], |
||||||
|
} |
||||||
|
export interface ArtistWithDetails extends ArtistBaseWithDetails { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface ArtistWithRefs extends ArtistBaseWithRefs { |
||||||
|
name: string, |
||||||
|
albumIds: number[], |
||||||
|
tagIds: number[], |
||||||
|
} |
||||||
|
export interface Artist extends ArtistBase { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface ArtistWithRefsWithId extends ArtistWithRefs { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface ArtistWithDetailsWithId extends ArtistWithDetails { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface ArtistWithId extends Artist { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export function isArtistBase(q: any): q is ArtistBase { |
||||||
|
return q.mbApi_typename && q.mbApi_typename === "artist"; |
||||||
|
} |
||||||
|
export function isArtistBaseWithRefs(q: any): q is ArtistBaseWithRefs { |
||||||
|
return isArtistBase(q) && q && "tagIds" in q && "albumIds" in q; |
||||||
|
} |
||||||
|
export function isArtistWithRefs(q: any): q is ArtistWithRefs { |
||||||
|
return isTrackBaseWithRefs(q) && "name" in q; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export interface AlbumBase { |
||||||
|
mbApi_typename: "album", |
||||||
|
|
||||||
|
name?: string, |
||||||
|
storeLinks?: string[], |
||||||
|
} |
||||||
|
export interface AlbumBaseWithRefs extends AlbumBase { |
||||||
|
artistIds?: number[], |
||||||
|
trackIds?: number[], |
||||||
|
tagIds?: number[], |
||||||
|
} |
||||||
|
export interface AlbumBaseWithDetails extends AlbumBase { |
||||||
|
artists: ArtistWithId[], |
||||||
|
tracks: TrackWithId[], |
||||||
|
tags: TagWithId[], |
||||||
|
} |
||||||
|
export interface AlbumWithDetails extends AlbumBaseWithDetails { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface AlbumWithRefs extends AlbumBaseWithRefs { |
||||||
|
name: string, |
||||||
|
artistIds: number[], |
||||||
|
trackIds: number[], |
||||||
|
tagIds: number[], |
||||||
|
} |
||||||
|
export interface Album extends AlbumBase { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface AlbumWithRefsWithId extends AlbumWithRefs { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface AlbumWithDetailsWithId extends AlbumWithDetails { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface AlbumWithId extends Album { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export function isAlbumBase(q: any): q is AlbumBase { |
||||||
|
return q.mbApi_typename && q.mbApi_typename === "album"; |
||||||
|
} |
||||||
|
export function isAlbumBaseWithRefs(q: any): q is AlbumBaseWithRefs { |
||||||
|
return isAlbumBase(q) && "artistIds" in q && "trackIds" in q && "tagIds" in q; |
||||||
|
} |
||||||
|
export function isAlbumWithRefs(q: any): q is AlbumWithRefs { |
||||||
|
return isAlbumBaseWithRefs(q) && "name" in q; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export interface TagBase { |
||||||
|
mbApi_typename: "tag", |
||||||
|
|
||||||
|
name?: string, |
||||||
|
} |
||||||
|
export interface TagBaseWithRefs extends TagBase { |
||||||
|
parentId?: number | null, |
||||||
|
} |
||||||
|
export interface TagBaseWithDetails extends TagBase { |
||||||
|
parent?: TagWithId | null, |
||||||
|
} |
||||||
|
export interface TagWithDetails extends TagBaseWithDetails { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface TagWithRefs extends TagBaseWithRefs { |
||||||
|
name: string, |
||||||
|
parentId: number | null, |
||||||
|
} |
||||||
|
export interface Tag extends TagBase { |
||||||
|
name: string, |
||||||
|
} |
||||||
|
export interface TagWithRefsWithId extends TagWithRefs { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface TagWithDetailsWithId extends TagWithDetails { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface TagWithId extends Tag { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export function isTagBase(q: any): q is TagBase { |
||||||
|
return q.mbApi_typename && q.mbApi_typename === "tag"; |
||||||
|
} |
||||||
|
export function isTagBaseWithRefs(q: any): q is TagBaseWithRefs { |
||||||
|
return isTagBase(q) && "parentId" in q; |
||||||
|
} |
||||||
|
export function isTagWithRefs(q: any): q is TagWithRefs { |
||||||
|
return isTagBaseWithRefs(q) && "name" in q; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// There are several implemented integration solutions,
|
||||||
|
// enumerated here.
|
||||||
|
export enum IntegrationImpl { |
||||||
|
SpotifyClientCredentials = "SpotifyClientCredentials", |
||||||
|
YoutubeWebScraper = "YoutubeWebScraper", |
||||||
|
} |
||||||
|
|
||||||
|
// External domains to integrate with are enumerated here.
|
||||||
|
export enum IntegrationWith { |
||||||
|
GooglePlayMusic = "Google Play Music", |
||||||
|
Spotify = "Spotify", |
||||||
|
YoutubeMusic = "Youtube Music", |
||||||
|
} |
||||||
|
|
||||||
|
// Links to integrated domains are identified by their domain or some
|
||||||
|
// other unique substring. These unique substrings are stored here.
|
||||||
|
export const IntegrationUrls: Record<IntegrationWith, string> = { |
||||||
|
[IntegrationWith.GooglePlayMusic]: 'play.google.com', |
||||||
|
[IntegrationWith.Spotify]: 'spotify.com', |
||||||
|
[IntegrationWith.YoutubeMusic]: 'music.youtube.com', |
||||||
|
} |
||||||
|
|
||||||
|
// Mapping: which domain does each implementation integrate with?
|
||||||
|
export const ImplIntegratesWith: Record<IntegrationImpl, IntegrationWith> = { |
||||||
|
[IntegrationImpl.SpotifyClientCredentials]: IntegrationWith.Spotify, |
||||||
|
[IntegrationImpl.YoutubeWebScraper]: IntegrationWith.YoutubeMusic, |
||||||
|
} |
||||||
|
|
||||||
|
// Data used for the Spotify Client Credentials implementation.
|
||||||
|
export interface SpotifyClientCredentialsDetails { |
||||||
|
clientId: string, |
||||||
|
} |
||||||
|
export interface SpotifyClientCredentialsSecretDetails { |
||||||
|
clientSecret: string, |
||||||
|
} |
||||||
|
|
||||||
|
// Data used for the Youtube Music Web Scraper implementation.
|
||||||
|
export interface YoutubeMusicWebScraperDetails { } |
||||||
|
export interface YoutubeMusicWebScraperSecretDetails { } |
||||||
|
|
||||||
|
export type IntegrationDetails = |
||||||
|
SpotifyClientCredentialsDetails | |
||||||
|
YoutubeMusicWebScraperDetails; |
||||||
|
export type IntegrationSecretDetails = |
||||||
|
SpotifyClientCredentialsSecretDetails | |
||||||
|
YoutubeMusicWebScraperSecretDetails; |
||||||
|
|
||||||
|
// Integration resource.
|
||||||
|
export interface PartialIntegrationData { |
||||||
|
mbApi_typename: "integrationData", |
||||||
|
|
||||||
|
name?: string, // Identifies this instance in the UI
|
||||||
|
type?: IntegrationImpl, |
||||||
|
details?: IntegrationDetails, // Any data needed to operate the integration.
|
||||||
|
secretDetails?: IntegrationSecretDetails, // Any data needed to only be stored in the back-end, to operate the integration.
|
||||||
|
} |
||||||
|
export interface IntegrationData extends PartialIntegrationData { |
||||||
|
name: string, |
||||||
|
type: IntegrationImpl, |
||||||
|
details: IntegrationDetails, |
||||||
|
} |
||||||
|
export interface IntegrationDataWithId extends IntegrationData { |
||||||
|
id: number, |
||||||
|
} |
||||||
|
export interface IntegrationDataWithSecret extends IntegrationData { |
||||||
|
secretDetails: IntegrationSecretDetails, |
||||||
|
} |
||||||
|
export function isPartialIntegrationData(q: any): q is PartialIntegrationData { |
||||||
|
return q.mbApi_typename && q.mbApi_typename === "integrationData"; |
||||||
|
} |
||||||
|
export function isIntegrationData(q: any): q is IntegrationData { |
||||||
|
return isPartialIntegrationData(q) && |
||||||
|
"name" in q && |
||||||
|
"type" in q && |
||||||
|
"details" in q; |
||||||
|
} |
||||||
|
export function isIntegrationDataWithSecret(q: any): q is IntegrationDataWithSecret { |
||||||
|
return isIntegrationData(q) && "secretDetails" in q; |
||||||
|
} |
||||||
|
|
||||||
|
// User resource.
|
||||||
|
export interface User { |
||||||
|
mbApi_typename: "user", |
||||||
|
email: string, |
||||||
|
password: string, |
||||||
|
} |
||||||
@ -1,10 +0,0 @@ |
|||||||
import * as serverApi from '../../api'; |
|
||||||
import backendRequest from './request'; |
|
||||||
|
|
||||||
export async function getSong(id: number) { |
|
||||||
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.SongDetailsEndpoint.replace(':id', `${id}`)) |
|
||||||
if (!response.ok) { |
|
||||||
throw new Error("Response to song request not OK: " + JSON.stringify(response)); |
|
||||||
} |
|
||||||
return await response.json(); |
|
||||||
} |
|
||||||
@ -0,0 +1,10 @@ |
|||||||
|
import * as serverApi from '../../api/api'; |
||||||
|
import backendRequest from './request'; |
||||||
|
|
||||||
|
export async function getTrack(id: number): Promise<serverApi.GetTrackResponse> { |
||||||
|
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetTrackEndpoint.replace(':id', `${id}`)) |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Response to track request not OK: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
return await response.json(); |
||||||
|
} |
||||||
@ -1,28 +0,0 @@ |
|||||||
export const songGetters = { |
|
||||||
getTitle: (song: any) => song.title, |
|
||||||
getId: (song: any) => song.songId, |
|
||||||
getArtistNames: (song: any) => song.artists.map((a: any) => a.name), |
|
||||||
getArtistIds: (song: any) => song.artists.map((a: any) => a.artistId), |
|
||||||
getAlbumNames: (song: any) => song.albums.map((a: any) => a.name), |
|
||||||
getAlbumIds: (song: any) => song.albums.map((a: any) => a.albumId), |
|
||||||
getTagNames: (song: any) => { |
|
||||||
// Recursively resolve the name.
|
|
||||||
const resolveTag = (tag: any) => { |
|
||||||
var r = [tag.name]; |
|
||||||
if (tag.parent) { r.unshift(resolveTag(tag.parent)); } |
|
||||||
return r; |
|
||||||
} |
|
||||||
|
|
||||||
return song.tags.map((tag: any) => resolveTag(tag)); |
|
||||||
}, |
|
||||||
getTagIds: (song: any) => { |
|
||||||
// Recursively resolve the id.
|
|
||||||
const resolveTag = (tag: any) => { |
|
||||||
var r = [tag.tagId]; |
|
||||||
if (tag.parent) { r.unshift(resolveTag(tag.parent)); } |
|
||||||
return r; |
|
||||||
} |
|
||||||
|
|
||||||
return song.tags.map((tag: any) => resolveTag(tag)); |
|
||||||
}, |
|
||||||
} |
|
||||||
@ -0,0 +1,28 @@ |
|||||||
|
export const trackGetters = { |
||||||
|
getTitle: (track: any) => track.title, |
||||||
|
getId: (track: any) => track.trackId, |
||||||
|
getArtistNames: (track: any) => track.artists.map((a: any) => a.name), |
||||||
|
getArtistIds: (track: any) => track.artists.map((a: any) => a.artistId), |
||||||
|
getAlbumNames: (track: any) => track.albums.map((a: any) => a.name), |
||||||
|
getAlbumIds: (track: any) => track.albums.map((a: any) => a.albumId), |
||||||
|
getTagNames: (track: any) => { |
||||||
|
// Recursively resolve the name.
|
||||||
|
const resolveTag = (tag: any) => { |
||||||
|
var r = [tag.name]; |
||||||
|
if (tag.parent) { r.unshift(resolveTag(tag.parent)); } |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
return track.tags.map((tag: any) => resolveTag(tag)); |
||||||
|
}, |
||||||
|
getTagIds: (track: any) => { |
||||||
|
// Recursively resolve the id.
|
||||||
|
const resolveTag = (tag: any) => { |
||||||
|
var r = [tag.tagId]; |
||||||
|
if (tag.parent) { r.unshift(resolveTag(tag.parent)); } |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
return track.tags.map((tag: any) => resolveTag(tag)); |
||||||
|
}, |
||||||
|
} |
||||||
@ -0,0 +1,405 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import { AlbumBaseWithRefs, AlbumWithDetails, AlbumWithRefs } from "../../client/src/api/api"; |
||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import asJson from "../lib/asJson"; |
||||||
|
import { DBError, DBErrorKind } from "../endpoints/types"; |
||||||
|
|
||||||
|
// Returns an album with details, or null if not found.
|
||||||
|
export async function getAlbum(id: number, userId: number, knex: Knex): |
||||||
|
Promise<AlbumWithDetails> { |
||||||
|
// Start transfers for tracks, tags and artists.
|
||||||
|
// Also request the album itself.
|
||||||
|
const tagsPromise: Promise<api.TagWithId[]> = |
||||||
|
knex.select('tagId') |
||||||
|
.from('albums_tags') |
||||||
|
.where({ 'albumId': id }) |
||||||
|
.then((tags: any) => tags.map((tag: any) => tag['tagId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'parentId']) |
||||||
|
.from('tags') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const tracksPromise: Promise<api.TrackWithId[]> = |
||||||
|
knex.select('trackId') |
||||||
|
.from('tracks_albums') |
||||||
|
.where({ 'albumId': id }) |
||||||
|
.then((tracks: any) => tracks.map((track: any) => track['trackId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'storeLinks']) |
||||||
|
.from('tracks') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const artistsPromise: Promise<api.ArtistWithId[]> = |
||||||
|
knex.select('artistId') |
||||||
|
.from('artists_albums') |
||||||
|
.where({ 'albumId': id }) |
||||||
|
.then((artists: any) => artists.map((artist: any) => artist['artistId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'storeLinks']) |
||||||
|
.from('artists') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const albumPromise: Promise<api.Album | undefined> = |
||||||
|
knex.select('name', 'storeLinks') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: id }) |
||||||
|
.then((albums: any) => albums[0]); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
const [album, tags, tracks, artists] = |
||||||
|
await Promise.all([albumPromise, tagsPromise, tracksPromise, artistsPromise]); |
||||||
|
|
||||||
|
if (album) { |
||||||
|
return { |
||||||
|
mbApi_typename: 'album', |
||||||
|
name: album['name'], |
||||||
|
artists: artists as api.ArtistWithId[], |
||||||
|
tags: tags as api.TagWithId[], |
||||||
|
tracks: tracks as api.TrackWithId[], |
||||||
|
storeLinks: asJson(album['storeLinks'] || []), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the id of the created album.
|
||||||
|
export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Knex): Promise<number> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving artists.
|
||||||
|
const artistIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', album.artistIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Start retrieving tags.
|
||||||
|
const tagIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', album.tagIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Start retrieving tracks.
|
||||||
|
const trackIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('tracks') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', album.trackIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [artists, tags, tracks] = await Promise.all([artistIdsPromise, tagIdsPromise, trackIdsPromise]);; |
||||||
|
|
||||||
|
// Check that we found all artists and tags we need.
|
||||||
|
if ((new Set(artists.map((a: any) => a['id'])) !== new Set(album.artistIds)) || |
||||||
|
(new Set(tags.map((a: any) => a['id'])) !== new Set(album.tagIds)) || |
||||||
|
(new Set(tracks.map((a: any) => a['id'])) !== new Set(album.trackIds))) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Create the album.
|
||||||
|
const albumId = (await trx('albums') |
||||||
|
.insert({ |
||||||
|
name: album.name, |
||||||
|
storeLinks: JSON.stringify(album.storeLinks || []), |
||||||
|
user: userId, |
||||||
|
}) |
||||||
|
.returning('id') // Needed for Postgres
|
||||||
|
)[0]; |
||||||
|
|
||||||
|
// Link the artists via the linking table.
|
||||||
|
if (artists && artists.length) { |
||||||
|
await trx('artists_albums').insert( |
||||||
|
artists.map((artistId: number) => { |
||||||
|
return { |
||||||
|
artistId: artistId, |
||||||
|
albumId: albumId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// Link the tags via the linking table.
|
||||||
|
if (tags && tags.length) { |
||||||
|
await trx('albums_tags').insert( |
||||||
|
tags.map((tagId: number) => { |
||||||
|
return { |
||||||
|
albumId: albumId, |
||||||
|
tagId: tagId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// Link the tracks via the linking table.
|
||||||
|
if (tracks && tracks.length) { |
||||||
|
await trx('tracks_albums').insert( |
||||||
|
tracks.map((trackId: number) => { |
||||||
|
return { |
||||||
|
albumId: albumId, |
||||||
|
trackId: trackId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return albumId; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function modifyAlbum(userId: number, albumId: number, album: AlbumBaseWithRefs, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the album itself.
|
||||||
|
const albumIdPromise: Promise<number | undefined> = |
||||||
|
trx.select('id') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: albumId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
||||||
|
|
||||||
|
// Start retrieving artists if we are modifying those.
|
||||||
|
const artistIdsPromise: Promise<number[] | undefined> = |
||||||
|
album.artistIds ? |
||||||
|
trx.select('artistId') |
||||||
|
.from('artists_albums') |
||||||
|
.whereIn('artistId', album.artistIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['artistId'])) |
||||||
|
: (async () => undefined)(); |
||||||
|
|
||||||
|
// Start retrieving tracks if we are modifying those.
|
||||||
|
const trackIdsPromise: Promise<number[] | undefined> = |
||||||
|
album.trackIds ? |
||||||
|
trx.select('artistId') |
||||||
|
.from('tracks_albums') |
||||||
|
.whereIn('albumId', album.trackIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['trackId'])) |
||||||
|
: (async () => undefined)(); |
||||||
|
|
||||||
|
// Start retrieving tags if we are modifying those.
|
||||||
|
const tagIdsPromise = |
||||||
|
album.tagIds ? |
||||||
|
trx.select('id') |
||||||
|
.from('albums_tags') |
||||||
|
.whereIn('tagId', album.tagIds) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||||
|
(async () => undefined)(); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [oldAlbum, artists, tags, tracks] = await Promise.all([albumIdPromise, artistIdsPromise, tagIdsPromise, trackIdsPromise]);; |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if ((!artists || new Set(artists.map((a: any) => a['id'])) !== new Set(album.artistIds)) || |
||||||
|
(!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(album.tagIds)) || |
||||||
|
(!tracks || new Set(tracks.map((a: any) => a['id'])) !== new Set(album.trackIds)) || |
||||||
|
!oldAlbum) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Modify the album.
|
||||||
|
var update: any = {}; |
||||||
|
if ("name" in album) { update["name"] = album.name; } |
||||||
|
if ("storeLinks" in album) { update["storeLinks"] = JSON.stringify(album.storeLinks || []); } |
||||||
|
|
||||||
|
const modifyAlbumPromise = trx('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': albumId }) |
||||||
|
.update(update) |
||||||
|
|
||||||
|
// Remove unlinked artists.
|
||||||
|
const removeUnlinkedArtists = artists ? trx('artists_albums') |
||||||
|
.where({ 'albumId': albumId }) |
||||||
|
.whereNotIn('artistId', album.artistIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Remove unlinked tags.
|
||||||
|
const removeUnlinkedTags = tags ? trx('albums_tags') |
||||||
|
.where({ 'albumId': albumId }) |
||||||
|
.whereNotIn('tagId', album.tagIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Remove unlinked tracks.
|
||||||
|
const removeUnlinkedTracks = tracks ? trx('tracks_albums') |
||||||
|
.where({ 'albumId': albumId }) |
||||||
|
.whereNotIn('trackId', album.trackIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Link new artists.
|
||||||
|
const addArtists = artists ? trx('artists_albums') |
||||||
|
.where({ 'albumId': albumId }) |
||||||
|
.then((as: any) => as.map((a: any) => a['artistId'])) |
||||||
|
.then((doneArtistIds: number[]) => { |
||||||
|
// Get the set of artists that are not yet linked
|
||||||
|
const toLink = (artists || []).filter((id: number) => { |
||||||
|
return !doneArtistIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((artistId: number) => { |
||||||
|
return { |
||||||
|
artistId: artistId, |
||||||
|
albumId: albumId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('artists_albums').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Link new tracks.
|
||||||
|
const addTracks = tracks ? trx('tracks_albums') |
||||||
|
.where({ 'albumId': albumId }) |
||||||
|
.then((as: any) => as.map((a: any) => a['trackId'])) |
||||||
|
.then((doneTrackIds: number[]) => { |
||||||
|
// Get the set of artists that are not yet linked
|
||||||
|
const toLink = (tracks || []).filter((id: number) => { |
||||||
|
return !doneTrackIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((trackId: number) => { |
||||||
|
return { |
||||||
|
trackId: trackId, |
||||||
|
albumId: albumId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('tracks_albums').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Link new tags.
|
||||||
|
const addTags = tags ? trx('albums_tags') |
||||||
|
.where({ 'albumId': albumId }) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
||||||
|
.then((doneTagIds: number[]) => { |
||||||
|
// Get the set of tags that are not yet linked
|
||||||
|
const toLink = tags.filter((id: number) => { |
||||||
|
return !doneTagIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((tagId: number) => { |
||||||
|
return { |
||||||
|
tagId: tagId, |
||||||
|
albumId: albumId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('albums_tags').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Wait for all operations to finish.
|
||||||
|
await Promise.all([ |
||||||
|
modifyAlbumPromise, |
||||||
|
removeUnlinkedArtists, |
||||||
|
removeUnlinkedTags, |
||||||
|
removeUnlinkedTracks, |
||||||
|
addArtists, |
||||||
|
addTags, |
||||||
|
addTracks, |
||||||
|
]); |
||||||
|
|
||||||
|
return; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const e: DBError = { |
||||||
|
kind: DBErrorKind.Unknown, |
||||||
|
message: "Reached the unreachable.", |
||||||
|
name: "DBError" |
||||||
|
} |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteAlbum(userId: number, albumId: number, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start by retrieving the album itself for sanity.
|
||||||
|
const confirmAlbumId: number | undefined = |
||||||
|
await trx.select('id') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: albumId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
||||||
|
|
||||||
|
if (!confirmAlbumId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Start deleting artist associations with the album.
|
||||||
|
const deleteArtistsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('artists_albums') |
||||||
|
.where({ 'albumId': albumId }); |
||||||
|
|
||||||
|
// Start deleting tag associations with the album.
|
||||||
|
const deleteTagsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('albums_tags') |
||||||
|
.where({ 'albumId': albumId }); |
||||||
|
|
||||||
|
// Start deleting track associations with the album.
|
||||||
|
const deleteTracksPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('tracks_albums') |
||||||
|
.where({ 'albumId': albumId }); |
||||||
|
|
||||||
|
// Start deleting the album.
|
||||||
|
const deleteAlbumPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('albums') |
||||||
|
.where({ id: albumId }); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTracksPromise, deleteAlbumPromise]); |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,323 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import { ArtistBaseWithRefs, ArtistWithDetails, ArtistWithRefs } from "../../client/src/api/api"; |
||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import asJson from "../lib/asJson"; |
||||||
|
import { DBError, DBErrorKind } from "../endpoints/types"; |
||||||
|
|
||||||
|
// Returns an artist with details, or null if not found.
|
||||||
|
export async function getArtist(id: number, userId: number, knex: Knex): |
||||||
|
Promise<ArtistWithDetails> { |
||||||
|
// Start transfers for tags and albums.
|
||||||
|
// Also request the artist itself.
|
||||||
|
const tagsPromise: Promise<api.TagWithId[]> = |
||||||
|
knex.select('tagId') |
||||||
|
.from('artists_tags') |
||||||
|
.where({ 'artistId': id }) |
||||||
|
.then((tags: any) => tags.map((tag: any) => tag['tagId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'parentId']) |
||||||
|
.from('tags') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const albumsPromise: Promise<api.AlbumWithId[]> = |
||||||
|
knex.select('albumId') |
||||||
|
.from('artists_albums') |
||||||
|
.where({ 'artistId': id }) |
||||||
|
.then((albums: any) => albums.map((album: any) => album['albumId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'storeLinks']) |
||||||
|
.from('album') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const artistPromise: Promise<api.Artist | undefined> = |
||||||
|
knex.select('name', 'storeLinks') |
||||||
|
.from('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: id }) |
||||||
|
.then((artists: any) => artists[0]); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
const [artist, tags, albums] = |
||||||
|
await Promise.all([artistPromise, tagsPromise, albumsPromise]); |
||||||
|
|
||||||
|
if (artist) { |
||||||
|
return { |
||||||
|
mbApi_typename: 'artist', |
||||||
|
name: artist['name'], |
||||||
|
albums: albums as api.AlbumWithId[], |
||||||
|
tags: tags as api.TagWithId[], |
||||||
|
storeLinks: asJson(artist['storeLinks'] || []), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the id of the created artist.
|
||||||
|
export async function createArtist(userId: number, artist: ArtistWithRefs, knex: Knex): Promise<number> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving albums.
|
||||||
|
const albumIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', artist.albumIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Start retrieving tags.
|
||||||
|
const tagIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', artist.tagIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [albums, tags] = await Promise.all([albumIdsPromise, tagIdsPromise]);; |
||||||
|
|
||||||
|
// Check that we found all artists and tags we need.
|
||||||
|
if ((new Set(albums.map((a: any) => a['id'])) !== new Set(artist.albumIds)) || |
||||||
|
(new Set(tags.map((a: any) => a['id'])) !== new Set(artist.tagIds))) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Create the artist.
|
||||||
|
const artistId = (await trx('artists') |
||||||
|
.insert({ |
||||||
|
name: artist.name, |
||||||
|
storeLinks: JSON.stringify(artist.storeLinks || []), |
||||||
|
user: userId, |
||||||
|
}) |
||||||
|
.returning('id') // Needed for Postgres
|
||||||
|
)[0]; |
||||||
|
|
||||||
|
// Link the albums via the linking table.
|
||||||
|
if (albums && albums.length) { |
||||||
|
await trx('artists_albums').insert( |
||||||
|
albums.map((albumId: number) => { |
||||||
|
return { |
||||||
|
albumId: albumId, |
||||||
|
artistId: artistId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// Link the tags via the linking table.
|
||||||
|
if (tags && tags.length) { |
||||||
|
await trx('artists_tags').insert( |
||||||
|
tags.map((tagId: number) => { |
||||||
|
return { |
||||||
|
artistId: artistId, |
||||||
|
tagId: tagId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return artistId; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function modifyArtist(userId: number, artistId: number, artist: ArtistBaseWithRefs, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the artist itself.
|
||||||
|
const artistIdPromise: Promise<number | undefined> = |
||||||
|
trx.select('id') |
||||||
|
.from('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: artistId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
||||||
|
|
||||||
|
// Start retrieving albums if we are modifying those.
|
||||||
|
const albumIdsPromise: Promise<number[] | undefined> = |
||||||
|
artist.albumIds ? |
||||||
|
trx.select('albumId') |
||||||
|
.from('artists_albums') |
||||||
|
.whereIn('id', artist.albumIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['albumId'])) |
||||||
|
: (async () => undefined)(); |
||||||
|
|
||||||
|
// Start retrieving tags if we are modifying those.
|
||||||
|
const tagIdsPromise = |
||||||
|
artist.tagIds ? |
||||||
|
trx.select('id') |
||||||
|
.from('artists_tags') |
||||||
|
.whereIn('id', artist.tagIds) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||||
|
(async () => undefined)(); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [oldArtist, albums, tags] = await Promise.all([artistIdPromise, albumIdsPromise, tagIdsPromise]);; |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if ((!albums || new Set(albums.map((a: any) => a['id'])) !== new Set(artist.albumIds)) || |
||||||
|
(!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(artist.tagIds)) || |
||||||
|
!oldArtist) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Modify the artist.
|
||||||
|
var update: any = {}; |
||||||
|
if ("name" in artist) { update["name"] = artist.name; } |
||||||
|
if ("storeLinks" in artist) { update["storeLinks"] = JSON.stringify(artist.storeLinks || []); } |
||||||
|
|
||||||
|
const modifyArtistPromise = trx('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': artistId }) |
||||||
|
.update(update) |
||||||
|
|
||||||
|
// Remove unlinked albums.
|
||||||
|
const removeUnlinkedAlbums = albums ? trx('artists_albums') |
||||||
|
.where({ 'artistId': artistId }) |
||||||
|
.whereNotIn('albumId', artist.albumIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Remove unlinked tags.
|
||||||
|
const removeUnlinkedTags = tags ? trx('artists_tags') |
||||||
|
.where({ 'artistId': artistId }) |
||||||
|
.whereNotIn('tagId', artist.tagIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Link new albums.
|
||||||
|
const addAlbums = albums ? trx('artists_albums') |
||||||
|
.where({ 'artistId': artistId }) |
||||||
|
.then((as: any) => as.map((a: any) => a['albumId'])) |
||||||
|
.then((doneAlbumIds: number[]) => { |
||||||
|
// Get the set of artists that are not yet linked
|
||||||
|
const toLink = (albums || []).filter((id: number) => { |
||||||
|
return !doneAlbumIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((albumId: number) => { |
||||||
|
return { |
||||||
|
artistId: artistId, |
||||||
|
albumId: albumId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('artists_artists').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Link new tags.
|
||||||
|
const addTags = tags ? trx('artists_tags') |
||||||
|
.where({ 'artistId': artistId }) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
||||||
|
.then((doneTagIds: number[]) => { |
||||||
|
// Get the set of tags that are not yet linked
|
||||||
|
const toLink = tags.filter((id: number) => { |
||||||
|
return !doneTagIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((tagId: number) => { |
||||||
|
return { |
||||||
|
tagId: tagId, |
||||||
|
artistId: artistId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('artists_tags').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Wait for all operations to finish.
|
||||||
|
await Promise.all([ |
||||||
|
modifyArtistPromise, |
||||||
|
removeUnlinkedAlbums, |
||||||
|
removeUnlinkedTags, |
||||||
|
addAlbums, |
||||||
|
addTags |
||||||
|
]); |
||||||
|
|
||||||
|
return; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteArtist(userId: number, artistId: number, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start by retrieving the artist itself for sanity.
|
||||||
|
const confirmArtistId: number | undefined = |
||||||
|
await trx.select('id') |
||||||
|
.from('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: artistId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
||||||
|
|
||||||
|
if (!confirmArtistId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Start deleting artist associations with the artist.
|
||||||
|
const deleteAlbumsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('artists_albums') |
||||||
|
.where({ 'artistId': artistId }); |
||||||
|
|
||||||
|
// Start deleting tag associations with the artist.
|
||||||
|
const deleteTagsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('artists_tags') |
||||||
|
.where({ 'artistId': artistId }); |
||||||
|
|
||||||
|
// Start deleting track associations with the artist.
|
||||||
|
const deleteTracksPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('tracks_artists') |
||||||
|
.where({ 'artistId': artistId }); |
||||||
|
|
||||||
|
// Start deleting the artist.
|
||||||
|
const deleteArtistPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('artists') |
||||||
|
.where({ id: artistId }); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
await Promise.all([deleteAlbumsPromise, deleteTagsPromise, deleteTracksPromise, deleteArtistPromise]); |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,204 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import { TrackWithRefsWithId, AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs } from "../../client/src/api/api"; |
||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import asJson from "../lib/asJson"; |
||||||
|
import { createArtist } from "./Artist"; |
||||||
|
import { createTag } from "./Tag"; |
||||||
|
import { createAlbum } from "./Album"; |
||||||
|
import { createTrack } from "./Track"; |
||||||
|
|
||||||
|
// This interface describes a JSON format in which the "interesting part"
|
||||||
|
// of the entire database for a user can be imported/exported.
|
||||||
|
// Worth noting is that the IDs used in this format only exist for cross-
|
||||||
|
// referencing between objects. They do not correspond to IDs in the actual
|
||||||
|
// database.
|
||||||
|
// Upon import, they might be replaced, and upon export, they might be randomly
|
||||||
|
// generated.
|
||||||
|
interface DBImportExportFormat { |
||||||
|
tracks: TrackWithRefsWithId[], |
||||||
|
albums: AlbumWithRefsWithId[], |
||||||
|
artists: ArtistWithRefsWithId[], |
||||||
|
tags: TagWithRefsWithId[], |
||||||
|
} |
||||||
|
|
||||||
|
export async function exportDB(userId: number, knex: Knex): Promise<DBImportExportFormat> { |
||||||
|
// First, retrieve all the objects without taking linking tables into account.
|
||||||
|
// Fetch the links separately.
|
||||||
|
|
||||||
|
let tracksPromise: Promise<api.TrackWithRefsWithId[]> = |
||||||
|
knex.select('id', 'name', 'storeLinks', 'albumId') |
||||||
|
.from('tracks') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.then((ts: any[]) => ts.map((t: any) => { |
||||||
|
return { |
||||||
|
mbApi_typename: 'track', |
||||||
|
name: t.name, |
||||||
|
id: t.id, |
||||||
|
storeLinks: asJson(t.storeLinks), |
||||||
|
albumId: t.albumId, |
||||||
|
artistIds: [], |
||||||
|
tagIds: [], |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
let albumsPromise: Promise<api.AlbumWithRefsWithId[]> = |
||||||
|
knex.select('name', 'storeLinks', 'id') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.then((ts: any[]) => ts.map((t: any) => { |
||||||
|
return { |
||||||
|
mbApi_typename: 'album', |
||||||
|
id: t.id, |
||||||
|
name: t.name, |
||||||
|
storeLinks: asJson(t.storeLinks), |
||||||
|
artistIds: [], |
||||||
|
tagIds: [], |
||||||
|
trackIds: [], |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
let artistsPromise: Promise<api.ArtistWithRefsWithId[]> = |
||||||
|
knex.select('name', 'storeLinks', 'id') |
||||||
|
.from('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.then((ts: any[]) => ts.map((t: any) => { |
||||||
|
return { |
||||||
|
mbApi_typename: 'artist', |
||||||
|
id: t.id, |
||||||
|
name: t.name, |
||||||
|
storeLinks: asJson(t.storeLinks), |
||||||
|
albumIds: [], |
||||||
|
tagIds: [], |
||||||
|
trackIds: [], |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
let tagsPromise: Promise<api.TagWithRefsWithId[]> = |
||||||
|
knex.select('name', 'parentId', 'id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.then((ts: any[]) => ts.map((t: any) => { |
||||||
|
return { |
||||||
|
mbApi_typename: 'tag', |
||||||
|
id: t.id, |
||||||
|
name: t.name, |
||||||
|
parentId: t.parentId, |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
let tracksArtistsPromise: Promise<[number, number][]> = |
||||||
|
knex.select('trackId', 'artistId') |
||||||
|
.from('tracks_artists') |
||||||
|
.then((rs: any) => rs.map((r: any) => [r.trackId, r.artistId])); |
||||||
|
|
||||||
|
let tracksTagsPromise: Promise<[number, number][]> = |
||||||
|
knex.select('trackId', 'tagId') |
||||||
|
.from('tracks_tags') |
||||||
|
.then((rs: any) => rs.map((r: any) => [r.trackId, r.tagId])); |
||||||
|
|
||||||
|
let artistsTagsPromise: Promise<[number, number][]> = |
||||||
|
knex.select('artistId', 'tagId') |
||||||
|
.from('artists_tags') |
||||||
|
.then((rs: any) => rs.map((r: any) => [r.artistId, r.tagId])); |
||||||
|
|
||||||
|
let albumsTagsPromise: Promise<[number, number][]> = |
||||||
|
knex.select('albumId', 'tagId') |
||||||
|
.from('albums_tags') |
||||||
|
.then((rs: any) => rs.map((r: any) => [r.albumId, r.tagId])); |
||||||
|
|
||||||
|
let artistsAlbumsPromise: Promise<[number, number][]> = |
||||||
|
knex.select('albumId', 'artistId') |
||||||
|
.from('artists_albums') |
||||||
|
.then((rs: any) => rs.map((r: any) => [r.albumId, r.artistId])); |
||||||
|
|
||||||
|
let [ |
||||||
|
tracks, |
||||||
|
albums, |
||||||
|
artists, |
||||||
|
tags, |
||||||
|
tracksArtists, |
||||||
|
tracksTags, |
||||||
|
artistsTags, |
||||||
|
albumsTags, |
||||||
|
artistsAlbums, |
||||||
|
] = await Promise.all([ |
||||||
|
tracksPromise, |
||||||
|
albumsPromise, |
||||||
|
artistsPromise, |
||||||
|
tagsPromise, |
||||||
|
tracksArtistsPromise, |
||||||
|
tracksTagsPromise, |
||||||
|
artistsTagsPromise, |
||||||
|
albumsTagsPromise, |
||||||
|
artistsAlbumsPromise, |
||||||
|
]); |
||||||
|
|
||||||
|
// Now store the links inside the resource objects.
|
||||||
|
tracksArtists.forEach((v: [number, number]) => { |
||||||
|
let [trackId, artistId] = v; |
||||||
|
tracks.find((t: TrackWithRefsWithId) => t.id === trackId)?.artistIds.push(artistId); |
||||||
|
}) |
||||||
|
tracksTags.forEach((v: [number, number]) => { |
||||||
|
let [trackId, tagId] = v; |
||||||
|
tracks.find((t: TrackWithRefsWithId) => t.id === trackId)?.tagIds.push(tagId); |
||||||
|
}) |
||||||
|
artistsTags.forEach((v: [number, number]) => { |
||||||
|
let [artistId, tagId] = v; |
||||||
|
artists.find((t: ArtistWithRefsWithId) => t.id === artistId)?.tagIds.push(tagId); |
||||||
|
}) |
||||||
|
albumsTags.forEach((v: [number, number]) => { |
||||||
|
let [albumId, tagId] = v; |
||||||
|
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.tagIds.push(tagId); |
||||||
|
}) |
||||||
|
artistsAlbums.forEach((v: [number, number]) => { |
||||||
|
let [artistId, albumId] = v; |
||||||
|
artists.find((t: ArtistWithRefsWithId) => t.id === artistId)?.albumIds.push(albumId); |
||||||
|
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.artistIds.push(artistId); |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
tracks: tracks, |
||||||
|
albums: albums, |
||||||
|
artists: artists, |
||||||
|
tags: tags, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function importDB(userId: number, db: DBImportExportFormat, knex: Knex): Promise<void> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
// Store the ID mappings in this record.
|
||||||
|
let tagIdMaps: Record<number, number> = {}; |
||||||
|
let artistIdMaps: Record<number, number> = {}; |
||||||
|
let albumIdMaps: Record<number, number> = {}; |
||||||
|
let trackIdMaps: Record<number, number> = {}; |
||||||
|
try { |
||||||
|
// Insert items one by one, remapping the IDs as we go.
|
||||||
|
await Promise.all(db.tags.map((tag: TagWithRefsWithId) => async () => { |
||||||
|
tagIdMaps[tag.id] = await createTag(userId, tag, knex); |
||||||
|
})); |
||||||
|
await Promise.all(db.artists.map((artist: ArtistWithRefsWithId) => async () => { |
||||||
|
artistIdMaps[artist.id] = await createArtist(userId, { |
||||||
|
...artist, |
||||||
|
tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]), |
||||||
|
}, knex); |
||||||
|
})) |
||||||
|
await Promise.all(db.albums.map((album: AlbumWithRefsWithId) => async () => { |
||||||
|
albumIdMaps[album.id] = await createAlbum(userId, { |
||||||
|
...album, |
||||||
|
tagIds: album.tagIds.map((id: number) => tagIdMaps[id]), |
||||||
|
artistIds: album.artistIds.map((id: number) => artistIdMaps[id]), |
||||||
|
}, knex); |
||||||
|
})) |
||||||
|
await Promise.all(db.tracks.map((track: TrackWithRefsWithId) => async () => { |
||||||
|
trackIdMaps[track.id] = await createTrack(userId, { |
||||||
|
...track, |
||||||
|
tagIds: track.tagIds.map((id: number) => tagIdMaps[id]), |
||||||
|
artistIds: track.artistIds.map((id: number) => artistIdMaps[id]), |
||||||
|
albumId: track.albumId ? albumIdMaps[track.albumId] : null, |
||||||
|
}, knex); |
||||||
|
})) |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import Knex from 'knex'; |
||||||
|
import asJson from '../lib/asJson'; |
||||||
|
import { DBError, DBErrorKind } from '../endpoints/types'; |
||||||
|
import { IntegrationDataWithId, IntegrationDataWithSecret, PartialIntegrationData } from '../../client/src/api/api'; |
||||||
|
|
||||||
|
export async function createIntegration(userId: number, integration: api.IntegrationDataWithSecret, knex: Knex): Promise<number> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Create the new integration.
|
||||||
|
var integration: any = { |
||||||
|
name: integration.name, |
||||||
|
user: userId, |
||||||
|
type: integration.type, |
||||||
|
details: JSON.stringify(integration.details), |
||||||
|
secretDetails: JSON.stringify(integration.secretDetails), |
||||||
|
} |
||||||
|
const integrationId = (await trx('integrations') |
||||||
|
.insert(integration) |
||||||
|
.returning('id') // Needed for Postgres
|
||||||
|
)[0]; |
||||||
|
|
||||||
|
return integrationId; |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function getIntegration(userId: number, id: number, knex: Knex): Promise<api.IntegrationData> { |
||||||
|
const integration = (await knex.select(['id', 'name', 'type', 'details']) |
||||||
|
.from('integrations') |
||||||
|
.where({ 'user': userId, 'id': id }))[0]; |
||||||
|
|
||||||
|
if (integration) { |
||||||
|
const r: api.IntegrationData = { |
||||||
|
mbApi_typename: "integrationData", |
||||||
|
name: integration.name, |
||||||
|
type: integration.type, |
||||||
|
details: asJson(integration.details), |
||||||
|
} |
||||||
|
return r; |
||||||
|
} else { |
||||||
|
let e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: "Resource not found." |
||||||
|
} |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function listIntegrations(userId: number, knex: Knex): Promise<api.IntegrationDataWithId[]> { |
||||||
|
const integrations: api.IntegrationDataWithId[] = ( |
||||||
|
await knex.select(['id', 'name', 'type', 'details']) |
||||||
|
.from('integrations') |
||||||
|
.where({ user: userId }) |
||||||
|
).map((object: any) => { |
||||||
|
return { |
||||||
|
mbApi_typename: "integrationData", |
||||||
|
id: object.id, |
||||||
|
name: object.name, |
||||||
|
type: object.type, |
||||||
|
details: asJson(object.details), |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return integrations; |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteIntegration(userId: number, id: number, knex: Knex) { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the integration itself.
|
||||||
|
const integrationId = await trx.select('id') |
||||||
|
.from('integrations') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: id }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if (!integrationId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: "Resource not found." |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Delete the integration.
|
||||||
|
await trx('integrations') |
||||||
|
.where({ 'user': userId, 'id': integrationId }) |
||||||
|
.del(); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function modifyIntegration(userId: number, id: number, integration: PartialIntegrationData, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the integration.
|
||||||
|
const integrationId = await trx.select('id') |
||||||
|
.from('integrations') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if (!integrationId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: "Resource not found", |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Modify the integration.
|
||||||
|
var update: any = {}; |
||||||
|
if ("name" in integration) { update["name"] = integration.name; } |
||||||
|
if ("details" in integration) { update["details"] = JSON.stringify(integration.details); } |
||||||
|
if ("type" in integration) { update["type"] = integration.type; } |
||||||
|
if ("secretDetails" in integration) { update["secretDetails"] = JSON.stringify(integration.details); } |
||||||
|
await trx('integrations') |
||||||
|
.where({ 'user': userId, 'id': id }) |
||||||
|
.update(update) |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,476 @@ |
|||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from '../endpoints/types'; |
||||||
|
import Knex from 'knex'; |
||||||
|
import asJson from '../lib/asJson'; |
||||||
|
|
||||||
|
export function toApiTag(dbObj: any): api.Tag { |
||||||
|
return <api.Tag>{ |
||||||
|
mbApi_typename: "tag", |
||||||
|
tagId: dbObj['tags.id'], |
||||||
|
name: dbObj['tags.name'], |
||||||
|
parentId: dbObj['tags.parentId'], |
||||||
|
parent: dbObj.parent ? toApiTag(dbObj.parent) : undefined, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function toApiArtist(dbObj: any): api.Artist { |
||||||
|
return <api.Artist>{ |
||||||
|
mbApi_typename: "artist", |
||||||
|
artistId: dbObj['artists.id'], |
||||||
|
name: dbObj['artists.name'], |
||||||
|
storeLinks: asJson(dbObj['artists.storeLinks']), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function toApiTrack(dbObj: any, artists: any[], tags: any[], album: any | undefined): api.Track { |
||||||
|
return <api.Track>{ |
||||||
|
mbApi_typename: "track", |
||||||
|
trackId: dbObj['tracks.id'], |
||||||
|
name: dbObj['tracks.name'], |
||||||
|
storeLinks: asJson(dbObj['tracks.storeLinks']), |
||||||
|
artists: artists.map((artist: any) => { |
||||||
|
return toApiArtist(artist); |
||||||
|
}), |
||||||
|
tags: tags.map((tag: any) => { |
||||||
|
return toApiTag(tag); |
||||||
|
}), |
||||||
|
album: album, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function toApiAlbum(dbObj: any): api.Album { |
||||||
|
return <api.Album>{ |
||||||
|
mbApi_typename: "album", |
||||||
|
albumId: dbObj['albums.id'], |
||||||
|
name: dbObj['albums.name'], |
||||||
|
storeLinks: asJson(dbObj['albums.storeLinks']), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
enum ObjectType { |
||||||
|
Track = 0, |
||||||
|
Artist, |
||||||
|
Tag, |
||||||
|
Album, |
||||||
|
} |
||||||
|
|
||||||
|
// To keep track of which database objects are needed to filter on
|
||||||
|
// certain properties.
|
||||||
|
const propertyObjects: Record<api.QueryElemProperty, ObjectType> = { |
||||||
|
[api.QueryElemProperty.albumId]: ObjectType.Album, |
||||||
|
[api.QueryElemProperty.albumName]: ObjectType.Album, |
||||||
|
[api.QueryElemProperty.artistId]: ObjectType.Artist, |
||||||
|
[api.QueryElemProperty.artistName]: ObjectType.Artist, |
||||||
|
[api.QueryElemProperty.trackId]: ObjectType.Track, |
||||||
|
[api.QueryElemProperty.trackName]: ObjectType.Track, |
||||||
|
[api.QueryElemProperty.tagId]: ObjectType.Tag, |
||||||
|
[api.QueryElemProperty.tagName]: ObjectType.Tag, |
||||||
|
[api.QueryElemProperty.trackStoreLinks]: ObjectType.Track, |
||||||
|
[api.QueryElemProperty.artistStoreLinks]: ObjectType.Artist, |
||||||
|
[api.QueryElemProperty.albumStoreLinks]: ObjectType.Album, |
||||||
|
} |
||||||
|
|
||||||
|
// To keep track of the tables in which objects are stored.
|
||||||
|
const objectTables: Record<ObjectType, string> = { |
||||||
|
[ObjectType.Album]: 'albums', |
||||||
|
[ObjectType.Artist]: 'artists', |
||||||
|
[ObjectType.Track]: 'tracks', |
||||||
|
[ObjectType.Tag]: 'tags', |
||||||
|
} |
||||||
|
|
||||||
|
// To keep track of linking tables between objects.
|
||||||
|
const linkingTables: any = [ |
||||||
|
[[ObjectType.Track, ObjectType.Album], 'tracks_albums'], |
||||||
|
[[ObjectType.Track, ObjectType.Artist], 'tracks_artists'], |
||||||
|
[[ObjectType.Track, ObjectType.Tag], 'tracks_tags'], |
||||||
|
[[ObjectType.Artist, ObjectType.Album], 'artists_albums'], |
||||||
|
[[ObjectType.Artist, ObjectType.Tag], 'artists_tags'], |
||||||
|
[[ObjectType.Album, ObjectType.Tag], 'albums_tags'], |
||||||
|
] |
||||||
|
function getLinkingTable(a: ObjectType, b: ObjectType): string { |
||||||
|
var res: string | undefined = undefined; |
||||||
|
linkingTables.forEach((row: any) => { |
||||||
|
if (row[0].includes(a) && row[0].includes(b)) { |
||||||
|
res = row[1]; |
||||||
|
} |
||||||
|
}) |
||||||
|
if (res) return res; |
||||||
|
|
||||||
|
throw "Could not find linking table for objects: " + JSON.stringify(a) + ", " + JSON.stringify(b); |
||||||
|
} |
||||||
|
|
||||||
|
// To keep track of ID fields used in linking tables.
|
||||||
|
const linkingTableIdNames: Record<ObjectType, string> = { |
||||||
|
[ObjectType.Album]: 'albumId', |
||||||
|
[ObjectType.Artist]: 'artistId', |
||||||
|
[ObjectType.Track]: 'trackId', |
||||||
|
[ObjectType.Tag]: 'tagId', |
||||||
|
} |
||||||
|
|
||||||
|
function getRequiredDatabaseObjects(queryElem: api.QueryElem): Set<ObjectType> { |
||||||
|
if (queryElem.prop) { |
||||||
|
// Leaf node.
|
||||||
|
return new Set([propertyObjects[queryElem.prop]]); |
||||||
|
} else if (queryElem.children) { |
||||||
|
// Branch node.
|
||||||
|
var r = new Set<ObjectType>(); |
||||||
|
queryElem.children.forEach((child: api.QueryElem) => { |
||||||
|
getRequiredDatabaseObjects(child).forEach(object => r.add(object)); |
||||||
|
}); |
||||||
|
return r; |
||||||
|
} |
||||||
|
return new Set([]); |
||||||
|
} |
||||||
|
|
||||||
|
function addJoin(knexQuery: any, base: ObjectType, other: ObjectType) { |
||||||
|
const linkTable = getLinkingTable(base, other); |
||||||
|
const baseTable = objectTables[base]; |
||||||
|
const otherTable = objectTables[other]; |
||||||
|
return knexQuery |
||||||
|
.join(linkTable, { [baseTable + '.id']: linkTable + '.' + linkingTableIdNames[base] }) |
||||||
|
.join(otherTable, { [otherTable + '.id']: linkTable + '.' + linkingTableIdNames[other] }); |
||||||
|
} |
||||||
|
|
||||||
|
enum WhereType { |
||||||
|
And = 0, |
||||||
|
Or, |
||||||
|
}; |
||||||
|
|
||||||
|
function getSQLValue(val: any) { |
||||||
|
console.log("Value:", val) |
||||||
|
if (typeof val === 'string') { |
||||||
|
return `'${val}'`; |
||||||
|
} else if (typeof val === 'number') { |
||||||
|
return `${val}`; |
||||||
|
} |
||||||
|
throw new Error("unimplemented SQL value type."); |
||||||
|
} |
||||||
|
|
||||||
|
function getSQLValues(vals: any[]) { |
||||||
|
if (vals.length === 0) { return '()' } |
||||||
|
let r = `(${getSQLValue(vals[0])}`; |
||||||
|
for (let i: number = 1; i < vals.length; i++) { |
||||||
|
r += `, ${getSQLValue(vals[i])}`; |
||||||
|
} |
||||||
|
r += ')'; |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
function getLeafWhere(queryElem: api.QueryElem): string { |
||||||
|
const simpleLeafOps: Record<any, string> = { |
||||||
|
[api.QueryLeafOp.Eq]: "=", |
||||||
|
[api.QueryLeafOp.Ne]: "!=", |
||||||
|
[api.QueryLeafOp.Like]: "LIKE", |
||||||
|
} |
||||||
|
|
||||||
|
const propertyKeys = { |
||||||
|
[api.QueryElemProperty.trackName]: '`tracks`.`name`', |
||||||
|
[api.QueryElemProperty.trackId]: '`tracks`.`id`', |
||||||
|
[api.QueryElemProperty.artistName]: '`artists`.`name`', |
||||||
|
[api.QueryElemProperty.artistId]: '`artists`.`id`', |
||||||
|
[api.QueryElemProperty.albumName]: '`albums`.`name`', |
||||||
|
[api.QueryElemProperty.albumId]: '`albums`.`id`', |
||||||
|
[api.QueryElemProperty.tagId]: '`tags`.`id`', |
||||||
|
[api.QueryElemProperty.tagName]: '`tags`.`name`', |
||||||
|
[api.QueryElemProperty.trackStoreLinks]: '`tracks`.`storeLinks`', |
||||||
|
[api.QueryElemProperty.artistStoreLinks]: '`artists`.`storeLinks`', |
||||||
|
[api.QueryElemProperty.albumStoreLinks]: '`albums`.`storeLinks`', |
||||||
|
} |
||||||
|
|
||||||
|
if (!queryElem.propOperator) throw "Cannot create where clause without an operator."; |
||||||
|
const operator = queryElem.propOperator || api.QueryLeafOp.Eq; |
||||||
|
const a = queryElem.prop && propertyKeys[queryElem.prop]; |
||||||
|
const b = operator === api.QueryLeafOp.Like ? |
||||||
|
'%' + (queryElem.propOperand || "") + '%' |
||||||
|
: (queryElem.propOperand || ""); |
||||||
|
|
||||||
|
if (Object.keys(simpleLeafOps).includes(operator)) { |
||||||
|
return `(${a} ${simpleLeafOps[operator]} ${getSQLValue(b)})`; |
||||||
|
} else if (operator == api.QueryLeafOp.In) { |
||||||
|
return `(${a} IN ${getSQLValues(b)})` |
||||||
|
} else if (operator == api.QueryLeafOp.NotIn) { |
||||||
|
return `(${a} NOT IN ${getSQLValues(b)})` |
||||||
|
} |
||||||
|
|
||||||
|
throw "Query filter not implemented"; |
||||||
|
} |
||||||
|
|
||||||
|
function getNodeWhere(queryElem: api.QueryElem): string { |
||||||
|
let ops = { |
||||||
|
[api.QueryNodeOp.And]: 'AND', |
||||||
|
[api.QueryNodeOp.Or]: 'OR', |
||||||
|
[api.QueryNodeOp.Not]: 'NOT', |
||||||
|
} |
||||||
|
let buildList = (subqueries: api.QueryElem[], operator: api.QueryNodeOp) => { |
||||||
|
if (subqueries.length === 0) { return 'true' } |
||||||
|
let r = `(${getWhere(subqueries[0])}`; |
||||||
|
for (let i: number = 1; i < subqueries.length; i++) { |
||||||
|
r += ` ${ops[operator]} ${getWhere(subqueries[i])}`; |
||||||
|
} |
||||||
|
r += ')'; |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
if (queryElem.children && queryElem.childrenOperator && queryElem.children.length) { |
||||||
|
if (queryElem.childrenOperator === api.QueryNodeOp.And || |
||||||
|
queryElem.childrenOperator === api.QueryNodeOp.Or) { |
||||||
|
return buildList(queryElem.children, queryElem.childrenOperator) |
||||||
|
} else if (queryElem.childrenOperator === api.QueryNodeOp.Not && |
||||||
|
queryElem.children.length === 1) { |
||||||
|
return `NOT ${getWhere(queryElem.children[0])}` |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error('invalid query') |
||||||
|
} |
||||||
|
|
||||||
|
function getWhere(queryElem: api.QueryElem): string { |
||||||
|
if (queryElem.prop) { return getLeafWhere(queryElem); } |
||||||
|
if (queryElem.children) { return getNodeWhere(queryElem); } |
||||||
|
return "true"; |
||||||
|
} |
||||||
|
|
||||||
|
const objectColumns = { |
||||||
|
[ObjectType.Track]: ['tracks.id as tracks.id', 'tracks.title as tracks.title', 'tracks.storeLinks as tracks.storeLinks'], |
||||||
|
[ObjectType.Artist]: ['artists.id as artists.id', 'artists.name as artists.name', 'artists.storeLinks as artists.storeLinks'], |
||||||
|
[ObjectType.Album]: ['albums.id as albums.id', 'albums.name as albums.name', 'albums.storeLinks as albums.storeLinks'], |
||||||
|
[ObjectType.Tag]: ['tags.id as tags.id', 'tags.name as tags.name', 'tags.parentId as tags.parentId'] |
||||||
|
}; |
||||||
|
|
||||||
|
function constructQuery(knex: Knex, userId: number, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering, |
||||||
|
offset: number, limit: number | null) { |
||||||
|
const joinObjects = getRequiredDatabaseObjects(queryElem); |
||||||
|
joinObjects.delete(queryFor); // We are already querying this object in the base query.
|
||||||
|
|
||||||
|
// Figure out what data we want to select from the results.
|
||||||
|
var columns: any[] = objectColumns[queryFor]; |
||||||
|
|
||||||
|
// TODO: there was a line here to add columns for the joined objects.
|
||||||
|
// Could not get it to work with Postgres, which wants aggregate functions
|
||||||
|
// to specify exactly how duplicates should be aggregated.
|
||||||
|
// Not sure whether we need these columns in the first place.
|
||||||
|
// joinObjects.forEach((obj: ObjectType) => columns.push(...objectColumns[obj]));
|
||||||
|
|
||||||
|
// First, we create a base query for the type of object we need to yield.
|
||||||
|
var q = knex.select(columns) |
||||||
|
.where({ [objectTables[queryFor] + '.user']: userId }) |
||||||
|
.groupBy(objectTables[queryFor] + '.' + 'id') |
||||||
|
.from(objectTables[queryFor]); |
||||||
|
|
||||||
|
// Now, we need to add join statements for other objects we want to filter on.
|
||||||
|
joinObjects.forEach((object: ObjectType) => { |
||||||
|
q = addJoin(q, queryFor, object); |
||||||
|
}) |
||||||
|
|
||||||
|
// Apply filtering.
|
||||||
|
q = q.andWhereRaw(getWhere(queryElem)); |
||||||
|
|
||||||
|
// Apply ordering
|
||||||
|
const orderKeys = { |
||||||
|
[api.OrderByType.Name]: objectTables[queryFor] + '.' + ((queryFor === ObjectType.Track) ? 'title' : 'name') |
||||||
|
}; |
||||||
|
q = q.orderBy(orderKeys[ordering.orderBy.type], |
||||||
|
(ordering.ascending ? 'asc' : 'desc')); |
||||||
|
|
||||||
|
// Apply limiting.
|
||||||
|
if (limit !== null) { |
||||||
|
q = q.limit(limit) |
||||||
|
} |
||||||
|
|
||||||
|
// Apply offsetting.
|
||||||
|
q = q.offset(offset); |
||||||
|
|
||||||
|
return q; |
||||||
|
} |
||||||
|
|
||||||
|
async function getLinkedObjects(knex: Knex, userId: number, base: ObjectType, linked: ObjectType, baseIds: number[]) { |
||||||
|
var result: Record<number, any[]> = {}; |
||||||
|
const otherTable = objectTables[linked]; |
||||||
|
const linkingTable = getLinkingTable(base, linked); |
||||||
|
const columns = objectColumns[linked]; |
||||||
|
|
||||||
|
await Promise.all(baseIds.map((baseId: number) => { |
||||||
|
return knex.select(columns).groupBy(otherTable + '.id').from(otherTable) |
||||||
|
.join(linkingTable, { [linkingTable + '.' + linkingTableIdNames[linked]]: otherTable + '.id' }) |
||||||
|
.where({ [otherTable + '.user']: userId }) |
||||||
|
.where({ [linkingTable + '.' + linkingTableIdNames[base]]: baseId }) |
||||||
|
.then((others: any) => { result[baseId] = others; }) |
||||||
|
})) |
||||||
|
|
||||||
|
console.log("Query results for", baseIds, ":", result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
// Resolve a tag into the full nested structure of its ancestors.
|
||||||
|
async function getFullTag(knex: Knex, userId: number, tag: any): Promise<any> { |
||||||
|
const resolveTag = async (t: any) => { |
||||||
|
if (t['tags.parentId']) { |
||||||
|
const parent = (await knex.select(objectColumns[ObjectType.Tag]) |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ [objectTables[ObjectType.Tag] + '.id']: t['tags.parentId'] }))[0]; |
||||||
|
t.parent = await resolveTag(parent); |
||||||
|
} |
||||||
|
return t; |
||||||
|
} |
||||||
|
|
||||||
|
return await resolveTag(tag); |
||||||
|
} |
||||||
|
|
||||||
|
export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex): Promise<api.QueryResponse> { |
||||||
|
const trackLimit = q.offsetsLimits.trackLimit; |
||||||
|
const trackOffset = q.offsetsLimits.trackOffset; |
||||||
|
const tagLimit = q.offsetsLimits.tagLimit; |
||||||
|
const tagOffset = q.offsetsLimits.tagOffset; |
||||||
|
const artistLimit = q.offsetsLimits.artistLimit; |
||||||
|
const artistOffset = q.offsetsLimits.artistOffset; |
||||||
|
const albumLimit = q.offsetsLimits.albumLimit; |
||||||
|
const albumOffset = q.offsetsLimits.albumOffset; |
||||||
|
|
||||||
|
const artistsPromise: Promise<any> = (artistLimit && artistLimit !== 0) ? |
||||||
|
constructQuery(knex, |
||||||
|
userId, |
||||||
|
ObjectType.Artist, |
||||||
|
q.query, |
||||||
|
q.ordering, |
||||||
|
artistOffset || 0, |
||||||
|
artistLimit >= 0 ? artistLimit : null, |
||||||
|
) : |
||||||
|
(async () => [])(); |
||||||
|
|
||||||
|
const albumsPromise: Promise<any> = (albumLimit && albumLimit !== 0) ? |
||||||
|
constructQuery(knex, |
||||||
|
userId, |
||||||
|
ObjectType.Album, |
||||||
|
q.query, |
||||||
|
q.ordering, |
||||||
|
artistOffset || 0, |
||||||
|
albumLimit >= 0 ? albumLimit : null, |
||||||
|
) : |
||||||
|
(async () => [])(); |
||||||
|
|
||||||
|
const tracksPromise: Promise<any> = (trackLimit && trackLimit !== 0) ? |
||||||
|
constructQuery(knex, |
||||||
|
userId, |
||||||
|
ObjectType.Track, |
||||||
|
q.query, |
||||||
|
q.ordering, |
||||||
|
trackOffset || 0, |
||||||
|
trackLimit >= 0 ? trackLimit : null, |
||||||
|
) : |
||||||
|
(async () => [])(); |
||||||
|
|
||||||
|
const tagsPromise: Promise<any> = (tagLimit && tagLimit !== 0) ? |
||||||
|
constructQuery(knex, |
||||||
|
userId, |
||||||
|
ObjectType.Tag, |
||||||
|
q.query, |
||||||
|
q.ordering, |
||||||
|
tagOffset || 0, |
||||||
|
tagLimit >= 0 ? tagLimit : null, |
||||||
|
) : |
||||||
|
(async () => [])(); |
||||||
|
|
||||||
|
// For some objects, we want to return linked information as well.
|
||||||
|
// For that we need to do further queries.
|
||||||
|
const trackIdsPromise = (async () => { |
||||||
|
const tracks = await tracksPromise; |
||||||
|
const ids = tracks.map((track: any) => track['tracks.id']); |
||||||
|
return ids; |
||||||
|
})(); |
||||||
|
const tracksArtistsPromise: Promise<Record<number, any[]>> = (trackLimit && trackLimit !== 0) ? |
||||||
|
(async () => { |
||||||
|
return await getLinkedObjects(knex, userId, ObjectType.Track, ObjectType.Artist, await trackIdsPromise); |
||||||
|
})() : |
||||||
|
(async () => { return {}; })(); |
||||||
|
const tracksTagsPromise: Promise<Record<number, any[]>> = (trackLimit && trackLimit !== 0) ? |
||||||
|
(async () => { |
||||||
|
const tagsPerTrack: Record<number, any> = await getLinkedObjects(knex, userId, ObjectType.Track, ObjectType.Tag, await trackIdsPromise); |
||||||
|
var result: Record<number, any> = {}; |
||||||
|
for (var key in tagsPerTrack) { |
||||||
|
const tags = tagsPerTrack[key]; |
||||||
|
var fullTags: any[] = []; |
||||||
|
for (var idx in tags) { |
||||||
|
fullTags.push(await getFullTag(knex, userId, tags[idx])); |
||||||
|
} |
||||||
|
result[key] = fullTags; |
||||||
|
} |
||||||
|
return result; |
||||||
|
})() : |
||||||
|
(async () => { return {}; })(); |
||||||
|
const tracksAlbumsPromise: Promise<Record<number, any[]>> = (trackLimit && trackLimit !== 0) ? |
||||||
|
(async () => { |
||||||
|
return await getLinkedObjects(knex, userId, ObjectType.Track, ObjectType.Album, await trackIdsPromise); |
||||||
|
})() : |
||||||
|
(async () => { return {}; })(); |
||||||
|
|
||||||
|
const [ |
||||||
|
tracks, |
||||||
|
artists, |
||||||
|
albums, |
||||||
|
tags, |
||||||
|
tracksArtists, |
||||||
|
tracksTags, |
||||||
|
tracksAlbums, |
||||||
|
] = |
||||||
|
await Promise.all([ |
||||||
|
tracksPromise, |
||||||
|
artistsPromise, |
||||||
|
albumsPromise, |
||||||
|
tagsPromise, |
||||||
|
tracksArtistsPromise, |
||||||
|
tracksTagsPromise, |
||||||
|
tracksAlbumsPromise, |
||||||
|
]); |
||||||
|
|
||||||
|
var response: api.QueryResponse = { |
||||||
|
tracks: [], |
||||||
|
artists: [], |
||||||
|
albums: [], |
||||||
|
tags: [], |
||||||
|
}; |
||||||
|
|
||||||
|
switch (q.responseType) { |
||||||
|
case api.QueryResponseType.Details: { |
||||||
|
response = { |
||||||
|
tracks: tracks.map((track: any) => { |
||||||
|
const id = track['tracks.id']; |
||||||
|
return toApiTrack(track, tracksArtists[id], tracksTags[id], tracksAlbums[id]); |
||||||
|
}), |
||||||
|
artists: artists.map((artist: any) => { |
||||||
|
return toApiArtist(artist); |
||||||
|
}), |
||||||
|
albums: albums.map((album: any) => { |
||||||
|
return toApiAlbum(album); |
||||||
|
}), |
||||||
|
tags: tags.map((tag: any) => { |
||||||
|
return toApiTag(tag); |
||||||
|
}), |
||||||
|
}; |
||||||
|
break; |
||||||
|
} |
||||||
|
case api.QueryResponseType.Ids: { |
||||||
|
response = { |
||||||
|
tracks: tracks.map((track: any) => track['tracks.id']), |
||||||
|
artists: artists.map((artist: any) => artist['artists.id']), |
||||||
|
albums: albums.map((album: any) => album['albums.id']), |
||||||
|
tags: tags.map((tag: any) => tag['tags.id']), |
||||||
|
}; |
||||||
|
break; |
||||||
|
} |
||||||
|
case api.QueryResponseType.Count: { |
||||||
|
response = { |
||||||
|
tracks: tracks.length, |
||||||
|
artists: artists.length, |
||||||
|
albums: albums.length, |
||||||
|
tags: tags.length, |
||||||
|
}; |
||||||
|
break; |
||||||
|
} |
||||||
|
default: { |
||||||
|
throw new Error("Unimplemented response type.") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return response; |
||||||
|
} |
||||||
@ -0,0 +1,274 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import { TagBaseWithRefs, TagWithDetails, TagWithId, TagWithRefs, TagWithRefsWithId } from "../../client/src/api/api"; |
||||||
|
import { DBError, DBErrorKind } from "../endpoints/types"; |
||||||
|
|
||||||
|
export async function getTagChildrenRecursive(id: number, userId: number, trx: any): Promise<number[]> { |
||||||
|
const directChildren = (await trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'parentId': id })).map((r: any) => r.id); |
||||||
|
|
||||||
|
const indirectChildrenPromises = directChildren.map( |
||||||
|
(child: number) => getTagChildrenRecursive(child, userId, trx) |
||||||
|
); |
||||||
|
const indirectChildrenNested = await Promise.all(indirectChildrenPromises); |
||||||
|
const indirectChildren = indirectChildrenNested.flat(); |
||||||
|
|
||||||
|
return [ |
||||||
|
...directChildren, |
||||||
|
...indirectChildren, |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the id of the created tag.
|
||||||
|
export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): Promise<number> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// If applicable, retrieve the parent tag.
|
||||||
|
const maybeParent: number | null = |
||||||
|
tag.parentId ? |
||||||
|
(await trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': tag.parentId }))[0]['id'] : |
||||||
|
null; |
||||||
|
|
||||||
|
// Check if the parent was found, if applicable.
|
||||||
|
if (tag.parentId && maybeParent !== tag.parentId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Create the new tag.
|
||||||
|
var tag: any = { |
||||||
|
name: tag.name, |
||||||
|
user: userId, |
||||||
|
}; |
||||||
|
if (maybeParent) { |
||||||
|
tag['parentId'] = maybeParent; |
||||||
|
} |
||||||
|
const tagId = (await trx('tags') |
||||||
|
.insert(tag) |
||||||
|
.returning('id') // Needed for Postgres
|
||||||
|
)[0]; |
||||||
|
|
||||||
|
return tagId; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteTag(userId: number, tagId: number, knex: Knex) { |
||||||
|
|
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving any child tags.
|
||||||
|
const childTagsPromise = |
||||||
|
getTagChildrenRecursive(tagId, userId, trx); |
||||||
|
|
||||||
|
// Start retrieving the tag itself.
|
||||||
|
const tagPromise = trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: tagId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [tag, children] = await Promise.all([tagPromise, childTagsPromise]); |
||||||
|
|
||||||
|
// Merge all IDs.
|
||||||
|
const toDelete = [tag, ...children]; |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if (!tag) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Start deleting artist associations with the tag.
|
||||||
|
const deleteArtistsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('artists_tags') |
||||||
|
.whereIn('tagId', toDelete); |
||||||
|
|
||||||
|
// Start deleting album associations with the tag.
|
||||||
|
const deleteAlbumsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('albums_tags') |
||||||
|
.whereIn('tagId', toDelete); |
||||||
|
|
||||||
|
// Start deleting track associations with the tag.
|
||||||
|
const deleteTracksPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('tracks_tags') |
||||||
|
.whereIn('tagId', toDelete); |
||||||
|
|
||||||
|
|
||||||
|
// Start deleting the tag and its children.
|
||||||
|
const deleteTags: Promise<any> = trx('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', toDelete) |
||||||
|
.del(); |
||||||
|
|
||||||
|
await Promise.all([deleteArtistsPromise, deleteAlbumsPromise, deleteTracksPromise, deleteTags]) |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function getTag(userId: number, tagId: number, knex: Knex): Promise<TagWithDetails> { |
||||||
|
const tagPromise: Promise<TagWithRefsWithId | undefined> = |
||||||
|
knex.select(['id', 'name', 'parentId']) |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': tagId }) |
||||||
|
.then((r: TagWithRefsWithId[] | undefined) => r ? r[0] : undefined); |
||||||
|
|
||||||
|
const parentPromise: Promise<TagWithId | null> = |
||||||
|
tagPromise |
||||||
|
.then((r: TagWithRefsWithId | undefined) => |
||||||
|
(r && r.parentId) ? ( |
||||||
|
getTag(userId, r.parentId, knex) |
||||||
|
.then((rr: TagWithDetails | null) => rr ? { ...rr, id: r.parentId || 0 } : null) |
||||||
|
) : null |
||||||
|
) |
||||||
|
|
||||||
|
const [maybeTag, maybeParent] = await Promise.all([tagPromise, parentPromise]); |
||||||
|
|
||||||
|
if (maybeTag) { |
||||||
|
let result: TagWithDetails = { |
||||||
|
mbApi_typename: "tag", |
||||||
|
name: maybeTag.name, |
||||||
|
parent: maybeParent, |
||||||
|
} |
||||||
|
return result; |
||||||
|
} else { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function modifyTag(userId: number, tagId: number, tag: TagBaseWithRefs, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the parent tag.
|
||||||
|
const parentTagIdPromise: Promise<number | undefined | null> = tag.parentId ? |
||||||
|
trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': tag.parentId }) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||||
|
(async () => { return null })(); |
||||||
|
|
||||||
|
// Start retrieving the tag itself.
|
||||||
|
const tagPromise = trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: tagId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [dbTag, parent] = await Promise.all([tagPromise, parentTagIdPromise]); |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if ((tag.parentId && !parent) || |
||||||
|
!dbTag) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Modify the tag.
|
||||||
|
await trx('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': tagId }) |
||||||
|
.update({ |
||||||
|
name: tag.name, |
||||||
|
parentId: tag.parentId || null, |
||||||
|
}) |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function mergeTag(userId: number, fromId: number, toId: number, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the "from" tag.
|
||||||
|
const fromTagIdPromise = trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: fromId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||||
|
|
||||||
|
// Start retrieving the "to" tag.
|
||||||
|
const toTagIdPromise = trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: toId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [fromTagId, toTagId] = await Promise.all([fromTagIdPromise, toTagIdPromise]); |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if (!fromTagId || !toTagId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Assign new tag ID to any objects referencing the to-be-merged tag.
|
||||||
|
const cPromise = trx('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'parentId': fromId }) |
||||||
|
.update({ 'parentId': toId }); |
||||||
|
const sPromise = trx('songs_tags') |
||||||
|
.where({ 'tagId': fromId }) |
||||||
|
.update({ 'tagId': toId }); |
||||||
|
const arPromise = trx('artists_tags') |
||||||
|
.where({ 'tagId': fromId }) |
||||||
|
.update({ 'tagId': toId }); |
||||||
|
const alPromise = trx('albums_tags') |
||||||
|
.where({ 'tagId': fromId }) |
||||||
|
.update({ 'tagId': toId }); |
||||||
|
await Promise.all([sPromise, arPromise, alPromise, cPromise]); |
||||||
|
|
||||||
|
// Delete the original tag.
|
||||||
|
await trx('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': fromId }) |
||||||
|
.del(); |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,343 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import { TrackBaseWithRefs, TrackWithDetails, TrackWithRefs } from "../../client/src/api/api"; |
||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import asJson from "../lib/asJson"; |
||||||
|
import { DBError, DBErrorKind } from "../endpoints/types"; |
||||||
|
|
||||||
|
// Returns an track with details, or null if not found.
|
||||||
|
export async function getTrack(id: number, userId: number, knex: Knex): |
||||||
|
Promise<TrackWithDetails> { |
||||||
|
// Start transfers for tracks, tags and artists.
|
||||||
|
// Also request the track itself.
|
||||||
|
const tagsPromise: Promise<api.TagWithId[]> = |
||||||
|
knex.select('tagId') |
||||||
|
.from('tracks_tags') |
||||||
|
.where({ 'trackId': id }) |
||||||
|
.then((tags: any) => tags.map((tag: any) => tag['tagId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'parentId']) |
||||||
|
.from('tags') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const artistsPromise: Promise<api.ArtistWithId[]> = |
||||||
|
knex.select('artistId') |
||||||
|
.from('artists_tracks') |
||||||
|
.where({ 'trackId': id }) |
||||||
|
.then((artists: any) => artists.map((artist: any) => artist['artistId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
knex.select(['id', 'name', 'storeLinks']) |
||||||
|
.from('artists') |
||||||
|
.whereIn('id', ids) |
||||||
|
); |
||||||
|
|
||||||
|
const trackPromise: Promise<api.Track | undefined> = |
||||||
|
knex.select('name', 'storeLinks') |
||||||
|
.from('tracks') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: id }) |
||||||
|
.then((tracks: any) => tracks[0]); |
||||||
|
|
||||||
|
|
||||||
|
const albumPromise: Promise<api.AlbumWithId | null> = |
||||||
|
trackPromise |
||||||
|
.then((t: api.Track | undefined) => |
||||||
|
t ? knex.select('id', 'name', 'storeLinks') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: t.albumId }) |
||||||
|
.then((albums: any) => albums.length > 0 ? albums[0] : null) |
||||||
|
: (() => null)() |
||||||
|
) |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
const [track, tags, album, artists] = |
||||||
|
await Promise.all([trackPromise, tagsPromise, albumPromise, artistsPromise]); |
||||||
|
|
||||||
|
if (track) { |
||||||
|
return { |
||||||
|
mbApi_typename: 'track', |
||||||
|
name: track['name'], |
||||||
|
artists: artists as api.ArtistWithId[], |
||||||
|
tags: tags as api.TagWithId[], |
||||||
|
album: album as api.AlbumWithId | null, |
||||||
|
storeLinks: asJson(track['storeLinks'] || []), |
||||||
|
}; |
||||||
|
} else { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the id of the created track.
|
||||||
|
export async function createTrack(userId: number, track: TrackWithRefs, knex: Knex): Promise<number> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving artists.
|
||||||
|
const artistIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('artists') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', track.artistIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Start retrieving tags.
|
||||||
|
const tagIdsPromise: Promise<number[]> = |
||||||
|
trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.whereIn('id', track.tagIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['id'])); |
||||||
|
|
||||||
|
// Start retrieving album.
|
||||||
|
const albumIdPromise: Promise<number | null> = |
||||||
|
knex.select('id') |
||||||
|
.from('albums') |
||||||
|
.where({ 'user': userId, 'albumId': track.albumId }) |
||||||
|
.then((albums: any) => albums.map((album: any) => album['albumId'])) |
||||||
|
.then((ids: number[]) => |
||||||
|
ids.length > 0 ? ids[0] : (() => null)() |
||||||
|
); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [artists, tags, album] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdPromise]);; |
||||||
|
|
||||||
|
// Check that we found all artists and tags we need.
|
||||||
|
if ((new Set((artists as number[]).map((a: any) => a['id'])) !== new Set(track.artistIds)) || |
||||||
|
(new Set((tags as number[]).map((a: any) => a['id'])) !== new Set(track.tagIds)) || |
||||||
|
(album === null)) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Create the track.
|
||||||
|
const trackId = (await trx('tracks') |
||||||
|
.insert({ |
||||||
|
name: track.name, |
||||||
|
storeLinks: JSON.stringify(track.storeLinks || []), |
||||||
|
user: userId, |
||||||
|
albumId: album, |
||||||
|
}) |
||||||
|
.returning('id') // Needed for Postgres
|
||||||
|
)[0]; |
||||||
|
|
||||||
|
// Link the artists via the linking table.
|
||||||
|
if (artists && artists.length) { |
||||||
|
await trx('artists_tracks').insert( |
||||||
|
artists.map((artistId: number) => { |
||||||
|
return { |
||||||
|
artistId: artistId, |
||||||
|
trackId: trackId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// Link the tags via the linking table.
|
||||||
|
if (tags && tags.length) { |
||||||
|
await trx('tracks_tags').insert( |
||||||
|
tags.map((tagId: number) => { |
||||||
|
return { |
||||||
|
trackId: trackId, |
||||||
|
tagId: tagId, |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return trackId; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function modifyTrack(userId: number, trackId: number, track: TrackBaseWithRefs, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start retrieving the track itself.
|
||||||
|
const trackIdPromise: Promise<number | undefined> = |
||||||
|
trx.select('id') |
||||||
|
.from('tracks') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: trackId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
||||||
|
|
||||||
|
// Start retrieving artists if we are modifying those.
|
||||||
|
const artistIdsPromise: Promise<number[] | undefined> = |
||||||
|
track.artistIds ? |
||||||
|
trx.select('artistId') |
||||||
|
.from('artists_tracks') |
||||||
|
.whereIn('artistId', track.artistIds) |
||||||
|
.then((as: any) => as.map((a: any) => a['artistId'])) |
||||||
|
: (async () => undefined)(); |
||||||
|
|
||||||
|
// Start retrieving tags if we are modifying those.
|
||||||
|
const tagIdsPromise = |
||||||
|
track.tagIds ? |
||||||
|
trx.select('id') |
||||||
|
.from('tracks_tags') |
||||||
|
.whereIn('tagId', track.tagIds) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||||
|
(async () => undefined)(); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [oldTrack, artists, tags] = await Promise.all([trackIdPromise, artistIdsPromise, tagIdsPromise]);; |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if ((!artists || new Set(artists.map((a: any) => a['id'])) !== new Set(track.artistIds)) || |
||||||
|
(!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(track.tagIds)) || |
||||||
|
!oldTrack) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Modify the track.
|
||||||
|
var update: any = {}; |
||||||
|
if ("name" in track) { update["name"] = track.name; } |
||||||
|
if ("storeLinks" in track) { update["storeLinks"] = JSON.stringify(track.storeLinks || []); } |
||||||
|
if ("albumId" in track) { update["albumId"] = track.albumId; } |
||||||
|
|
||||||
|
const modifyTrackPromise = trx('tracks') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': trackId }) |
||||||
|
.update(update) |
||||||
|
|
||||||
|
// Remove unlinked artists.
|
||||||
|
const removeUnlinkedArtists = artists ? trx('artists_tracks') |
||||||
|
.where({ 'trackId': trackId }) |
||||||
|
.whereNotIn('artistId', track.artistIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Remove unlinked tags.
|
||||||
|
const removeUnlinkedTags = tags ? trx('tracks_tags') |
||||||
|
.where({ 'trackId': trackId }) |
||||||
|
.whereNotIn('tagId', track.tagIds || []) |
||||||
|
.delete() : undefined; |
||||||
|
|
||||||
|
// Link new artists.
|
||||||
|
const addArtists = artists ? trx('artists_tracks') |
||||||
|
.where({ 'trackId': trackId }) |
||||||
|
.then((as: any) => as.map((a: any) => a['artistId'])) |
||||||
|
.then((doneArtistIds: number[]) => { |
||||||
|
// Get the set of artists that are not yet linked
|
||||||
|
const toLink = (artists || []).filter((id: number) => { |
||||||
|
return !doneArtistIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((artistId: number) => { |
||||||
|
return { |
||||||
|
artistId: artistId, |
||||||
|
trackId: trackId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('artists_tracks').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Link new tags.
|
||||||
|
const addTags = tags ? trx('tracks_tags') |
||||||
|
.where({ 'trackId': trackId }) |
||||||
|
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
||||||
|
.then((doneTagIds: number[]) => { |
||||||
|
// Get the set of tags that are not yet linked
|
||||||
|
const toLink = tags.filter((id: number) => { |
||||||
|
return !doneTagIds.includes(id); |
||||||
|
}); |
||||||
|
const insertObjects = toLink.map((tagId: number) => { |
||||||
|
return { |
||||||
|
tagId: tagId, |
||||||
|
trackId: trackId, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Link them
|
||||||
|
return Promise.all( |
||||||
|
insertObjects.map((obj: any) => |
||||||
|
trx('tracks_tags').insert(obj) |
||||||
|
) |
||||||
|
); |
||||||
|
}) : undefined; |
||||||
|
|
||||||
|
// Wait for all operations to finish.
|
||||||
|
await Promise.all([ |
||||||
|
modifyTrackPromise, |
||||||
|
removeUnlinkedArtists, |
||||||
|
removeUnlinkedTags, |
||||||
|
addArtists, |
||||||
|
addTags, |
||||||
|
]); |
||||||
|
|
||||||
|
return; |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteTrack(userId: number, trackId: number, knex: Knex): Promise<void> { |
||||||
|
await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// Start by retrieving the track itself for sanity.
|
||||||
|
const confirmTrackId: number | undefined = |
||||||
|
await trx.select('id') |
||||||
|
.from('tracks') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ id: trackId }) |
||||||
|
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
||||||
|
|
||||||
|
if (!confirmTrackId) { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all resources were found.', |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Start deleting artist associations with the track.
|
||||||
|
const deleteArtistsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('artists_tracks') |
||||||
|
.where({ 'trackId': trackId }); |
||||||
|
|
||||||
|
// Start deleting tag associations with the track.
|
||||||
|
const deleteTagsPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('tracks_tags') |
||||||
|
.where({ 'trackId': trackId }); |
||||||
|
|
||||||
|
// Start deleting the track.
|
||||||
|
const deleteTrackPromise: Promise<any> = |
||||||
|
trx.delete() |
||||||
|
.from('tracks') |
||||||
|
.where({ id: trackId }); |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTrackPromise]); |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import Knex from 'knex'; |
||||||
|
|
||||||
|
import { sha512 } from 'js-sha512'; |
||||||
|
import { DBErrorKind, DBError } from '../endpoints/types'; |
||||||
|
|
||||||
|
export async function createUser(user: api.User, knex: Knex): Promise<number> { |
||||||
|
return await knex.transaction(async (trx) => { |
||||||
|
try { |
||||||
|
// check if the user already exists
|
||||||
|
const newUser = (await trx |
||||||
|
.select('id') |
||||||
|
.from('users') |
||||||
|
.where({ email: user.email }))[0]; |
||||||
|
if (newUser) { |
||||||
|
let e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceConflict, |
||||||
|
message: "User with given e-mail already exists.", |
||||||
|
} |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Create the new user.
|
||||||
|
const passwordHash = sha512(user.password); |
||||||
|
const userId = (await trx('users') |
||||||
|
.insert({ |
||||||
|
email: user.email, |
||||||
|
passwordHash: passwordHash, |
||||||
|
}) |
||||||
|
.returning('id') // Needed for Postgres
|
||||||
|
)[0]; |
||||||
|
|
||||||
|
return userId; |
||||||
|
} catch (e) { |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
@ -1,314 +1,113 @@ |
|||||||
import * as api from '../../client/src/api'; |
import * as api from '../../client/src/api/api'; |
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||||
import Knex from 'knex'; |
import Knex from 'knex'; |
||||||
import asJson from '../lib/asJson'; |
import asJson from '../lib/asJson'; |
||||||
|
import { AlbumWithDetails } from '../../client/src/api/api'; |
||||||
|
import { createAlbum, deleteAlbum, getAlbum, modifyAlbum } from '../db/Album'; |
||||||
|
import { GetArtist } from './Artist'; |
||||||
|
|
||||||
export const GetAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const GetAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkAlbumDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid GetAlbum request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
try { |
try { |
||||||
// Start transfers for songs, tags and artists.
|
const maybeAlbum: api.GetAlbumResponse | null = |
||||||
// Also request the album itself.
|
await getAlbum(req.params.id, userId, knex); |
||||||
const tagsPromise: Promise<api.TagDetailsResponseWithId[]> = knex.select('tagId') |
|
||||||
.from('albums_tags') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((tags: any) => { |
|
||||||
return tags.map((tag: any) => tag['tagId']) |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'name', 'parentId']) |
|
||||||
.from('tags') |
|
||||||
.whereIn('id', ids)); |
|
||||||
|
|
||||||
const songsPromise: Promise<api.SongDetailsResponseWithId[]> = knex.select('songId') |
|
||||||
.from('songs_albums') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((songs: any) => { |
|
||||||
return songs.map((song: any) => song['songId']) |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'title', 'storeLinks']) |
|
||||||
.from('songs') |
|
||||||
.whereIn('id', ids)); |
|
||||||
|
|
||||||
const artistsPromise = knex.select('artistId') |
if (maybeAlbum) { |
||||||
.from('artists_albums') |
await res.send(maybeAlbum); |
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((artists: any) => { |
|
||||||
return artists.map((artist: any) => artist['artistId']) |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) |
|
||||||
.from('artists') |
|
||||||
.whereIn('id', ids)); |
|
||||||
|
|
||||||
const albumPromise = knex.select('name', 'storeLinks') |
|
||||||
.from('albums') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: req.params.id }) |
|
||||||
.then((albums: any) => albums[0]); |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
const [album, tags, songs, artists] = |
|
||||||
await Promise.all([albumPromise, tagsPromise, songsPromise, artistsPromise]); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
if (album) { |
|
||||||
const response: api.AlbumDetailsResponse = { |
|
||||||
name: album['name'], |
|
||||||
artists: artists, |
|
||||||
tags: tags, |
|
||||||
songs: songs, |
|
||||||
storeLinks: asJson(album['storeLinks']), |
|
||||||
}; |
|
||||||
await res.send(response); |
|
||||||
} else { |
} else { |
||||||
await res.status(404).send({}); |
await res.status(404).send({}); |
||||||
} |
} |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkCreateAlbumRequest(req)) { |
if (!api.checkPostAlbumRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PostAlbum request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PostAlbum request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.CreateAlbumRequest = req.body; |
const reqObject: api.PostAlbumRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Post Album ", reqObject); |
console.log("User ", userId, ": Post Album ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Start retrieving artists.
|
let id = await createAlbum(userId, reqObject, knex); |
||||||
const artistIdsPromise = reqObject.artistIds ? |
res.status(200).send(id); |
||||||
trx.select('id') |
|
||||||
.from('artists') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', reqObject.artistIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['id'])) : |
|
||||||
(async () => { return [] })(); |
|
||||||
|
|
||||||
// Start retrieving tags.
|
|
||||||
const tagIdsPromise = reqObject.tagIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', reqObject.tagIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['id'])) : |
|
||||||
(async () => { return [] })(); |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [artists, tags] = await Promise.all([artistIdsPromise, tagIdsPromise]);; |
|
||||||
|
|
||||||
// Check that we found all artists and tags we need.
|
|
||||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
|
||||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateAlbum request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Create the album.
|
|
||||||
const albumId = (await trx('albums') |
|
||||||
.insert({ |
|
||||||
name: reqObject.name, |
|
||||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
|
||||||
user: userId, |
|
||||||
}) |
|
||||||
.returning('id') // Needed for Postgres
|
|
||||||
)[0]; |
|
||||||
|
|
||||||
// Link the artists via the linking table.
|
|
||||||
if (artists && artists.length) { |
|
||||||
await trx('artists_albums').insert( |
|
||||||
artists.map((artistId: number) => { |
|
||||||
return { |
|
||||||
artistId: artistId, |
|
||||||
albumId: albumId, |
|
||||||
} |
|
||||||
}) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Link the tags via the linking table.
|
|
||||||
if (tags && tags.length) { |
|
||||||
await trx('albums_tags').insert( |
|
||||||
tags.map((tagId: number) => { |
|
||||||
return { |
|
||||||
albumId: albumId, |
|
||||||
tagId: tagId, |
|
||||||
} |
|
||||||
}) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
const responseObject: api.CreateSongResponse = { |
|
||||||
id: albumId |
|
||||||
}; |
|
||||||
res.status(200).send(responseObject); |
|
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkModifyAlbumRequest(req)) { |
if (!api.checkPutAlbumRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PutAlbum request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PutAlbum request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.ModifyAlbumRequest = req.body; |
const reqObject: api.PutAlbumRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Put Album ", reqObject); |
console.log("User ", userId, ": Put Album ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
|
modifyAlbum(userId, req.params.id, reqObject, knex); |
||||||
|
res.status(200).send(); |
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
// Start retrieving the album itself.
|
export const PatchAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
const albumPromise = trx.select('id') |
if (!api.checkPatchAlbumRequest(req)) { |
||||||
.from('albums') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: req.params.id }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); |
|
||||||
|
|
||||||
// Start retrieving artists.
|
|
||||||
const artistIdsPromise = reqObject.artistIds ? |
|
||||||
trx.select('artistId') |
|
||||||
.from('artists_albums') |
|
||||||
.whereIn('id', reqObject.artistIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['artistId'])) : |
|
||||||
(async () => { return undefined })(); |
|
||||||
|
|
||||||
// Start retrieving tags.
|
|
||||||
const tagIdsPromise = reqObject.tagIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('albums_tags') |
|
||||||
.whereIn('id', reqObject.tagIds) |
|
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
|
||||||
(async () => { return undefined })(); |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [album, artists, tags] = await Promise.all([albumPromise, artistIdsPromise, tagIdsPromise]);; |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
|
||||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
|
||||||
!album) { |
|
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifyAlbum request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PatchAlbum request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
|
const reqObject: api.PatchAlbumRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
// Modify the album.
|
console.log("User ", userId, ": Patch Album ", reqObject); |
||||||
var update: any = {}; |
|
||||||
if ("name" in reqObject) { update["name"] = reqObject.name; } |
|
||||||
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } |
|
||||||
const modifyAlbumPromise = trx('albums') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': req.params.id }) |
|
||||||
.update(update) |
|
||||||
|
|
||||||
// Remove unlinked artists.
|
|
||||||
// TODO: test this!
|
|
||||||
const removeUnlinkedArtists = artists ? trx('artists_albums') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.whereNotIn('artistId', reqObject.artistIds || []) |
|
||||||
.delete() : undefined; |
|
||||||
|
|
||||||
// Remove unlinked tags.
|
|
||||||
// TODO: test this!
|
|
||||||
const removeUnlinkedTags = tags ? trx('albums_tags') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.whereNotIn('tagId', reqObject.tagIds || []) |
|
||||||
.delete() : undefined; |
|
||||||
|
|
||||||
// Link new artists.
|
|
||||||
// TODO: test this!
|
|
||||||
const addArtists = artists ? trx('artists_albums') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((as: any) => as.map((a: any) => a['artistId'])) |
|
||||||
.then((doneArtistIds: number[]) => { |
|
||||||
// Get the set of artists that are not yet linked
|
|
||||||
const toLink = artists.filter((id: number) => { |
|
||||||
return !doneArtistIds.includes(id); |
|
||||||
}); |
|
||||||
const insertObjects = toLink.map((artistId: number) => { |
|
||||||
return { |
|
||||||
artistId: artistId, |
|
||||||
albumId: req.params.id, |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// Link them
|
|
||||||
return Promise.all( |
|
||||||
insertObjects.map((obj: any) => |
|
||||||
trx('artists_albums').insert(obj) |
|
||||||
) |
|
||||||
); |
|
||||||
}) : undefined; |
|
||||||
|
|
||||||
// Link new tags.
|
try { |
||||||
// TODO: test this!
|
modifyAlbum(userId, req.params.id, reqObject, knex); |
||||||
const addTags = tags ? trx('albums_tags') |
res.status(200).send(); |
||||||
.where({ 'albumId': req.params.id }) |
} catch (e) { |
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
handleErrorsInEndpoint(e); |
||||||
.then((doneTagIds: number[]) => { |
|
||||||
// Get the set of tags that are not yet linked
|
|
||||||
const toLink = tags.filter((id: number) => { |
|
||||||
return !doneTagIds.includes(id); |
|
||||||
}); |
|
||||||
const insertObjects = toLink.map((tagId: number) => { |
|
||||||
return { |
|
||||||
tagId: tagId, |
|
||||||
albumId: req.params.id, |
|
||||||
} |
} |
||||||
}) |
} |
||||||
|
|
||||||
// Link them
|
export const DeleteAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
return Promise.all( |
const { id: userId } = req.user; |
||||||
insertObjects.map((obj: any) => |
|
||||||
trx('albums_tags').insert(obj) |
|
||||||
) |
|
||||||
); |
|
||||||
}) : undefined; |
|
||||||
|
|
||||||
// Wait for all operations to finish.
|
console.log("User ", userId, ": Delete Album ", req.params.id); |
||||||
await Promise.all([ |
|
||||||
modifyAlbumPromise, |
|
||||||
removeUnlinkedArtists, |
|
||||||
removeUnlinkedTags, |
|
||||||
addArtists, |
|
||||||
addTags |
|
||||||
]); |
|
||||||
|
|
||||||
// Respond to the request.
|
try { |
||||||
|
await deleteAlbum(userId, req.params.id, knex); |
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
|
export const albumEndpoints: [ string, string, boolean, EndpointHandler ][] = [ |
||||||
|
[ api.PostAlbumEndpoint, 'post', true, PostAlbum ], |
||||||
|
[ api.GetAlbumEndpoint, 'get', true, GetAlbum ], |
||||||
|
[ api.PutAlbumEndpoint, 'put', true, PutAlbum ], |
||||||
|
[ api.PatchAlbumEndpoint, 'patch', true, PatchAlbum ], |
||||||
|
[ api.DeleteAlbumEndpoint, 'delete', true, DeleteAlbum ], |
||||||
|
]; |
||||||
@ -1,221 +1,108 @@ |
|||||||
import * as api from '../../client/src/api'; |
import * as api from '../../client/src/api/api'; |
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||||
import Knex from 'knex'; |
import Knex from 'knex'; |
||||||
import asJson from '../lib/asJson'; |
import asJson from '../lib/asJson'; |
||||||
|
import { createArtist, deleteArtist, getArtist, modifyArtist } from '../db/Artist'; |
||||||
|
|
||||||
export const GetArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const GetArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkArtistDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid GetArtist request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
try { |
try { |
||||||
const tags: api.TagDetailsResponseWithId[] = await knex.select('tagId') |
let artist = await getArtist(req.params.id, userId, knex); |
||||||
.from('artists_tags') |
await res.status(200).send(artist); |
||||||
.where({ 'artistId': req.params.id }) |
|
||||||
.then((ts: any) => { |
|
||||||
return Array.from(new Set( |
|
||||||
ts.map((tag: any) => tag['tagId']) |
|
||||||
)) as number[]; |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'name', 'parentId']) |
|
||||||
.from('tags') |
|
||||||
.whereIn('id', ids)); |
|
||||||
|
|
||||||
const results = await knex.select(['id', 'name', 'storeLinks']) |
|
||||||
.from('artists') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': req.params.id }); |
|
||||||
|
|
||||||
if (results[0]) { |
|
||||||
const response: api.ArtistDetailsResponse = { |
|
||||||
name: results[0].name, |
|
||||||
tags: tags, |
|
||||||
storeLinks: asJson(results[0].storeLinks), |
|
||||||
} |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e) |
handleErrorsInEndpoint(e) |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
export const PostArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PostArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkCreateArtistRequest(req)) { |
if (!api.checkPostArtistRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PostArtist request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PostArtist request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.CreateArtistRequest = req.body; |
const reqObject: api.PostArtistRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Create artist ", reqObject) |
console.log("User ", userId, ": Create artist ", reqObject) |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Retrieve tag instances to link the artist to.
|
const id = await createArtist(userId, reqObject, knex); |
||||||
const tags: number[] = reqObject.tagIds ? |
await res.status(200).send({ id: id }); |
||||||
Array.from(new Set( |
|
||||||
(await trx.select('id').from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', reqObject.tagIds)) |
|
||||||
.map((tag: any) => tag['id']) |
|
||||||
)) |
|
||||||
: []; |
|
||||||
|
|
||||||
if (reqObject.tagIds && tags && tags.length !== reqObject.tagIds.length) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Create the artist.
|
|
||||||
const artistId = (await trx('artists') |
|
||||||
.insert({ |
|
||||||
name: reqObject.name, |
|
||||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
|
||||||
user: userId, |
|
||||||
}) |
|
||||||
.returning('id') // Needed for Postgres
|
|
||||||
)[0]; |
|
||||||
|
|
||||||
// Link the tags via the linking table.
|
|
||||||
if (tags && tags.length) { |
|
||||||
await trx('artists_tags').insert( |
|
||||||
tags.map((tagId: number) => { |
|
||||||
return { |
|
||||||
artistId: artistId, |
|
||||||
tagId: tagId, |
|
||||||
} |
|
||||||
}) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
const responseObject: api.CreateSongResponse = { |
|
||||||
id: artistId |
|
||||||
}; |
|
||||||
await res.status(200).send(responseObject); |
|
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}); |
|
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
export const PutArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PutArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkModifyArtistRequest(req)) { |
if (!api.checkPutArtistRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PutArtist request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PutArtist request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.ModifyArtistRequest = req.body; |
const reqObject: api.PutArtistRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Put Artist ", reqObject); |
console.log("User ", userId, ": Put Artist ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
const artistId = parseInt(req.params.id); |
await modifyArtist(userId, req.params.id, reqObject, knex); |
||||||
|
res.status(200).send(); |
||||||
// Start retrieving the artist itself.
|
|
||||||
const artistPromise = trx.select('id') |
} catch (e) { |
||||||
.from('artists') |
handleErrorsInEndpoint(e); |
||||||
.where({ 'user': userId }) |
} |
||||||
.where({ id: artistId }) |
} |
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
export const PatchArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
// Start retrieving tags.
|
if (!api.checkPatchArtistRequest(req)) { |
||||||
const tagIdsPromise = reqObject.tagIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('artists_tags') |
|
||||||
.whereIn('id', reqObject.tagIds) |
|
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
|
||||||
(async () => { return undefined })(); |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [artist, tags] = await Promise.all([artistPromise, tagIdsPromise]);; |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if ((reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
|
||||||
!artist) { |
|
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Not all artists and/or tags exist for ModifyArtist request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PatchArtist request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
|
const reqObject: api.PatchArtistRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Patch Artist ", reqObject); |
||||||
|
|
||||||
|
try { |
||||||
|
await modifyArtist(userId, req.params.id, reqObject, knex); |
||||||
|
res.status(200).send(); |
||||||
|
|
||||||
// Modify the artist.
|
} catch (e) { |
||||||
var update: any = {}; |
handleErrorsInEndpoint(e); |
||||||
if ("name" in reqObject) { update["name"] = reqObject.name; } |
|
||||||
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } |
|
||||||
const modifyArtistPromise = trx('artists') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': artistId }) |
|
||||||
.update(update) |
|
||||||
|
|
||||||
// Remove unlinked tags.
|
|
||||||
// TODO: test this!
|
|
||||||
const removeUnlinkedTags = tags ? |
|
||||||
trx('artists_tags') |
|
||||||
.where({ 'artistId': artistId }) |
|
||||||
.whereNotIn('tagId', reqObject.tagIds || []) |
|
||||||
.delete() : |
|
||||||
undefined; |
|
||||||
|
|
||||||
// Link new tags.
|
|
||||||
// TODO: test this!
|
|
||||||
const addTags = tags ? trx('artists_tags') |
|
||||||
.where({ 'artistId': artistId }) |
|
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
|
||||||
.then((doneTagIds: number[]) => { |
|
||||||
// Get the set of tags that are not yet linked
|
|
||||||
const toLink = tags.filter((id: number) => { |
|
||||||
return !doneTagIds.includes(id); |
|
||||||
}); |
|
||||||
const insertObjects = toLink.map((tagId: number) => { |
|
||||||
return { |
|
||||||
tagId: tagId, |
|
||||||
artistId: artistId, |
|
||||||
} |
} |
||||||
}) |
} |
||||||
|
|
||||||
// Link them
|
export const DeleteArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
return Promise.all( |
const { id: userId } = req.user; |
||||||
insertObjects.map((obj: any) => |
|
||||||
trx('artists_tags').insert(obj) |
console.log("User ", userId, ": Delete Artist ", req.params.id); |
||||||
) |
|
||||||
); |
try { |
||||||
}) : undefined; |
await deleteArtist(userId, req.params.id, knex); |
||||||
|
|
||||||
// Wait for all operations to finish.
|
|
||||||
await Promise.all([ |
|
||||||
modifyArtistPromise, |
|
||||||
removeUnlinkedTags, |
|
||||||
addTags |
|
||||||
]); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
|
export const artistEndpoints: [ string, string, boolean, EndpointHandler ][] = [ |
||||||
|
[ api.PostArtistEndpoint, 'post', true, PostArtist ], |
||||||
|
[ api.GetArtistEndpoint, 'get', true, GetArtist ], |
||||||
|
[ api.PutArtistEndpoint, 'put', true, PutArtist ], |
||||||
|
[ api.PatchArtistEndpoint, 'patch', true, PatchArtist ], |
||||||
|
[ api.DeleteArtistEndpoint, 'delete', true, DeleteArtist ], |
||||||
|
]; |
||||||
@ -1,206 +1,122 @@ |
|||||||
import * as api from '../../client/src/api'; |
import * as api from '../../client/src/api/api'; |
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||||
import Knex from 'knex'; |
import Knex from 'knex'; |
||||||
import asJson from '../lib/asJson'; |
import asJson from '../lib/asJson'; |
||||||
|
import { createIntegration, deleteIntegration, getIntegration, listIntegrations, modifyIntegration } from '../db/Integration'; |
||||||
|
import { IntegrationDataWithId } from '../../client/src/api/api'; |
||||||
|
|
||||||
export const PostIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PostIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkCreateIntegrationRequest(req)) { |
if (!api.checkPostIntegrationRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PostIntegration request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PostIntegration request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.CreateIntegrationRequest = req.body; |
const reqObject: api.PostIntegrationRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Post Integration ", reqObject); |
console.log("User ", userId, ": Post Integration ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Create the new integration.
|
let id = await createIntegration(userId, reqObject, knex); |
||||||
var integration: any = { |
const responseObject: api.PostIntegrationResponse = { |
||||||
name: reqObject.name, |
id: id |
||||||
user: userId, |
|
||||||
type: reqObject.type, |
|
||||||
details: JSON.stringify(reqObject.details), |
|
||||||
secretDetails: JSON.stringify(reqObject.secretDetails), |
|
||||||
} |
|
||||||
const integrationId = (await trx('integrations') |
|
||||||
.insert(integration) |
|
||||||
.returning('id') // Needed for Postgres
|
|
||||||
)[0]; |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
const responseObject: api.CreateIntegrationResponse = { |
|
||||||
id: integrationId |
|
||||||
}; |
}; |
||||||
res.status(200).send(responseObject); |
res.status(200).send(responseObject); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
export const GetIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const GetIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkIntegrationDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid GetIntegration request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
try { |
try { |
||||||
const integration = (await knex.select(['id', 'name', 'type', 'details']) |
let integration = await getIntegration(req.user.id, req.params.id, knex); |
||||||
.from('integrations') |
res.status(200).send(integration); |
||||||
.where({ 'user': userId, 'id': req.params.id }))[0]; |
|
||||||
|
|
||||||
if (integration) { |
|
||||||
const response: api.IntegrationDetailsResponse = { |
|
||||||
name: integration.name, |
|
||||||
type: integration.type, |
|
||||||
details: asJson(integration.details), |
|
||||||
} |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e) |
handleErrorsInEndpoint(e) |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
export const ListIntegrations: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const ListIntegrations: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkIntegrationDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid ListIntegrations request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("List integrations"); |
console.log("List integrations"); |
||||||
|
|
||||||
try { |
try { |
||||||
const integrations: api.ListIntegrationsResponse = ( |
const integrations: IntegrationDataWithId[] = await listIntegrations(req.user.id, knex); |
||||||
await knex.select(['id', 'name', 'type', 'details']) |
|
||||||
.from('integrations') |
|
||||||
.where({ user: userId }) |
|
||||||
).map((object: any) => { |
|
||||||
return { |
|
||||||
id: object.id, |
|
||||||
name: object.name, |
|
||||||
type: object.type, |
|
||||||
details: asJson(object.details), |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
console.log("Found integrations:", integrations); |
console.log("Found integrations:", integrations); |
||||||
await res.send(integrations); |
await res.status(200).send(integrations); |
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e) |
handleErrorsInEndpoint(e) |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
export const DeleteIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const DeleteIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkDeleteIntegrationRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid DeleteIntegration request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.DeleteIntegrationRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Delete Integration ", reqObject); |
console.log("User ", userId, ": Delete Integration ", req.params.id); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Start retrieving the integration itself.
|
await deleteIntegration(userId, req.params.id, knex); |
||||||
const integrationId = await trx.select('id') |
|
||||||
.from('integrations') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: req.params.id }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if (!integrationId) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Integration does not exist for DeleteIntegration request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 404 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Delete the integration.
|
|
||||||
await trx('integrations') |
|
||||||
.where({ 'user': userId, 'id': integrationId }) |
|
||||||
.del(); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkModifyIntegrationRequest(req)) { |
if (!api.checkPutIntegrationRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PutIntegration request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PutIntegration request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.ModifyIntegrationRequest = req.body; |
const reqObject: api.PutIntegrationRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Put Integration ", reqObject); |
console.log("User ", userId, ": Put Integration ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Start retrieving the integration.
|
await modifyIntegration(userId, req.params.id, reqObject, knex); |
||||||
const integrationId = await trx.select('id') |
res.status(200).send(); |
||||||
.from('integrations') |
|
||||||
.where({ 'user': userId }) |
} catch (e) { |
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
// Check that we found all objects we need.
|
} |
||||||
if (!integrationId) { |
|
||||||
|
export const PatchIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkPatchIntegrationRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Integration does not exist for ModifyIntegration request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
httpStatus: 404 |
message: 'Invalid PatchIntegration request', |
||||||
|
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
|
const reqObject: api.PatchIntegrationRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Patch Integration ", reqObject); |
||||||
|
|
||||||
// Modify the integration.
|
try { |
||||||
var update: any = {}; |
await modifyIntegration(userId, req.params.id, reqObject, knex); |
||||||
if ("name" in reqObject) { update["name"] = reqObject.name; } |
|
||||||
if ("details" in reqObject) { update["details"] = JSON.stringify(reqObject.details); } |
|
||||||
if ("type" in reqObject) { update["type"] = reqObject.type; } |
|
||||||
if ("secretDetails" in reqObject) { update["secretDetails"] = JSON.stringify(reqObject.details); } |
|
||||||
await trx('integrations') |
|
||||||
.where({ 'user': userId, 'id': req.params.id }) |
|
||||||
.update(update) |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
|
export const integrationEndpoints: [string, string, boolean, EndpointHandler][] = [ |
||||||
|
[api.PostIntegrationEndpoint, 'post', true, PostIntegration], |
||||||
|
[api.GetIntegrationEndpoint, 'get', true, GetIntegration], |
||||||
|
[api.PutIntegrationEndpoint, 'put', true, PutIntegration], |
||||||
|
[api.PatchIntegrationEndpoint, 'patch', true, PatchIntegration], |
||||||
|
[api.DeleteIntegrationEndpoint, 'delete', true, DeleteIntegration], |
||||||
|
[api.ListIntegrationsEndpoint, 'get', true, ListIntegrations], |
||||||
|
]; |
||||||
@ -1,49 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
|
|
||||||
export const RegisterUser: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkRegisterUserRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.RegisterUserRequest = req.body; |
|
||||||
|
|
||||||
console.log("Register User: ", reqObject); |
|
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
|
||||||
// check if the user already exists
|
|
||||||
const user = (await trx |
|
||||||
.select('id') |
|
||||||
.from('users') |
|
||||||
.where({ email: reqObject.email }))[0]; |
|
||||||
if(user) { |
|
||||||
res.status(400).send(); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// Create the new user.
|
|
||||||
const passwordHash = sha512(reqObject.password); |
|
||||||
const userId = (await trx('users') |
|
||||||
.insert({ |
|
||||||
email: reqObject.email, |
|
||||||
passwordHash: passwordHash, |
|
||||||
}) |
|
||||||
.returning('id') // Needed for Postgres
|
|
||||||
)[0]; |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
|
||||||
|
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
trx.rollback(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,382 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
import asJson from '../lib/asJson'; |
|
||||||
|
|
||||||
export const PostSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkCreateSongRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid PostSong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.CreateSongRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Post Song ", reqObject); |
|
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
|
||||||
// Start retrieving artists.
|
|
||||||
const artistIdsPromise = reqObject.artistIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('artists') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', reqObject.artistIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['id'])) : |
|
||||||
(async () => { return [] })(); |
|
||||||
|
|
||||||
// Start retrieving tags.
|
|
||||||
const tagIdsPromise = reqObject.tagIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', reqObject.tagIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['id'])) : |
|
||||||
(async () => { return [] })(); |
|
||||||
|
|
||||||
// Start retrieving albums.
|
|
||||||
const albumIdsPromise = reqObject.albumIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('albums') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', reqObject.albumIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['id'])) : |
|
||||||
(async () => { return [] })(); |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [artists, tags, albums] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdsPromise]);; |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
|
||||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
|
||||||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Create the song.
|
|
||||||
const songId = (await trx('songs') |
|
||||||
.insert({ |
|
||||||
title: reqObject.title, |
|
||||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
|
||||||
user: userId, |
|
||||||
}) |
|
||||||
.returning('id') // Needed for Postgres
|
|
||||||
)[0]; |
|
||||||
|
|
||||||
// Link the artists via the linking table.
|
|
||||||
if (artists && artists.length) { |
|
||||||
await Promise.all( |
|
||||||
artists.map((artistId: number) => { |
|
||||||
return trx('songs_artists').insert({ |
|
||||||
artistId: artistId, |
|
||||||
songId: songId, |
|
||||||
}) |
|
||||||
}) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Link the tags via the linking table.
|
|
||||||
if (tags && tags.length) { |
|
||||||
await Promise.all( |
|
||||||
tags.map((tagId: number) => { |
|
||||||
return trx('songs_tags').insert({ |
|
||||||
songId: songId, |
|
||||||
tagId: tagId, |
|
||||||
}) |
|
||||||
}) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Link the albums via the linking table.
|
|
||||||
if (albums && albums.length) { |
|
||||||
await Promise.all( |
|
||||||
albums.map((albumId: number) => { |
|
||||||
return trx('songs_albums').insert({ |
|
||||||
songId: songId, |
|
||||||
albumId: albumId, |
|
||||||
}) |
|
||||||
}) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
const responseObject: api.CreateSongResponse = { |
|
||||||
id: songId |
|
||||||
}; |
|
||||||
res.status(200).send(responseObject); |
|
||||||
|
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
trx.rollback(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const GetSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkSongDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid GetSong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
try { |
|
||||||
const tagsPromise: Promise<api.TagDetailsResponseWithId[]> = knex.select('tagId') |
|
||||||
.from('songs_tags') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.then((ts: any) => { |
|
||||||
return Array.from(new Set( |
|
||||||
ts.map((tag: any) => tag['tagId']) |
|
||||||
)) as number[]; |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'name', 'parentId']) |
|
||||||
.from('tags') |
|
||||||
.whereIn('id', ids)) |
|
||||||
|
|
||||||
const albumsPromise: Promise<api.AlbumDetailsResponseWithId[]> = knex.select('albumId') |
|
||||||
.from('songs_albums') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.then((as: any) => { |
|
||||||
return Array.from(new Set( |
|
||||||
as.map((album: any) => album['albumId']) |
|
||||||
)) as number[]; |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) |
|
||||||
.from('albums') |
|
||||||
.whereIn('id', ids)) |
|
||||||
|
|
||||||
const artistsPromise: Promise<api.ArtistDetailsResponseWithId[]> = knex.select('artistId') |
|
||||||
.from('songs_artists') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.then((as: any) => { |
|
||||||
return Array.from(new Set( |
|
||||||
as.map((artist: any) => artist['artistId']) |
|
||||||
)) as number[]; |
|
||||||
}) |
|
||||||
.then((ids: number[]) => knex.select(['id', 'name', 'storeLinks']) |
|
||||||
.from('albums') |
|
||||||
.whereIn('id', ids)) |
|
||||||
|
|
||||||
const songPromise = await knex.select(['id', 'title', 'storeLinks']) |
|
||||||
.from('songs') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': req.params.id }) |
|
||||||
.then((ss: any) => ss[0]) |
|
||||||
|
|
||||||
const [tags, albums, artists, song] = |
|
||||||
await Promise.all([tagsPromise, albumsPromise, artistsPromise, songPromise]); |
|
||||||
|
|
||||||
if (song) { |
|
||||||
const response: api.SongDetailsResponse = { |
|
||||||
title: song.title, |
|
||||||
tags: tags, |
|
||||||
artists: artists, |
|
||||||
albums: albums, |
|
||||||
storeLinks: asJson(song.storeLinks), |
|
||||||
} |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
export const PutSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkModifySongRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid PutSong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.ModifySongRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Put Song ", reqObject); |
|
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
|
||||||
// Retrieve the song to be modified itself.
|
|
||||||
const songPromise = trx.select('id') |
|
||||||
.from('songs') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: req.params.id }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
|
||||||
// Start retrieving artists.
|
|
||||||
const artistIdsPromise = reqObject.artistIds ? |
|
||||||
trx.select('artistId') |
|
||||||
.from('songs_artists') |
|
||||||
.whereIn('id', reqObject.artistIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['artistId'])) : |
|
||||||
(async () => { return undefined })(); |
|
||||||
|
|
||||||
// Start retrieving tags.
|
|
||||||
const tagIdsPromise = reqObject.tagIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('songs_tags') |
|
||||||
.whereIn('id', reqObject.tagIds) |
|
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
|
||||||
(async () => { return undefined })(); |
|
||||||
|
|
||||||
// Start retrieving albums.
|
|
||||||
const albumIdsPromise = reqObject.albumIds ? |
|
||||||
trx.select('id') |
|
||||||
.from('songs_albums') |
|
||||||
.whereIn('id', reqObject.albumIds) |
|
||||||
.then((as: any) => as.map((a: any) => a['albumId'])) : |
|
||||||
(async () => { return undefined })(); |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [song, artists, tags, albums] = |
|
||||||
await Promise.all([songPromise, artistIdsPromise, tagIdsPromise, albumIdsPromise]);; |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
|
||||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
|
||||||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length) || |
|
||||||
!song) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifySong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Modify the song.
|
|
||||||
var update: any = {}; |
|
||||||
if ("title" in reqObject) { update["title"] = reqObject.title; } |
|
||||||
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } |
|
||||||
const modifySongPromise = trx('songs') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': req.params.id }) |
|
||||||
.update(update) |
|
||||||
|
|
||||||
// Remove unlinked artists.
|
|
||||||
// TODO: test this!
|
|
||||||
const removeUnlinkedArtists = artists ? trx('songs_artists') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.whereNotIn('artistId', reqObject.artistIds || []) |
|
||||||
.delete() : undefined; |
|
||||||
|
|
||||||
// Remove unlinked tags.
|
|
||||||
// TODO: test this!
|
|
||||||
const removeUnlinkedTags = tags ? trx('songs_tags') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.whereNotIn('tagId', reqObject.tagIds || []) |
|
||||||
.delete() : undefined; |
|
||||||
|
|
||||||
// Remove unlinked albums.
|
|
||||||
// TODO: test this!
|
|
||||||
const removeUnlinkedAlbums = albums ? trx('songs_albums') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.whereNotIn('albumId', reqObject.albumIds || []) |
|
||||||
.delete() : undefined; |
|
||||||
|
|
||||||
// Link new artists.
|
|
||||||
// TODO: test this!
|
|
||||||
const addArtists = artists ? trx('songs_artists') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.then((as: any) => as.map((a: any) => a['artistId'])) |
|
||||||
.then((doneArtistIds: number[]) => { |
|
||||||
// Get the set of artists that are not yet linked
|
|
||||||
const toLink = artists.filter((id: number) => { |
|
||||||
return !doneArtistIds.includes(id); |
|
||||||
}); |
|
||||||
const insertObjects = toLink.map((artistId: number) => { |
|
||||||
return { |
|
||||||
artistId: artistId, |
|
||||||
songId: req.params.id, |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// Link them
|
|
||||||
return Promise.all( |
|
||||||
insertObjects.map((obj: any) => |
|
||||||
trx('songs_artists').insert(obj) |
|
||||||
) |
|
||||||
); |
|
||||||
}) : undefined; |
|
||||||
|
|
||||||
// Link new tags.
|
|
||||||
// TODO: test this!
|
|
||||||
const addTags = tags ? trx('songs_tags') |
|
||||||
.where({ 'songId': req.params.id }) |
|
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
|
||||||
.then((doneTagIds: number[]) => { |
|
||||||
// Get the set of tags that are not yet linked
|
|
||||||
const toLink = tags.filter((id: number) => { |
|
||||||
return !doneTagIds.includes(id); |
|
||||||
}); |
|
||||||
const insertObjects = toLink.map((tagId: number) => { |
|
||||||
return { |
|
||||||
tagId: tagId, |
|
||||||
songId: req.params.id, |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// Link them
|
|
||||||
return Promise.all( |
|
||||||
insertObjects.map((obj: any) => |
|
||||||
trx('songs_tags').insert(obj) |
|
||||||
) |
|
||||||
); |
|
||||||
}) : undefined; |
|
||||||
|
|
||||||
// Link new albums.
|
|
||||||
// TODO: test this!
|
|
||||||
const addAlbums = albums ? trx('songs_albums') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((as: any) => as.map((a: any) => a['albumId'])) |
|
||||||
.then((doneAlbumIds: number[]) => { |
|
||||||
// Get the set of albums that are not yet linked
|
|
||||||
const toLink = albums.filter((id: number) => { |
|
||||||
return !doneAlbumIds.includes(id); |
|
||||||
}); |
|
||||||
const insertObjects = toLink.map((albumId: number) => { |
|
||||||
return { |
|
||||||
albumId: albumId, |
|
||||||
songId: req.params.id, |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// Link them
|
|
||||||
return Promise.all( |
|
||||||
insertObjects.map((obj: any) => |
|
||||||
trx('songs_albums').insert(obj) |
|
||||||
) |
|
||||||
); |
|
||||||
}) : undefined; |
|
||||||
|
|
||||||
// Wait for all operations to finish.
|
|
||||||
await Promise.all([ |
|
||||||
modifySongPromise, |
|
||||||
removeUnlinkedArtists, |
|
||||||
removeUnlinkedTags, |
|
||||||
removeUnlinkedAlbums, |
|
||||||
addArtists, |
|
||||||
addTags, |
|
||||||
addAlbums, |
|
||||||
]); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
|
||||||
|
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
trx.rollback(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,306 +1,124 @@ |
|||||||
import * as api from '../../client/src/api'; |
import * as api from '../../client/src/api/api'; |
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||||
import Knex from 'knex'; |
import Knex from 'knex'; |
||||||
|
import { createTag, deleteTag, getTag, mergeTag, modifyTag } from '../db/Tag'; |
||||||
|
|
||||||
export const PostTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PostTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkCreateTagRequest(req)) { |
if (!api.checkPostTagRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PostTag request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PostTag request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.CreateTagRequest = req.body; |
const reqObject: api.PostTagRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Post Tag ", reqObject); |
console.log("User ", userId, ": Post Tag ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// If applicable, retrieve the parent tag.
|
|
||||||
const maybeParent: number | undefined = |
|
||||||
reqObject.parentId ? |
|
||||||
(await trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': reqObject.parentId }))[0]['id'] : |
|
||||||
undefined; |
|
||||||
|
|
||||||
// Check if the parent was found, if applicable.
|
|
||||||
if (reqObject.parentId && maybeParent !== reqObject.parentId) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Could not find parent tag for CreateTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Create the new tag.
|
|
||||||
var tag: any = { |
|
||||||
name: reqObject.name, |
|
||||||
user: userId, |
|
||||||
}; |
|
||||||
if (maybeParent) { |
|
||||||
tag['parentId'] = maybeParent; |
|
||||||
} |
|
||||||
const tagId = (await trx('tags') |
|
||||||
.insert(tag) |
|
||||||
.returning('id') // Needed for Postgres
|
|
||||||
)[0]; |
|
||||||
|
|
||||||
// Respond to the request.
|
// Respond to the request.
|
||||||
const responseObject: api.CreateTagResponse = { |
const responseObject: api.PostTagResponse = { |
||||||
id: tagId |
id: await createTag(userId, reqObject, knex) |
||||||
}; |
}; |
||||||
res.status(200).send(responseObject); |
res.status(200).send(responseObject); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
async function getChildrenRecursive(id: number, userId: number, trx: any) { |
|
||||||
const directChildren = (await trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'parentId': id })).map((r: any) => r.id); |
|
||||||
|
|
||||||
const indirectChildrenPromises = directChildren.map( |
|
||||||
(child: number) => getChildrenRecursive(child, userId, trx) |
|
||||||
); |
|
||||||
const indirectChildrenNested = await Promise.all(indirectChildrenPromises); |
|
||||||
const indirectChildren = indirectChildrenNested.flat(); |
|
||||||
|
|
||||||
return [ |
|
||||||
...directChildren, |
|
||||||
...indirectChildren, |
|
||||||
] |
|
||||||
} |
|
||||||
|
|
||||||
export const DeleteTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const DeleteTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkDeleteTagRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid DeleteTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.DeleteTagRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
console.log("User ", userId, ": Delete Tag ", req.params.id); |
||||||
|
|
||||||
console.log("User ", userId, ": Delete Tag ", reqObject); |
|
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Start retrieving any child tags.
|
|
||||||
const childTagsPromise =
|
|
||||||
getChildrenRecursive(req.params.id, userId, trx); |
|
||||||
|
|
||||||
// Start retrieving the tag itself.
|
|
||||||
const tagPromise = trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: req.params.id }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [tag, children] = await Promise.all([tagPromise, childTagsPromise]); |
|
||||||
|
|
||||||
// Merge all IDs.
|
|
||||||
const toDelete = [ tag, ...children ]; |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
deleteTag(userId, req.params.id, knex); |
||||||
if (!tag) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Tag or parent does not exist for DeleteTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Delete the tag and its children.
|
|
||||||
await trx('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.whereIn('id', toDelete) |
|
||||||
.del(); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
export const GetTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const GetTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkTagDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid GetTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
try { |
try { |
||||||
const results = await knex.select(['id', 'name', 'parentId']) |
let tag = await getTag(req.params.id, userId, knex); |
||||||
.from('tags') |
await res.status(200).send(tag); |
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': req.params.id }); |
|
||||||
|
|
||||||
if (results[0]) { |
|
||||||
const response: api.TagDetailsResponse = { |
|
||||||
name: results[0].name, |
|
||||||
parentId: results[0].parentId || undefined, |
|
||||||
} |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e) |
handleErrorsInEndpoint(e) |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkModifyTagRequest(req)) { |
if (!api.checkPutTagRequest(req)) { |
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Invalid PutTag request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PutTag request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
const reqObject: api.ModifyTagRequest = req.body; |
const reqObject: api.PutTagRequest = req.body; |
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Put Tag ", reqObject); |
console.log("User ", userId, ": Put Tag ", reqObject); |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Start retrieving the parent tag.
|
await modifyTag(userId, req.params.id, reqObject, knex); |
||||||
const parentTagPromise = reqObject.parentId ? |
res.status(200).send(); |
||||||
trx.select('id') |
} catch (e) { |
||||||
.from('tags') |
handleErrorsInEndpoint(e); |
||||||
.where({ 'user': userId }) |
} |
||||||
.where({ 'id': reqObject.parentId }) |
} |
||||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
|
||||||
(async () => { return [] })(); |
|
||||||
|
|
||||||
// Start retrieving the tag itself.
|
|
||||||
const tagPromise = trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: req.params.id }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [tag, parent] = await Promise.all([tagPromise, parentTagPromise]);; |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
export const PatchTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if ((reqObject.parentId && !parent) || |
if (!api.checkPatchTagRequest(req)) { |
||||||
!tag) { |
|
||||||
const e: EndpointError = { |
const e: EndpointError = { |
||||||
internalMessage: 'Tag or parent does not exist for ModifyTag request: ' + JSON.stringify(req.body), |
name: "EndpointError", |
||||||
|
message: 'Invalid PatchTag request', |
||||||
httpStatus: 400 |
httpStatus: 400 |
||||||
}; |
}; |
||||||
throw e; |
throw e; |
||||||
} |
} |
||||||
|
const reqObject: api.PatchTagRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
// Modify the tag.
|
console.log("User ", userId, ": Patch Tag ", reqObject); |
||||||
await trx('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': req.params.id }) |
|
||||||
.update({ |
|
||||||
name: reqObject.name, |
|
||||||
parentId: reqObject.parentId || null, |
|
||||||
}) |
|
||||||
|
|
||||||
// Respond to the request.
|
try { |
||||||
|
await modifyTag(userId, req.params.id, reqObject, knex); |
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
if (!api.checkMergeTagRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.DeleteTagRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
const { id: userId } = req.user; |
||||||
|
|
||||||
console.log("User ", userId, ": Merge Tag ", reqObject); |
console.log("User ", userId, ": Merge Tag ", req.params.id, req.params.toId); |
||||||
const fromId = req.params.id; |
const fromId = req.params.id; |
||||||
const toId = req.params.toId; |
const toId = req.params.toId; |
||||||
|
|
||||||
await knex.transaction(async (trx) => { |
|
||||||
try { |
try { |
||||||
// Start retrieving the "from" tag.
|
mergeTag(userId, fromId, toId, knex); |
||||||
const fromTagPromise = trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: fromId }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
|
||||||
// Start retrieving the "to" tag.
|
|
||||||
const toTagPromise = trx.select('id') |
|
||||||
.from('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ id: toId }) |
|
||||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if (!fromTag || !toTag) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Assign new tag ID to any objects referencing the to-be-merged tag.
|
|
||||||
const cPromise = trx('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'parentId': fromId }) |
|
||||||
.update({ 'parentId': toId }); |
|
||||||
const sPromise = trx('songs_tags') |
|
||||||
.where({ 'tagId': fromId }) |
|
||||||
.update({ 'tagId': toId }); |
|
||||||
const arPromise = trx('artists_tags') |
|
||||||
.where({ 'tagId': fromId }) |
|
||||||
.update({ 'tagId': toId }); |
|
||||||
const alPromise = trx('albums_tags') |
|
||||||
.where({ 'tagId': fromId }) |
|
||||||
.update({ 'tagId': toId }); |
|
||||||
await Promise.all([sPromise, arPromise, alPromise, cPromise]); |
|
||||||
|
|
||||||
// Delete the original tag.
|
|
||||||
await trx('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': fromId }) |
|
||||||
.del(); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
res.status(200).send(); |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
catchUnhandledErrors(e); |
handleErrorsInEndpoint(e); |
||||||
trx.rollback(); |
|
||||||
} |
} |
||||||
}) |
|
||||||
} |
} |
||||||
|
|
||||||
|
export const tagEndpoints: [ string, string, boolean, EndpointHandler ][] = [ |
||||||
|
[ api.PostTagEndpoint, 'post', true, PostTag ], |
||||||
|
[ api.GetTagEndpoint, 'get', true, GetTag ], |
||||||
|
[ api.PutTagEndpoint, 'put', true, PutTag ], |
||||||
|
[ api.PatchTagEndpoint, 'patch', true, PatchTag ], |
||||||
|
[ api.DeleteTagEndpoint, 'delete', true, DeleteTag ], |
||||||
|
[ api.MergeTagEndpoint, 'post', true, MergeTag ], |
||||||
|
]; |
||||||
@ -0,0 +1,106 @@ |
|||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||||
|
import Knex from 'knex'; |
||||||
|
import asJson from '../lib/asJson'; |
||||||
|
import { createTrack, deleteTrack, getTrack, modifyTrack } from '../db/Track'; |
||||||
|
|
||||||
|
export const PostTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkPostTrackRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
name: "EndpointError", |
||||||
|
message: 'Invalid PostTrack request', |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.PostTrackRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Post Track ", reqObject); |
||||||
|
|
||||||
|
try { |
||||||
|
res.status(200).send({ |
||||||
|
id: await createTrack(userId, reqObject, knex) |
||||||
|
}); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const GetTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
try { |
||||||
|
let track = await getTrack(req.params.id, userId, knex); |
||||||
|
await res.status(200).send(track); |
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const PutTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkPutTrackRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
name: "EndpointError", |
||||||
|
message: 'Invalid PutTrack request', |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.PutTrackRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Put Track ", reqObject); |
||||||
|
|
||||||
|
try { |
||||||
|
modifyTrack(userId, req.params.id, reqObject, knex); |
||||||
|
res.status(200).send(); |
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const PatchTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkPatchTrackRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
name: "EndpointError", |
||||||
|
message: 'Invalid PatchTrack request', |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.PatchTrackRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Patch Track ", reqObject); |
||||||
|
|
||||||
|
try { |
||||||
|
modifyTrack(userId, req.params.id, reqObject, knex); |
||||||
|
res.status(200).send(); |
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const DeleteTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Delete Track ", req.params.id); |
||||||
|
|
||||||
|
try { |
||||||
|
await deleteTrack(userId, req.params.id, knex); |
||||||
|
res.status(200).send(); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const trackEndpoints: [ string, string, boolean, EndpointHandler ][] = [ |
||||||
|
[ api.PostTrackEndpoint, 'post', true, PostTrack ], |
||||||
|
[ api.GetTrackEndpoint, 'get', true, GetTrack ], |
||||||
|
[ api.PutTrackEndpoint, 'put', true, PutTrack ], |
||||||
|
[ api.PatchTrackEndpoint, 'patch', true, PatchTrack ], |
||||||
|
[ api.DeleteTrackEndpoint, 'delete', true, DeleteTrack ], |
||||||
|
]; |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import * as api from '../../client/src/api/api'; |
||||||
|
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||||
|
import Knex from 'knex'; |
||||||
|
|
||||||
|
import { sha512 } from 'js-sha512'; |
||||||
|
import { createUser } from '../db/User'; |
||||||
|
|
||||||
|
export const RegisterUser: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkRegisterUserRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
name: "EndpointError", |
||||||
|
message: 'Invalid RegisterUser request', |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.RegisterUserRequest = req.body; |
||||||
|
|
||||||
|
console.log("Register User: ", reqObject); |
||||||
|
try { |
||||||
|
await createUser(reqObject, knex); |
||||||
|
res.status(200).send(); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
handleErrorsInEndpoint(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const userEndpoints: [ string, string, boolean, EndpointHandler ][] = [ |
||||||
|
[ api.RegisterUserEndpoint, 'post', false, RegisterUser ], |
||||||
|
]; |
||||||
@ -1,44 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import asJson from './asJson'; |
|
||||||
|
|
||||||
export function toApiTag(dbObj: any): api.TagDetails { |
|
||||||
return <api.TagDetails>{ |
|
||||||
tagId: dbObj['tags.id'], |
|
||||||
name: dbObj['tags.name'], |
|
||||||
parentId: dbObj['tags.parentId'], |
|
||||||
parent: dbObj.parent ? toApiTag(dbObj.parent) : undefined, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
export function toApiArtist(dbObj: any) { |
|
||||||
return <api.ArtistDetails>{ |
|
||||||
artistId: dbObj['artists.id'], |
|
||||||
name: dbObj['artists.name'], |
|
||||||
storeLinks: asJson(dbObj['artists.storeLinks']), |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
export function toApiSong(dbObj: any, artists: any[], tags: any[], albums: any[]) { |
|
||||||
return <api.SongDetails>{ |
|
||||||
songId: dbObj['songs.id'], |
|
||||||
title: dbObj['songs.title'], |
|
||||||
storeLinks: asJson(dbObj['songs.storeLinks']), |
|
||||||
artists: artists.map((artist: any) => { |
|
||||||
return toApiArtist(artist); |
|
||||||
}), |
|
||||||
tags: tags.map((tag: any) => { |
|
||||||
return toApiTag(tag); |
|
||||||
}), |
|
||||||
albums: albums.map((album: any) => { |
|
||||||
return toApiAlbum(album); |
|
||||||
}), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function toApiAlbum(dbObj: any) { |
|
||||||
return <api.AlbumDetails>{ |
|
||||||
albumId: dbObj['albums.id'], |
|
||||||
name: dbObj['albums.name'], |
|
||||||
storeLinks: asJson(dbObj['albums.storeLinks']), |
|
||||||
}; |
|
||||||
} |
|
||||||
@ -1,73 +0,0 @@ |
|||||||
import * as Knex from "knex"; |
|
||||||
import { sha512 } from "js-sha512"; |
|
||||||
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> { |
|
||||||
// Users table.
|
|
||||||
await knex.schema.createTable( |
|
||||||
'users', |
|
||||||
(table: any) => { |
|
||||||
table.increments('id'); |
|
||||||
table.string('email'); |
|
||||||
table.string('passwordHash') |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
// Add user column to other object tables.
|
|
||||||
await knex.schema.alterTable( |
|
||||||
'songs', |
|
||||||
(table: any) => { |
|
||||||
table.integer('user').unsigned().notNullable().defaultTo(1); |
|
||||||
} |
|
||||||
) |
|
||||||
await knex.schema.alterTable( |
|
||||||
'albums', |
|
||||||
(table: any) => { |
|
||||||
table.integer('user').unsigned().notNullable().defaultTo(1); |
|
||||||
} |
|
||||||
) |
|
||||||
await knex.schema.alterTable( |
|
||||||
'tags', |
|
||||||
(table: any) => { |
|
||||||
table.integer('user').unsigned().notNullable().defaultTo(1); |
|
||||||
} |
|
||||||
) |
|
||||||
await knex.schema.alterTable( |
|
||||||
'artists', |
|
||||||
(table: any) => { |
|
||||||
table.integer('user').unsigned().notNullable().defaultTo(1); |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> { |
|
||||||
await knex.schema.dropTable('users'); |
|
||||||
|
|
||||||
// Remove the user column
|
|
||||||
await knex.schema.alterTable( |
|
||||||
'songs', |
|
||||||
(table: any) => { |
|
||||||
table.dropColumn('user'); |
|
||||||
} |
|
||||||
) |
|
||||||
await knex.schema.alterTable( |
|
||||||
'albums', |
|
||||||
(table: any) => { |
|
||||||
table.dropColumn('user'); |
|
||||||
} |
|
||||||
) |
|
||||||
await knex.schema.alterTable( |
|
||||||
'tags', |
|
||||||
(table: any) => { |
|
||||||
table.dropColumn('user'); |
|
||||||
} |
|
||||||
) |
|
||||||
await knex.schema.alterTable( |
|
||||||
'artists', |
|
||||||
(table: any) => { |
|
||||||
table.dropColumn('user'); |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,24 +0,0 @@ |
|||||||
import * as Knex from "knex"; |
|
||||||
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> { |
|
||||||
// Integrations table.
|
|
||||||
await knex.schema.createTable( |
|
||||||
'integrations', |
|
||||||
(table: any) => { |
|
||||||
table.increments('id'); |
|
||||||
table.integer('user').unsigned().notNullable().defaultTo(1); |
|
||||||
table.string('name').notNullable(); // Uniquely identifies this integration configuration for the user.
|
|
||||||
table.string('type').notNullable(); // Enumerates different supported integration types (e.g. Spotify)
|
|
||||||
table.json('details'); // Stores anything that might be needed for the integration to work.
|
|
||||||
table.json('secretDetails'); // Stores anything that might be needed for the integration to work and which
|
|
||||||
// should never leave the server.
|
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> { |
|
||||||
await knex.schema.dropTable('integrations'); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,58 +0,0 @@ |
|||||||
import * as Knex from "knex"; |
|
||||||
|
|
||||||
/* |
|
||||||
This migration converts the storeLinks column from JSON to plain text. |
|
||||||
The reason is that there are too many differences between the JSON support |
|
||||||
of different back-ends, making plain text easier to deal with. |
|
||||||
*/ |
|
||||||
|
|
||||||
async function castStoreLinks(table: string, knex: any) { |
|
||||||
await knex.schema.alterTable(table, (t: any) => { |
|
||||||
t.string('storeLinksTemp'); |
|
||||||
}); |
|
||||||
await knex(table).update({ |
|
||||||
storeLinksTemp: knex.raw('CAST("storeLinks" AS VARCHAR(255))') |
|
||||||
}) |
|
||||||
await knex.schema.alterTable(table, (t: any) => { |
|
||||||
t.dropColumn('storeLinks'); |
|
||||||
}); |
|
||||||
await knex.schema.alterTable(table, (t: any) => { |
|
||||||
t.renameColumn('storeLinksTemp', 'storeLinks'); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function revertStoreLinks(table: string, knex: any) { |
|
||||||
await knex.schema.alterTable(table, (t: any) => { |
|
||||||
t.json('storeLinksTemp'); |
|
||||||
}); |
|
||||||
if (knex.client.config.client === 'sqlite3') { |
|
||||||
await knex(table).update({ |
|
||||||
storeLinksTemp: knex.raw('"storeLinks"') |
|
||||||
}) |
|
||||||
} else { |
|
||||||
await knex(table).update({ |
|
||||||
storeLinksTemp: knex.raw('CAST("storeLinks" AS json)') |
|
||||||
}) |
|
||||||
} |
|
||||||
await knex.schema.alterTable(table, (t: any) => { |
|
||||||
t.dropColumn('storeLinks'); |
|
||||||
}); |
|
||||||
await knex.schema.alterTable(table, (t: any) => { |
|
||||||
t.renameColumn('storeLinksTemp', 'storeLinks'); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> { |
|
||||||
console.log("Knex client:", knex.client.config); |
|
||||||
await castStoreLinks('songs', knex); |
|
||||||
await castStoreLinks('albums', knex); |
|
||||||
await castStoreLinks('artists', knex); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> { |
|
||||||
await revertStoreLinks('songs', knex); |
|
||||||
await revertStoreLinks('albums', knex); |
|
||||||
await revertStoreLinks('artists', knex); |
|
||||||
} |
|
||||||
|
|
||||||
Loading…
Reference in new issue