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 { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import * as api from '../../client/src/api/api'; |
||||
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||
import Knex from 'knex'; |
||||
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) => { |
||||
if (!api.checkAlbumDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid GetAlbum request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
// Start transfers for songs, tags and artists.
|
||||
// Also request the album itself.
|
||||
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 maybeAlbum: api.GetAlbumResponse | null = |
||||
await getAlbum(req.params.id, userId, knex); |
||||
|
||||
const artistsPromise = knex.select('artistId') |
||||
.from('artists_albums') |
||||
.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); |
||||
if (maybeAlbum) { |
||||
await res.send(maybeAlbum); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateAlbumRequest(req)) { |
||||
if (!api.checkPostAlbumRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostAlbum request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PostAlbum request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateAlbumRequest = req.body; |
||||
const reqObject: api.PostAlbumRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Post Album ", 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 [] })(); |
||||
|
||||
// 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) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
try { |
||||
let id = await createAlbum(userId, reqObject, knex); |
||||
res.status(200).send(id); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyAlbumRequest(req)) { |
||||
if (!api.checkPutAlbumRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutAlbum request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PutAlbum request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyAlbumRequest = req.body; |
||||
const reqObject: api.PutAlbumRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Album ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
|
||||
// Start retrieving the album itself.
|
||||
const albumPromise = trx.select('id') |
||||
.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 = { |
||||
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifyAlbum request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the album.
|
||||
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; |
||||
try { |
||||
modifyAlbum(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
// Remove unlinked tags.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedTags = tags ? trx('albums_tags') |
||||
.where({ 'albumId': req.params.id }) |
||||
.whereNotIn('tagId', reqObject.tagIds || []) |
||||
.delete() : undefined; |
||||
export const PatchAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkPatchAlbumRequest(req)) { |
||||
const e: EndpointError = { |
||||
name: "EndpointError", |
||||
message: 'Invalid PatchAlbum request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.PatchAlbumRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
// 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, |
||||
} |
||||
}) |
||||
console.log("User ", userId, ": Patch Album ", reqObject); |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('artists_albums').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
try { |
||||
modifyAlbum(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
// Link new tags.
|
||||
// TODO: test this!
|
||||
const addTags = tags ? trx('albums_tags') |
||||
.where({ 'albumId': 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, |
||||
albumId: req.params.id, |
||||
} |
||||
}) |
||||
export const DeleteAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
const { id: userId } = req.user; |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('albums_tags').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
console.log("User ", userId, ": Delete Album ", req.params.id); |
||||
|
||||
// Wait for all operations to finish.
|
||||
await Promise.all([ |
||||
modifyAlbumPromise, |
||||
removeUnlinkedArtists, |
||||
removeUnlinkedTags, |
||||
addArtists, |
||||
addTags |
||||
]); |
||||
try { |
||||
await deleteAlbum(userId, req.params.id, knex); |
||||
res.status(200).send(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(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 { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import * as api from '../../client/src/api/api'; |
||||
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||
import Knex from 'knex'; |
||||
import asJson from '../lib/asJson'; |
||||
import { createArtist, deleteArtist, getArtist, modifyArtist } from '../db/Artist'; |
||||
|
||||
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; |
||||
|
||||
try { |
||||
const tags: api.TagDetailsResponseWithId[] = await knex.select('tagId') |
||||
.from('artists_tags') |
||||
.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({}); |
||||
} |
||||
let artist = await getArtist(req.params.id, userId, knex); |
||||
await res.status(200).send(artist); |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
handleErrorsInEndpoint(e) |
||||
} |
||||
} |
||||
|
||||
export const PostArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateArtistRequest(req)) { |
||||
if (!api.checkPostArtistRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostArtist request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PostArtist request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateArtistRequest = req.body; |
||||
const reqObject: api.PostArtistRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Create artist ", reqObject) |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Retrieve tag instances to link the artist to.
|
||||
const tags: number[] = reqObject.tagIds ? |
||||
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) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}); |
||||
try { |
||||
const id = await createArtist(userId, reqObject, knex); |
||||
await res.status(200).send({ id: id }); |
||||
|
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
|
||||
export const PutArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyArtistRequest(req)) { |
||||
if (!api.checkPutArtistRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutArtist request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PutArtist request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyArtistRequest = req.body; |
||||
const reqObject: api.PutArtistRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Artist ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
const artistId = parseInt(req.params.id); |
||||
|
||||
// Start retrieving the artist itself.
|
||||
const artistPromise = trx.select('id') |
||||
.from('artists') |
||||
.where({ 'user': userId }) |
||||
.where({ id: artistId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Start retrieving tags.
|
||||
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 = { |
||||
internalMessage: 'Not all artists and/or tags exist for ModifyArtist request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the artist.
|
||||
var update: any = {}; |
||||
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
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('artists_tags').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Wait for all operations to finish.
|
||||
await Promise.all([ |
||||
modifyArtistPromise, |
||||
removeUnlinkedTags, |
||||
addTags |
||||
]); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
try { |
||||
await modifyArtist(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const PatchArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkPatchArtistRequest(req)) { |
||||
const e: EndpointError = { |
||||
name: "EndpointError", |
||||
message: 'Invalid PatchArtist request', |
||||
httpStatus: 400 |
||||
}; |
||||
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(); |
||||
|
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const DeleteArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Delete Artist ", req.params.id); |
||||
|
||||
try { |
||||
await deleteArtist(userId, req.params.id, knex); |
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
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 { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import * as api from '../../client/src/api/api'; |
||||
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||
import Knex from 'knex'; |
||||
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) => { |
||||
if (!api.checkCreateIntegrationRequest(req)) { |
||||
if (!api.checkPostIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostIntegration request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PostIntegration request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateIntegrationRequest = req.body; |
||||
const reqObject: api.PostIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Post Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Create the new integration.
|
||||
var integration: any = { |
||||
name: reqObject.name, |
||||
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); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
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 |
||||
try { |
||||
let id = await createIntegration(userId, reqObject, knex); |
||||
const responseObject: api.PostIntegrationResponse = { |
||||
id: id |
||||
}; |
||||
throw e; |
||||
} |
||||
res.status(200).send(responseObject); |
||||
|
||||
const { id: userId } = req.user; |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const GetIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
try { |
||||
const integration = (await knex.select(['id', 'name', 'type', 'details']) |
||||
.from('integrations') |
||||
.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({}); |
||||
} |
||||
let integration = await getIntegration(req.user.id, req.params.id, knex); |
||||
res.status(200).send(integration); |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
handleErrorsInEndpoint(e) |
||||
} |
||||
} |
||||
|
||||
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; |
||||
console.log("List integrations"); |
||||
try { |
||||
const integrations: IntegrationDataWithId[] = await listIntegrations(req.user.id, knex); |
||||
console.log("Found integrations:", integrations); |
||||
await res.status(200).send(integrations); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e) |
||||
} |
||||
} |
||||
|
||||
export const DeleteIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("List integrations"); |
||||
console.log("User ", userId, ": Delete Integration ", req.params.id); |
||||
|
||||
try { |
||||
const integrations: api.ListIntegrationsResponse = ( |
||||
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), |
||||
} |
||||
}) |
||||
await deleteIntegration(userId, req.params.id, knex); |
||||
res.status(200).send(); |
||||
|
||||
console.log("Found integrations:", integrations); |
||||
await res.send(integrations); |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const DeleteIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkDeleteIntegrationRequest(req)) { |
||||
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkPutIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid DeleteIntegration request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PutIntegration request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteIntegrationRequest = req.body; |
||||
const reqObject: api.PutIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Delete Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the integration itself.
|
||||
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(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
console.log("User ", userId, ": Put Integration ", reqObject); |
||||
|
||||
try { |
||||
await modifyIntegration(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyIntegrationRequest(req)) { |
||||
export const PatchIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkPatchIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutIntegration request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PatchIntegration request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyIntegrationRequest = req.body; |
||||
const reqObject: api.PatchIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Integration ", reqObject); |
||||
console.log("User ", userId, ": Patch Integration ", reqObject); |
||||
|
||||
try { |
||||
await modifyIntegration(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
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: EndpointError = { |
||||
internalMessage: 'Integration does not exist for ModifyIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 404 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the integration.
|
||||
var update: any = {}; |
||||
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(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(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 { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import * as api from '../../client/src/api/api'; |
||||
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; |
||||
import Knex from 'knex'; |
||||
import { createTag, deleteTag, getTag, mergeTag, modifyTag } from '../db/Tag'; |
||||
|
||||
export const PostTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateTagRequest(req)) { |
||||
if (!api.checkPostTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostTag request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PostTag request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateTagRequest = req.body; |
||||
const reqObject: api.PostTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Post Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
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.
|
||||
const responseObject: api.CreateTagResponse = { |
||||
id: tagId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
try { |
||||
// Respond to the request.
|
||||
const responseObject: api.PostTagResponse = { |
||||
id: await createTag(userId, reqObject, knex) |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
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) => { |
||||
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; |
||||
console.log("User ", userId, ": Delete Tag ", req.params.id); |
||||
|
||||
console.log("User ", userId, ": Delete Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
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.
|
||||
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(); |
||||
try { |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
deleteTag(userId, req.params.id, knex); |
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
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; |
||||
|
||||
try { |
||||
const results = await knex.select(['id', 'name', 'parentId']) |
||||
.from('tags') |
||||
.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({}); |
||||
} |
||||
let tag = await getTag(req.params.id, userId, knex); |
||||
await res.status(200).send(tag); |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
handleErrorsInEndpoint(e) |
||||
} |
||||
} |
||||
|
||||
export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyTagRequest(req)) { |
||||
if (!api.checkPutTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutTag request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PutTag request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyTagRequest = req.body; |
||||
const reqObject: api.PutTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the parent tag.
|
||||
const parentTagPromise = reqObject.parentId ? |
||||
trx.select('id') |
||||
.from('tags') |
||||
.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.
|
||||
if ((reqObject.parentId && !parent) || |
||||
!tag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Tag or parent does not exist for ModifyTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the tag.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.update({ |
||||
name: reqObject.name, |
||||
parentId: reqObject.parentId || null, |
||||
}) |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
try { |
||||
await modifyTag(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkMergeTagRequest(req)) { |
||||
export const PatchTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkPatchTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), |
||||
name: "EndpointError", |
||||
message: 'Invalid PatchTag request', |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteTagRequest = req.body; |
||||
const reqObject: api.PatchTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Merge Tag ", reqObject); |
||||
const fromId = req.params.id; |
||||
const toId = req.params.toId; |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the "from" tag.
|
||||
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) |
||||
console.log("User ", userId, ": Patch Tag ", reqObject); |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); |
||||
try { |
||||
await modifyTag(userId, req.params.id, reqObject, knex); |
||||
res.status(200).send(); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
// 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; |
||||
} |
||||
export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
const { id: userId } = req.user; |
||||
|
||||
// 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]); |
||||
console.log("User ", userId, ": Merge Tag ", req.params.id, req.params.toId); |
||||
const fromId = req.params.id; |
||||
const toId = req.params.toId; |
||||
|
||||
// Delete the original tag.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': fromId }) |
||||
.del(); |
||||
try { |
||||
mergeTag(userId, fromId, toId, knex); |
||||
res.status(200).send(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
} catch (e) { |
||||
handleErrorsInEndpoint(e); |
||||
} |
||||
} |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(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