Add groundwork for third-party integrations. Spotify is the first. #34
Merged
sander
merged 8 commits from integrations
into master
5 years ago
49 changed files with 2686 additions and 1077 deletions
After Width: | Height: | Size: 907 B |
@ -0,0 +1,330 @@ |
|||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import { Box, CircularProgress, IconButton, Typography, MenuItem, TextField, Menu, Button, Card, CardHeader, CardContent, CardActions, Dialog, DialogTitle } from '@material-ui/core'; |
||||||
|
import { getIntegrations, createIntegration, modifyIntegration, deleteIntegration } from '../../../lib/backend/integrations'; |
||||||
|
import AddIcon from '@material-ui/icons/Add'; |
||||||
|
import EditIcon from '@material-ui/icons/Edit'; |
||||||
|
import CheckIcon from '@material-ui/icons/Check'; |
||||||
|
import DeleteIcon from '@material-ui/icons/Delete'; |
||||||
|
import ClearIcon from '@material-ui/icons/Clear'; |
||||||
|
import * as serverApi from '../../../api'; |
||||||
|
import { v4 as genUuid } from 'uuid'; |
||||||
|
import { useIntegrations, IntegrationClasses, IntegrationState, isIntegrationState, makeDefaultIntegrationProperties, makeIntegration } from '../../../lib/integration/useIntegrations'; |
||||||
|
import Alert from '@material-ui/lab/Alert'; |
||||||
|
import Integration from '../../../lib/integration/Integration'; |
||||||
|
let _ = require('lodash') |
||||||
|
|
||||||
|
// This widget is used to either display or edit a few
|
||||||
|
// specifically needed for Spotify Client credentials integration.
|
||||||
|
function EditSpotifyClientCredentialsDetails(props: { |
||||||
|
clientId: string, |
||||||
|
clientSecret: string | null, |
||||||
|
editing: boolean, |
||||||
|
onChangeClientId: (v: string) => void, |
||||||
|
onChangeClientSecret: (v: string) => void, |
||||||
|
}) { |
||||||
|
return <Box> |
||||||
|
<Box mt={1} mb={1}> |
||||||
|
<TextField |
||||||
|
variant="outlined" |
||||||
|
disabled={!props.editing} |
||||||
|
value={props.clientId || ""} |
||||||
|
label="Client id" |
||||||
|
fullWidth |
||||||
|
onChange={(e: any) => props.onChangeClientId(e.target.value)} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box mt={1} mb={1}> |
||||||
|
<TextField |
||||||
|
variant="outlined" |
||||||
|
disabled={!props.editing} |
||||||
|
value={props.clientSecret === null ? "••••••••••••••••" : props.clientSecret} |
||||||
|
label="Client secret" |
||||||
|
fullWidth |
||||||
|
onChange={(e: any) => { |
||||||
|
props.onChangeClientSecret(e.target.value) |
||||||
|
}} |
||||||
|
onFocus={(e: any) => { |
||||||
|
if (props.clientSecret === null) { |
||||||
|
// Change from dots to empty input
|
||||||
|
console.log("Focus!") |
||||||
|
props.onChangeClientSecret(''); |
||||||
|
} |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box>; |
||||||
|
} |
||||||
|
|
||||||
|
// An editing widget which is meant to either display or edit properties
|
||||||
|
// of an integration.
|
||||||
|
function EditIntegration(props: { |
||||||
|
upstreamId?: number, |
||||||
|
integration: serverApi.CreateIntegrationRequest, |
||||||
|
editing?: boolean, |
||||||
|
showSubmitButton?: boolean | "InProgress", |
||||||
|
showDeleteButton?: boolean | "InProgress", |
||||||
|
showEditButton?: boolean, |
||||||
|
showTestButton?: boolean | "InProgress", |
||||||
|
showCancelButton?: boolean, |
||||||
|
flashMessage?: React.ReactFragment, |
||||||
|
isNew: boolean, |
||||||
|
onChange?: (p: serverApi.CreateIntegrationRequest) => void, |
||||||
|
onSubmit?: (p: serverApi.CreateIntegrationRequest) => void, |
||||||
|
onDelete?: () => void, |
||||||
|
onEdit?: () => void, |
||||||
|
onTest?: () => void, |
||||||
|
onCancel?: () => void, |
||||||
|
}) { |
||||||
|
let IntegrationHeaders: Record<any, any> = { |
||||||
|
[serverApi.IntegrationType.SpotifyClientCredentials]: |
||||||
|
<Box display="flex" alignItems="center"> |
||||||
|
<Box mr={1}> |
||||||
|
{IntegrationClasses[serverApi.IntegrationType.SpotifyClientCredentials].getIcon({ |
||||||
|
style: { height: '40px', width: '40px' } |
||||||
|
})} |
||||||
|
</Box> |
||||||
|
<Typography>Spotify (using Client Credentials)</Typography> |
||||||
|
</Box> |
||||||
|
} |
||||||
|
let IntegrationDescription: Record<any, any> = { |
||||||
|
[serverApi.IntegrationType.SpotifyClientCredentials]: |
||||||
|
<Typography> |
||||||
|
This integration allows using the Spotify API to make requests that are |
||||||
|
tied to any specific user, such as searching items and retrieving item |
||||||
|
metadata.<br /> |
||||||
|
Please see the Spotify API documentation on how to generate a client ID |
||||||
|
and client secret. Once set, you will only be able to overwrite the secret |
||||||
|
here, not read it. |
||||||
|
</Typography> |
||||||
|
} |
||||||
|
|
||||||
|
return <Card variant="outlined"> |
||||||
|
<CardHeader |
||||||
|
avatar={ |
||||||
|
IntegrationHeaders[props.integration.type] |
||||||
|
} |
||||||
|
> |
||||||
|
</CardHeader> |
||||||
|
<CardContent> |
||||||
|
<Box mb={2}>{IntegrationDescription[props.integration.type]}</Box> |
||||||
|
<Box mt={1} mb={1}> |
||||||
|
<TextField |
||||||
|
variant="outlined" |
||||||
|
value={props.integration.name || ""} |
||||||
|
label="Integration name" |
||||||
|
fullWidth |
||||||
|
disabled={!props.editing} |
||||||
|
onChange={(e: any) => props.onChange && props.onChange({ |
||||||
|
...props.integration, |
||||||
|
name: e.target.value, |
||||||
|
})} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
{props.integration.type === serverApi.IntegrationType.SpotifyClientCredentials && |
||||||
|
<EditSpotifyClientCredentialsDetails |
||||||
|
clientId={props.integration.details.clientId} |
||||||
|
clientSecret={props.integration.secretDetails ?
|
||||||
|
props.integration.secretDetails.clientSecret : |
||||||
|
(props.isNew ? "" : null)} |
||||||
|
editing={props.editing || false} |
||||||
|
onChangeClientId={(v: string) => props.onChange && props.onChange({ |
||||||
|
...props.integration, |
||||||
|
details: { |
||||||
|
...props.integration.details, |
||||||
|
clientId: v, |
||||||
|
} |
||||||
|
})} |
||||||
|
onChangeClientSecret={(v: string) => props.onChange && props.onChange({ |
||||||
|
...props.integration, |
||||||
|
secretDetails: { |
||||||
|
...props.integration.secretDetails, |
||||||
|
clientSecret: v, |
||||||
|
} |
||||||
|
})} |
||||||
|
/> |
||||||
|
} |
||||||
|
{props.flashMessage && props.flashMessage} |
||||||
|
</CardContent> |
||||||
|
<CardActions> |
||||||
|
{props.showEditButton && <IconButton |
||||||
|
onClick={props.onEdit} |
||||||
|
><EditIcon /></IconButton>} |
||||||
|
{props.showSubmitButton && <IconButton |
||||||
|
onClick={() => props.onSubmit && props.onSubmit(props.integration)} |
||||||
|
><CheckIcon /></IconButton>} |
||||||
|
{props.showDeleteButton && <IconButton |
||||||
|
onClick={props.onDelete} |
||||||
|
><DeleteIcon /></IconButton>} |
||||||
|
{props.showCancelButton && <IconButton |
||||||
|
onClick={props.onCancel} |
||||||
|
><ClearIcon /></IconButton>} |
||||||
|
{props.showTestButton && <Button |
||||||
|
onClick={props.onTest} |
||||||
|
>Test</Button>} |
||||||
|
</CardActions> |
||||||
|
</Card> |
||||||
|
} |
||||||
|
|
||||||
|
let EditorWithTest = (props: any) => { |
||||||
|
const [testFlashMessage, setTestFlashMessage] = |
||||||
|
React.useState<React.ReactFragment | undefined>(undefined); |
||||||
|
let { integration, ...rest } = props; |
||||||
|
return <EditIntegration |
||||||
|
onTest={() => { |
||||||
|
integration.integration.test({}) |
||||||
|
.then(() => { |
||||||
|
setTestFlashMessage( |
||||||
|
<Alert severity="success">Integration is active.</Alert> |
||||||
|
) |
||||||
|
}) |
||||||
|
}} |
||||||
|
flashMessage={testFlashMessage} |
||||||
|
showTestButton={true} |
||||||
|
integration={integration.properties} |
||||||
|
{...rest} |
||||||
|
/>; |
||||||
|
} |
||||||
|
|
||||||
|
function AddIntegrationMenu(props: { |
||||||
|
position: null | number[], |
||||||
|
open: boolean, |
||||||
|
onClose?: () => void, |
||||||
|
onAdd?: (type: serverApi.IntegrationType) => void, |
||||||
|
}) { |
||||||
|
const pos = props.open && props.position ? |
||||||
|
{ left: props.position[0], top: props.position[1] } |
||||||
|
: { left: 0, top: 0 } |
||||||
|
|
||||||
|
return <Menu |
||||||
|
open={props.open} |
||||||
|
anchorReference="anchorPosition" |
||||||
|
anchorPosition={pos} |
||||||
|
keepMounted |
||||||
|
onClose={props.onClose} |
||||||
|
> |
||||||
|
<MenuItem |
||||||
|
onClick={() => { |
||||||
|
props.onAdd && props.onAdd(serverApi.IntegrationType.SpotifyClientCredentials); |
||||||
|
props.onClose && props.onClose(); |
||||||
|
}} |
||||||
|
>Spotify</MenuItem> |
||||||
|
</Menu> |
||||||
|
} |
||||||
|
|
||||||
|
function EditIntegrationDialog(props: { |
||||||
|
open: boolean, |
||||||
|
onClose?: () => void, |
||||||
|
upstreamId?: number, |
||||||
|
integration: IntegrationState, |
||||||
|
onSubmit?: (p: serverApi.CreateIntegrationRequest) => void, |
||||||
|
isNew: boolean, |
||||||
|
}) { |
||||||
|
let [editingIntegration, setEditingIntegration] = |
||||||
|
useState<IntegrationState>(props.integration); |
||||||
|
|
||||||
|
useEffect(() => { setEditingIntegration(props.integration); }, [props.integration]); |
||||||
|
|
||||||
|
return <Dialog |
||||||
|
onClose={props.onClose} |
||||||
|
open={props.open} |
||||||
|
disableBackdropClick={true} |
||||||
|
> |
||||||
|
<DialogTitle>Edit Integration</DialogTitle> |
||||||
|
<EditIntegration |
||||||
|
isNew={props.isNew} |
||||||
|
editing={true} |
||||||
|
upstreamId={props.upstreamId} |
||||||
|
integration={editingIntegration.properties} |
||||||
|
showCancelButton={true} |
||||||
|
showSubmitButton={props.onSubmit !== undefined} |
||||||
|
showTestButton={false} |
||||||
|
onCancel={props.onClose} |
||||||
|
onSubmit={props.onSubmit} |
||||||
|
onChange={(i: any) => { |
||||||
|
setEditingIntegration({ |
||||||
|
...editingIntegration, |
||||||
|
properties: i, |
||||||
|
integration: makeIntegration(i, editingIntegration.id), |
||||||
|
}); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Dialog> |
||||||
|
} |
||||||
|
|
||||||
|
export default function IntegrationSettings(props: {}) { |
||||||
|
const [addMenuPos, setAddMenuPos] = React.useState<null | number[]>(null); |
||||||
|
const [editingState, setEditingState] = React.useState<IntegrationState | null>(null); |
||||||
|
|
||||||
|
let { |
||||||
|
state: integrations, |
||||||
|
addIntegration, |
||||||
|
modifyIntegration, |
||||||
|
deleteIntegration, |
||||||
|
updateFromUpstream, |
||||||
|
} = useIntegrations(); |
||||||
|
|
||||||
|
const onOpenAddMenu = (e: any) => { |
||||||
|
setAddMenuPos([e.clientX, e.clientY]) |
||||||
|
}; |
||||||
|
const onCloseAddMenu = () => { |
||||||
|
setAddMenuPos(null); |
||||||
|
}; |
||||||
|
|
||||||
|
return <> |
||||||
|
<Box> |
||||||
|
{integrations === null && <CircularProgress />} |
||||||
|
{Array.isArray(integrations) && <Box display="flex" flexDirection="column" alignItems="center" flexWrap="wrap"> |
||||||
|
{integrations.map((state: IntegrationState) => <Box m={1} width="90%"> |
||||||
|
<EditorWithTest |
||||||
|
upstreamId={state.id} |
||||||
|
integration={state} |
||||||
|
showEditButton={true} |
||||||
|
showDeleteButton={true} |
||||||
|
onEdit={() => { setEditingState(state); }} |
||||||
|
onDelete={() => { |
||||||
|
deleteIntegration(state.id) |
||||||
|
.then(updateFromUpstream) |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box>)} |
||||||
|
<IconButton onClick={onOpenAddMenu}> |
||||||
|
<AddIcon /> |
||||||
|
</IconButton> |
||||||
|
</Box>} |
||||||
|
</Box> |
||||||
|
<AddIntegrationMenu |
||||||
|
position={addMenuPos} |
||||||
|
open={addMenuPos !== null} |
||||||
|
onClose={onCloseAddMenu} |
||||||
|
onAdd={(type: serverApi.IntegrationType) => { |
||||||
|
let p = makeDefaultIntegrationProperties(type); |
||||||
|
setEditingState({ |
||||||
|
properties: p, |
||||||
|
integration: makeIntegration(p, -1), |
||||||
|
id: -1, |
||||||
|
}) |
||||||
|
}} |
||||||
|
/> |
||||||
|
{editingState && <EditIntegrationDialog |
||||||
|
open={!(editingState === null)} |
||||||
|
onClose={() => { setEditingState(null); }} |
||||||
|
integration={editingState} |
||||||
|
isNew={editingState.id === -1} |
||||||
|
onSubmit={(v: serverApi.CreateIntegrationRequest) => { |
||||||
|
if (editingState.id >= 0) { |
||||||
|
const id = editingState.id; |
||||||
|
setEditingState(null); |
||||||
|
modifyIntegration(id, v) |
||||||
|
.then(updateFromUpstream) |
||||||
|
} else { |
||||||
|
setEditingState(null); |
||||||
|
createIntegration({ |
||||||
|
...v, |
||||||
|
secretDetails: v.secretDetails || {}, |
||||||
|
}) |
||||||
|
.then(updateFromUpstream) |
||||||
|
} |
||||||
|
}} |
||||||
|
/>} |
||||||
|
</>; |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
import React, { useReducer } from 'react'; |
||||||
|
import { WindowState } from "../Windows"; |
||||||
|
import { Box, Paper, Typography, TextField, Button } from "@material-ui/core"; |
||||||
|
import { useHistory } from 'react-router'; |
||||||
|
import { useAuth, Auth } from '../../../lib/useAuth'; |
||||||
|
import Alert from '@material-ui/lab/Alert'; |
||||||
|
import { Link } from 'react-router-dom'; |
||||||
|
import IntegrationSettingsEditor from './IntegrationSettings'; |
||||||
|
|
||||||
|
export enum SettingsTab { |
||||||
|
Integrations = 0, |
||||||
|
} |
||||||
|
|
||||||
|
export interface SettingsWindowState extends WindowState { |
||||||
|
activeTab: SettingsTab, |
||||||
|
} |
||||||
|
export enum SettingsWindowStateActions { |
||||||
|
SetActiveTab = "SetActiveTab", |
||||||
|
} |
||||||
|
export function SettingsWindowReducer(state: SettingsWindowState, action: any) { |
||||||
|
switch (action.type) { |
||||||
|
case SettingsWindowStateActions.SetActiveTab: |
||||||
|
return { ...state, activeTab: action.value } |
||||||
|
default: |
||||||
|
throw new Error("Unimplemented SettingsWindow state update.") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default function SettingsWindow(props: {}) { |
||||||
|
const [state, dispatch] = useReducer(SettingsWindowReducer, { |
||||||
|
activeTab: SettingsTab.Integrations, |
||||||
|
}); |
||||||
|
|
||||||
|
return <SettingsWindowControlled state={state} dispatch={dispatch} /> |
||||||
|
} |
||||||
|
|
||||||
|
export function SettingsWindowControlled(props: { |
||||||
|
state: SettingsWindowState, |
||||||
|
dispatch: (action: any) => void, |
||||||
|
}) { |
||||||
|
let history: any = useHistory(); |
||||||
|
let auth: Auth = useAuth(); |
||||||
|
|
||||||
|
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
||||||
|
<Box |
||||||
|
m={1} |
||||||
|
mt={4} |
||||||
|
width="60%" |
||||||
|
> |
||||||
|
<Paper> |
||||||
|
<Box p={3}> |
||||||
|
<Box mb={3}><Typography variant="h5">User Settings</Typography></Box> |
||||||
|
<Typography variant="h6">Integrations</Typography> |
||||||
|
<IntegrationSettingsEditor/> |
||||||
|
</Box> |
||||||
|
</Paper> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
import * as serverApi from '../../api'; |
||||||
|
import { useAuth } from '../useAuth'; |
||||||
|
import backendRequest from './request'; |
||||||
|
|
||||||
|
export async function createIntegration(details: serverApi.CreateIntegrationRequest) { |
||||||
|
const requestOpts = { |
||||||
|
method: 'POST', |
||||||
|
headers: { 'Content-Type': 'application/json' }, |
||||||
|
body: JSON.stringify(details), |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.CreateIntegrationEndpoint, requestOpts) |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Response to integration creation not OK: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
|
||||||
|
return await response.json(); |
||||||
|
} |
||||||
|
|
||||||
|
export async function modifyIntegration(id: number, details: serverApi.ModifyIntegrationRequest) { |
||||||
|
const requestOpts = { |
||||||
|
method: 'PUT', |
||||||
|
headers: { 'Content-Type': 'application/json' }, |
||||||
|
body: JSON.stringify(details), |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await backendRequest( |
||||||
|
(process.env.REACT_APP_BACKEND || "") + serverApi.ModifyIntegrationEndpoint.replace(':id', id.toString()), |
||||||
|
requestOpts |
||||||
|
); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Response to integration modification not OK: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function deleteIntegration(id: number) { |
||||||
|
const requestOpts = { |
||||||
|
method: 'DELETE', |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await backendRequest( |
||||||
|
(process.env.REACT_APP_BACKEND || "") + serverApi.DeleteIntegrationEndpoint.replace(':id', id.toString()), |
||||||
|
requestOpts |
||||||
|
); |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Response to integration deletion not OK: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function getIntegrations() { |
||||||
|
const requestOpts = { |
||||||
|
method: 'GET', |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await backendRequest( |
||||||
|
(process.env.REACT_APP_BACKEND || "") + serverApi.ListIntegrationsEndpoint, |
||||||
|
requestOpts |
||||||
|
); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Response to integration list not OK: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
|
||||||
|
let json = await response.json(); |
||||||
|
return json; |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
import { ResponsiveFontSizesOptions } from "@material-ui/core/styles/responsiveFontSizes"; |
||||||
|
import { useHistory } from "react-router"; |
||||||
|
import { Auth } from "../useAuth"; |
||||||
|
|
||||||
|
export class NotLoggedInError extends Error { |
||||||
|
constructor(message: string) { |
||||||
|
super(message); |
||||||
|
this.name = "NotLoggedInError"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default async function backendRequest(url: any, ...restArgs: any[]): Promise<Response> { |
||||||
|
let response = await fetch(url, ...restArgs); |
||||||
|
if (response.status === 401 && (await response.json()).reason === "NotLoggedIn") { |
||||||
|
console.log("Not logged in!") |
||||||
|
throw new NotLoggedInError("Not logged in."); |
||||||
|
} |
||||||
|
return response; |
||||||
|
} |
||||||
|
|
||||||
|
export function handleNotLoggedIn(auth: Auth, e: NotLoggedInError) { |
||||||
|
console.log("Not logged in!") |
||||||
|
auth.signout(); |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
import React, { ReactFragment } from 'react'; |
||||||
|
|
||||||
|
export interface IntegrationAlbum { |
||||||
|
name?: string, |
||||||
|
artist?: IntegrationArtist, |
||||||
|
url?: string, // An URL to access the item externally.
|
||||||
|
} |
||||||
|
|
||||||
|
export interface IntegrationArtist { |
||||||
|
name?: string, |
||||||
|
url?: string, // An URL to access the item externally.
|
||||||
|
} |
||||||
|
|
||||||
|
export interface IntegrationSong { |
||||||
|
title?: string, |
||||||
|
album?: IntegrationAlbum, |
||||||
|
artist?: IntegrationArtist, |
||||||
|
url?: string, // An URL to access the item externally.
|
||||||
|
} |
||||||
|
|
||||||
|
export enum IntegrationFeature { |
||||||
|
// Used to test whether the integration is active.
|
||||||
|
Test = 0, |
||||||
|
|
||||||
|
// Used to get a bucket of songs (typically: the whole library)
|
||||||
|
GetSongs, |
||||||
|
|
||||||
|
// Used to search items and get some amount of candidate results.
|
||||||
|
SearchSong, |
||||||
|
SearchAlbum, |
||||||
|
SearchArtist, |
||||||
|
} |
||||||
|
|
||||||
|
export interface IntegrationDescriptor { |
||||||
|
supports: IntegrationFeature[], |
||||||
|
} |
||||||
|
|
||||||
|
export default class Integration { |
||||||
|
constructor(integrationId: number) { } |
||||||
|
|
||||||
|
// Common
|
||||||
|
static getFeatures(): IntegrationFeature[] { return []; } |
||||||
|
static getIcon(props: any): ReactFragment { return <></> } |
||||||
|
|
||||||
|
// Requires feature: Test
|
||||||
|
async test(testParams: any): Promise<void> {} |
||||||
|
|
||||||
|
// Requires feature: GetSongs
|
||||||
|
async getSongs(getSongsParams: any): Promise<IntegrationSong[]> { return []; } |
||||||
|
|
||||||
|
// Requires feature: SearchSongs
|
||||||
|
async searchSong(songProps: IntegrationSong): Promise<IntegrationSong[]> { return []; } |
||||||
|
|
||||||
|
// Requires feature: SearchAlbum
|
||||||
|
async searchAlbum(albumProps: IntegrationAlbum): Promise<IntegrationAlbum[]> { return []; } |
||||||
|
|
||||||
|
// Requires feature: SearchArtist
|
||||||
|
async searchArtist(artistProps: IntegrationArtist): Promise<IntegrationArtist[]> { return []; } |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import Integration, { IntegrationFeature, IntegrationAlbum, IntegrationArtist, IntegrationSong } from '../Integration'; |
||||||
|
import StoreLinkIcon, { ExternalStore } from '../../../components/common/StoreLinkIcon'; |
||||||
|
|
||||||
|
enum SearchType { |
||||||
|
Song = 'song', |
||||||
|
Artist = 'artist', |
||||||
|
Album = 'album', |
||||||
|
}; |
||||||
|
|
||||||
|
export default class SpotifyClientCreds extends Integration { |
||||||
|
integrationId: number; |
||||||
|
|
||||||
|
constructor(integrationId: number) { |
||||||
|
super(integrationId); |
||||||
|
this.integrationId = integrationId; |
||||||
|
} |
||||||
|
|
||||||
|
static getFeatures(): IntegrationFeature[] { |
||||||
|
return [ |
||||||
|
IntegrationFeature.Test, |
||||||
|
IntegrationFeature.SearchSong, |
||||||
|
IntegrationFeature.SearchAlbum, |
||||||
|
IntegrationFeature.SearchArtist, |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
static getIcon(props: any) { |
||||||
|
return <StoreLinkIcon whichStore={ExternalStore.Spotify} {...props} /> |
||||||
|
} |
||||||
|
|
||||||
|
async test(testParams: {}) { |
||||||
|
const response = await fetch( |
||||||
|
(process.env.REACT_APP_BACKEND || "") + |
||||||
|
`/integrations/${this.integrationId}/v1/search?q=queens&type=artist`); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Spttify Client Credentails test failed: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async searchSong(songProps: IntegrationSong): Promise<IntegrationSong[]> { return []; } |
||||||
|
async searchAlbum(albumProps: IntegrationAlbum): Promise<IntegrationAlbum[]> { return []; } |
||||||
|
async searchArtist(artistProps: IntegrationArtist): Promise<IntegrationArtist[]> { return []; } |
||||||
|
|
||||||
|
async search(query: string, type: SearchType): |
||||||
|
Promise<IntegrationSong[] | IntegrationAlbum[] | IntegrationArtist[]> { |
||||||
|
const response = await fetch( |
||||||
|
(process.env.REACT_APP_BACKEND || "") + |
||||||
|
`/integrations/${this.integrationId}/v1/search?q=${encodeURIComponent(query)}&type=${type}`); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error("Spotify Client Credentails search failed: " + JSON.stringify(response)); |
||||||
|
} |
||||||
|
|
||||||
|
switch(type) { |
||||||
|
case SearchType.Song: { |
||||||
|
return (await response.json()).tracks.items.map((r: any): IntegrationSong => { |
||||||
|
return { |
||||||
|
title: r.name, |
||||||
|
url: r.external_urls.spotify, |
||||||
|
artist: { |
||||||
|
name: r.artists[0].name, |
||||||
|
url: r.artists[0].external_urls.spotify, |
||||||
|
}, |
||||||
|
album: { |
||||||
|
name: r.albums[0].name, |
||||||
|
url: r.albums[0].external_urls.spotify, |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
case SearchType.Artist: { |
||||||
|
return (await response.json()).artists.items.map((r: any): IntegrationArtist => { |
||||||
|
return { |
||||||
|
name: r.name, |
||||||
|
url: r.external_urls.spotify, |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
case SearchType.Album: { |
||||||
|
return (await response.json()).albums.items.map((r: any): IntegrationAlbum => { |
||||||
|
return { |
||||||
|
name: r.name, |
||||||
|
url: r.external_urls.spotify, |
||||||
|
artist: { |
||||||
|
name: r.artists[0].name, |
||||||
|
url: r.artists[0].external_urls.spotify, |
||||||
|
}, |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,161 @@ |
|||||||
|
import React, { useState, useContext, createContext, useReducer, useEffect } from "react"; |
||||||
|
import Integration from "./Integration"; |
||||||
|
import * as serverApi from '../../api'; |
||||||
|
import SpotifyClientCreds from "./spotify/SpotifyClientCreds"; |
||||||
|
import * as backend from "../backend/integrations"; |
||||||
|
import { handleNotLoggedIn, NotLoggedInError } from "../backend/request"; |
||||||
|
import { useAuth } from "../useAuth"; |
||||||
|
|
||||||
|
export type IntegrationState = { |
||||||
|
id: number, |
||||||
|
integration: Integration, |
||||||
|
properties: serverApi.CreateIntegrationRequest, |
||||||
|
}; |
||||||
|
export type IntegrationsState = IntegrationState[] | "Loading"; |
||||||
|
|
||||||
|
export function isIntegrationState(v: any) : v is IntegrationState { |
||||||
|
return 'id' in v && 'integration' in v && 'properties' in v; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Integrations { |
||||||
|
state: IntegrationsState, |
||||||
|
addIntegration: (v: serverApi.CreateIntegrationRequest) => Promise<number>, |
||||||
|
deleteIntegration: (id: number) => Promise<void>, |
||||||
|
modifyIntegration: (id: number, v: serverApi.CreateIntegrationRequest) => Promise<void>, |
||||||
|
updateFromUpstream: () => Promise<void>, |
||||||
|
}; |
||||||
|
|
||||||
|
export const IntegrationClasses: Record<any, any> = { |
||||||
|
[serverApi.IntegrationType.SpotifyClientCredentials]: SpotifyClientCreds, |
||||||
|
} |
||||||
|
|
||||||
|
export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType):
|
||||||
|
serverApi.CreateIntegrationRequest { |
||||||
|
switch(type) { |
||||||
|
case serverApi.IntegrationType.SpotifyClientCredentials: { |
||||||
|
return { |
||||||
|
name: "Spotify", |
||||||
|
type: type, |
||||||
|
details: { clientId: "" }, |
||||||
|
secretDetails: { clientSecret: "" }, |
||||||
|
} |
||||||
|
} |
||||||
|
default: { |
||||||
|
throw new Error("Unimplemented default integration.") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function makeIntegration(p: serverApi.CreateIntegrationRequest, id: number) { |
||||||
|
switch(p.type) { |
||||||
|
case serverApi.IntegrationType.SpotifyClientCredentials: { |
||||||
|
return new SpotifyClientCreds(id); |
||||||
|
} |
||||||
|
default: { |
||||||
|
throw new Error("Unimplemented integration type.") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const integrationsContext = createContext<Integrations>({ |
||||||
|
state: [], |
||||||
|
addIntegration: async () => 0, |
||||||
|
modifyIntegration: async () => { }, |
||||||
|
deleteIntegration: async () => { }, |
||||||
|
updateFromUpstream: async () => { }, |
||||||
|
}); |
||||||
|
|
||||||
|
export function ProvideIntegrations(props: { children: any }) { |
||||||
|
const integrations = useProvideIntegrations(); |
||||||
|
return <integrationsContext.Provider value={integrations}>{props.children}</integrationsContext.Provider>; |
||||||
|
} |
||||||
|
|
||||||
|
export const useIntegrations = () => { |
||||||
|
return useContext(integrationsContext); |
||||||
|
}; |
||||||
|
|
||||||
|
function useProvideIntegrations(): Integrations { |
||||||
|
let auth = useAuth(); |
||||||
|
enum IntegrationsActions { |
||||||
|
SetItem = "SetItem", |
||||||
|
Set = "Set", |
||||||
|
DeleteItem = "DeleteItem", |
||||||
|
AddItem = "AddItem", |
||||||
|
} |
||||||
|
let IntegrationsReducer = (state: IntegrationsState, action: any): IntegrationsState => { |
||||||
|
switch (action.type) { |
||||||
|
case IntegrationsActions.SetItem: { |
||||||
|
if (state !== "Loading") { |
||||||
|
return state.map((item: any) => { |
||||||
|
return (item.id === action.id) ? action.value : item; |
||||||
|
}) |
||||||
|
} |
||||||
|
return state; |
||||||
|
} |
||||||
|
case IntegrationsActions.Set: { |
||||||
|
return action.value; |
||||||
|
} |
||||||
|
case IntegrationsActions.DeleteItem: { |
||||||
|
if (state !== "Loading") { |
||||||
|
const newState = [...state]; |
||||||
|
return newState.filter((item: any) => item.id !== action.id); |
||||||
|
} |
||||||
|
return state; |
||||||
|
} |
||||||
|
case IntegrationsActions.AddItem: { |
||||||
|
return [...state, action.value]; |
||||||
|
} |
||||||
|
default: |
||||||
|
throw new Error("Unimplemented Integrations state update.") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(IntegrationsReducer, []) |
||||||
|
|
||||||
|
let updateFromUpstream = async () => { |
||||||
|
backend.getIntegrations() |
||||||
|
.then((integrations: serverApi.ListIntegrationsResponse) => { |
||||||
|
dispatch({ |
||||||
|
type: IntegrationsActions.Set, |
||||||
|
value: integrations.map((i: any) => { |
||||||
|
return { |
||||||
|
integration: new (IntegrationClasses[i.type])(i.id), |
||||||
|
properties: { ...i }, |
||||||
|
id: i.id, |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
|
}) |
||||||
|
.catch((e: NotLoggedInError) => handleNotLoggedIn(auth, e)); |
||||||
|
} |
||||||
|
|
||||||
|
let addIntegration = async (v: serverApi.CreateIntegrationRequest) => { |
||||||
|
const id = await backend.createIntegration(v); |
||||||
|
await updateFromUpstream(); |
||||||
|
return id; |
||||||
|
} |
||||||
|
|
||||||
|
let deleteIntegration = async (id: number) => { |
||||||
|
await backend.deleteIntegration(id); |
||||||
|
await updateFromUpstream(); |
||||||
|
} |
||||||
|
|
||||||
|
let modifyIntegration = async (id: number, v: serverApi.CreateIntegrationRequest) => { |
||||||
|
await backend.modifyIntegration(id, v); |
||||||
|
await updateFromUpstream(); |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (auth.user) { |
||||||
|
updateFromUpstream() |
||||||
|
} |
||||||
|
}, [auth]); |
||||||
|
|
||||||
|
return { |
||||||
|
state: state, |
||||||
|
addIntegration: addIntegration, |
||||||
|
modifyIntegration: modifyIntegration, |
||||||
|
deleteIntegration: deleteIntegration, |
||||||
|
updateFromUpstream: updateFromUpstream, |
||||||
|
} |
||||||
|
} |
@ -1,64 +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 AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkAlbumDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid AlbumDetails 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 tagIdsPromise = knex.select('tagId') |
|
||||||
.from('albums_tags') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((tags: any) => { |
|
||||||
return tags.map((tag: any) => tag['tagId']) |
|
||||||
}); |
|
||||||
const songIdsPromise = knex.select('songId') |
|
||||||
.from('songs_albums') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((songs: any) => { |
|
||||||
return songs.map((song: any) => song['songId']) |
|
||||||
}); |
|
||||||
const artistIdsPromise = knex.select('artistId') |
|
||||||
.from('artists_albums') |
|
||||||
.where({ 'albumId': req.params.id }) |
|
||||||
.then((artists: any) => { |
|
||||||
return artists.map((artist: any) => artist['artistId']) |
|
||||||
}); |
|
||||||
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, tagIdsPromise, songIdsPromise, artistIdsPromise]); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
if (album) { |
|
||||||
const response: api.AlbumDetailsResponse = { |
|
||||||
name: album['name'], |
|
||||||
artistIds: artists, |
|
||||||
tagIds: tags, |
|
||||||
songIds: songs, |
|
||||||
storeLinks: asJson(album['storeLinks']), |
|
||||||
}; |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
} |
|
||||||
} |
|
@ -1,41 +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 ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkArtistDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid ArtistDetails request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
try { |
|
||||||
const tagIds = Array.from(new Set((await knex.select('tagId') |
|
||||||
.from('artists_tags') |
|
||||||
.where({ 'artistId': req.params.id }) |
|
||||||
).map((tag: any) => tag['tagId']))); |
|
||||||
|
|
||||||
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, |
|
||||||
tagIds: tagIds, |
|
||||||
storeLinks: asJson(results[0].storeLinks), |
|
||||||
} |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e) |
|
||||||
} |
|
||||||
} |
|
@ -1,96 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkCreateAlbumRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid CreateAlbum request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.CreateAlbumRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Create 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(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
@ -1,70 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkCreateArtistRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid CreateArtist request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.CreateArtistRequest = 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(); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
@ -1,118 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkCreateSongRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid CreateSong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.CreateSongRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Create 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(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
@ -1,62 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkCreateTagRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid CreateTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.CreateTagRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Create 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); |
|
||||||
|
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
trx.rollback(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
@ -1,78 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
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 DeleteTagEndpointHandler: 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 ", 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(); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
|
||||||
|
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
trx.rollback(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
@ -0,0 +1,206 @@ |
|||||||
|
import * as api from '../../client/src/api'; |
||||||
|
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||||
|
import Knex from 'knex'; |
||||||
|
import asJson from '../lib/asJson'; |
||||||
|
|
||||||
|
export const PostIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkCreateIntegrationRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Invalid PostIntegration request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.CreateIntegrationRequest = 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 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
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({}); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
catchUnhandledErrors(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: 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), |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
console.log("Found integrations:", integrations); |
||||||
|
await res.send(integrations); |
||||||
|
} catch (e) { |
||||||
|
catchUnhandledErrors(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const DeleteIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkDeleteIntegrationRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Invalid DeleteIntegration request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.DeleteIntegrationRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
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(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkModifyIntegrationRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Invalid PutIntegration request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.ModifyIntegrationRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
console.log("User ", userId, ": Put Integration ", reqObject); |
||||||
|
|
||||||
|
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(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
@ -1,78 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkMergeTagRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.DeleteTagRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
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) |
|
||||||
|
|
||||||
// Wait for the requests to finish.
|
|
||||||
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); |
|
||||||
|
|
||||||
// Check that we found all objects we need.
|
|
||||||
if (!fromTag || !toTag) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
// Assign new tag ID to any objects referencing the to-be-merged tag.
|
|
||||||
const cPromise = trx('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'parentId': fromId }) |
|
||||||
.update({ 'parentId': toId }); |
|
||||||
const sPromise = trx('songs_tags') |
|
||||||
.where({ 'tagId': fromId }) |
|
||||||
.update({ 'tagId': toId }); |
|
||||||
const arPromise = trx('artists_tags') |
|
||||||
.where({ 'tagId': fromId }) |
|
||||||
.update({ 'tagId': toId }); |
|
||||||
const alPromise = trx('albums_tags') |
|
||||||
.where({ 'tagId': fromId }) |
|
||||||
.update({ 'tagId': toId }); |
|
||||||
await Promise.all([sPromise, arPromise, alPromise, cPromise]); |
|
||||||
|
|
||||||
// Delete the original tag.
|
|
||||||
await trx('tags') |
|
||||||
.where({ 'user': userId }) |
|
||||||
.where({ 'id': fromId }) |
|
||||||
.del(); |
|
||||||
|
|
||||||
// Respond to the request.
|
|
||||||
res.status(200).send(); |
|
||||||
|
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e); |
|
||||||
trx.rollback(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
@ -1,191 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkModifySongRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid ModifySong request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.ModifySongRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Modify 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,66 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkModifyTagRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid ModifyTag request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
const reqObject: api.ModifyTagRequest = req.body; |
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
console.log("User ", userId, ": Modify 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(); |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
@ -0,0 +1,372 @@ |
|||||||
|
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 tagIdsPromise: Promise<number[]> = 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']) |
||||||
|
)); |
||||||
|
}) |
||||||
|
|
||||||
|
const albumIdsPromise: Promise<number[]> = 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']) |
||||||
|
)); |
||||||
|
}) |
||||||
|
|
||||||
|
const artistIdsPromise: Promise<number[]> = 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']) |
||||||
|
)); |
||||||
|
}) |
||||||
|
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([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]); |
||||||
|
|
||||||
|
if (song) { |
||||||
|
const response: api.SongDetailsResponse = { |
||||||
|
title: song.title, |
||||||
|
tagIds: tags, |
||||||
|
artistIds: artists, |
||||||
|
albumIds: 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,68 +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 SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkSongDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid SongDetails request: ' + JSON.stringify(req.body), |
|
||||||
httpStatus: 400 |
|
||||||
}; |
|
||||||
throw e; |
|
||||||
} |
|
||||||
|
|
||||||
const { id: userId } = req.user; |
|
||||||
|
|
||||||
try { |
|
||||||
const tagIdsPromise: Promise<number[]> = 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']) |
|
||||||
)); |
|
||||||
}) |
|
||||||
|
|
||||||
const albumIdsPromise: Promise<number[]> = 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']) |
|
||||||
)); |
|
||||||
}) |
|
||||||
|
|
||||||
const artistIdsPromise: Promise<number[]> = 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']) |
|
||||||
)); |
|
||||||
}) |
|
||||||
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([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]); |
|
||||||
|
|
||||||
if (song) { |
|
||||||
const response: api.SongDetailsResponse = { |
|
||||||
title: song.title, |
|
||||||
tagIds: tags, |
|
||||||
artistIds: artists, |
|
||||||
albumIds: albums, |
|
||||||
storeLinks: asJson(song.storeLinks), |
|
||||||
} |
|
||||||
await res.send(response); |
|
||||||
} else { |
|
||||||
await res.status(404).send({}); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,306 @@ |
|||||||
|
import * as api from '../../client/src/api'; |
||||||
|
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||||
|
import Knex from 'knex'; |
||||||
|
|
||||||
|
export const PostTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkCreateTagRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Invalid PostTag request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.CreateTagRequest = 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); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
catchUnhandledErrors(e); |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async function getChildrenRecursive(id: number, userId: number, trx: any) { |
||||||
|
const directChildren = (await trx.select('id') |
||||||
|
.from('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'parentId': id })).map((r: any) => r.id); |
||||||
|
|
||||||
|
const indirectChildrenPromises = directChildren.map( |
||||||
|
(child: number) => getChildrenRecursive(child, userId, trx) |
||||||
|
); |
||||||
|
const indirectChildrenNested = await Promise.all(indirectChildrenPromises); |
||||||
|
const indirectChildren = indirectChildrenNested.flat(); |
||||||
|
|
||||||
|
return [ |
||||||
|
...directChildren, |
||||||
|
...indirectChildren, |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
export const DeleteTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
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 ", 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(); |
||||||
|
|
||||||
|
// Respond to the request.
|
||||||
|
res.status(200).send(); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
catchUnhandledErrors(e); |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
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({}); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
catchUnhandledErrors(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkModifyTagRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Invalid PutTag request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.ModifyTagRequest = 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(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||||
|
if (!api.checkMergeTagRequest(req)) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
const reqObject: api.DeleteTagRequest = req.body; |
||||||
|
const { id: userId } = req.user; |
||||||
|
|
||||||
|
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) |
||||||
|
|
||||||
|
// Wait for the requests to finish.
|
||||||
|
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); |
||||||
|
|
||||||
|
// Check that we found all objects we need.
|
||||||
|
if (!fromTag || !toTag) { |
||||||
|
const e: EndpointError = { |
||||||
|
internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body), |
||||||
|
httpStatus: 400 |
||||||
|
}; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
|
||||||
|
// Assign new tag ID to any objects referencing the to-be-merged tag.
|
||||||
|
const cPromise = trx('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'parentId': fromId }) |
||||||
|
.update({ 'parentId': toId }); |
||||||
|
const sPromise = trx('songs_tags') |
||||||
|
.where({ 'tagId': fromId }) |
||||||
|
.update({ 'tagId': toId }); |
||||||
|
const arPromise = trx('artists_tags') |
||||||
|
.where({ 'tagId': fromId }) |
||||||
|
.update({ 'tagId': toId }); |
||||||
|
const alPromise = trx('albums_tags') |
||||||
|
.where({ 'tagId': fromId }) |
||||||
|
.update({ 'tagId': toId }); |
||||||
|
await Promise.all([sPromise, arPromise, alPromise, cPromise]); |
||||||
|
|
||||||
|
// Delete the original tag.
|
||||||
|
await trx('tags') |
||||||
|
.where({ 'user': userId }) |
||||||
|
.where({ 'id': fromId }) |
||||||
|
.del(); |
||||||
|
|
||||||
|
// Respond to the request.
|
||||||
|
res.status(200).send(); |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
catchUnhandledErrors(e); |
||||||
|
trx.rollback(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
@ -1,34 +0,0 @@ |
|||||||
import * as api from '../../client/src/api'; |
|
||||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
|
||||||
import Knex from 'knex'; |
|
||||||
|
|
||||||
export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
|
||||||
if (!api.checkTagDetailsRequest(req)) { |
|
||||||
const e: EndpointError = { |
|
||||||
internalMessage: 'Invalid TagDetails 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({}); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
catchUnhandledErrors(e) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,111 @@ |
|||||||
|
import Knex from "knex"; |
||||||
|
import { IntegrationType } from "../../client/src/api"; |
||||||
|
|
||||||
|
const { createProxyMiddleware } = require('http-proxy-middleware'); |
||||||
|
let axios = require('axios') |
||||||
|
let qs = require('querystring') |
||||||
|
|
||||||
|
async function getSpotifyCCAuthToken(clientId: string, clientSecret: string) { |
||||||
|
console.log("Details: ", clientId, clientSecret); |
||||||
|
|
||||||
|
let buf = Buffer.from(clientId + ':' + clientSecret) |
||||||
|
let encoded = buf.toString('base64'); |
||||||
|
|
||||||
|
let response = await axios.post( |
||||||
|
'https://accounts.spotify.com/api/token', |
||||||
|
qs.stringify({ 'grant_type': 'client_credentials' }), |
||||||
|
{ |
||||||
|
'headers': { |
||||||
|
'Authorization': 'Basic ' + encoded, |
||||||
|
'Content-Type': 'application/x-www-form-urlencoded' |
||||||
|
} |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (response.status != 200) { |
||||||
|
throw new Error("Unable to get a Spotify auth token.") |
||||||
|
} |
||||||
|
|
||||||
|
return (await response).data.access_token; |
||||||
|
} |
||||||
|
|
||||||
|
export function createIntegrations(knex: Knex) { |
||||||
|
// This will enable the app to redirect requests like:
|
||||||
|
// /integrations/5/v1/search?q=query
|
||||||
|
// To the external API represented by integration 5, e.g. for spotify:
|
||||||
|
// https://api.spotify.com/v1/search?q=query
|
||||||
|
// Requests need to already have a .user.id set.
|
||||||
|
|
||||||
|
let proxySpotifyCC = createProxyMiddleware({ |
||||||
|
target: 'https://api.spotify.com/', |
||||||
|
changeOrigin: true, |
||||||
|
logLevel: 'debug', |
||||||
|
pathRewrite: (path: string, req: any) => { |
||||||
|
// Remove e.g. "/integrations/5"
|
||||||
|
console.log("Rewrite URL:", path); |
||||||
|
return path.replace(/^\/integrations\/[0-9]+/, ''); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// In the first layer, retrieve integration details and save details
|
||||||
|
// in the request.
|
||||||
|
return async (req: any, res: any, next: any) => { |
||||||
|
// Determine the integration to use.
|
||||||
|
req._integrationId = parseInt(req.url.match(/^\/([0-9]+)/)[1]); |
||||||
|
console.log("URL:", req.url, 'match:', req._integrationId) |
||||||
|
if (!req._integrationId) { |
||||||
|
res.status(400).send({ reason: "An integration ID should be provided in the URL." }); |
||||||
|
return; |
||||||
|
} |
||||||
|
req._integration = (await knex.select(['id', 'name', 'type', 'details', 'secretDetails']) |
||||||
|
.from('integrations') |
||||||
|
.where({ 'user': req.user.id, 'id': req._integrationId }))[0]; |
||||||
|
if (!req._integration) { |
||||||
|
res.status(404).send(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
req._integration.details = JSON.parse(req._integration.details); |
||||||
|
req._integration.secretDetails = JSON.parse(req._integration.secretDetails); |
||||||
|
|
||||||
|
switch (req._integration.type) { |
||||||
|
case IntegrationType.SpotifyClientCredentials: { |
||||||
|
console.log("Integration: ", req._integration) |
||||||
|
// FIXME: persist the token
|
||||||
|
req._access_token = await getSpotifyCCAuthToken( |
||||||
|
req._integration.details.clientId, |
||||||
|
req._integration.secretDetails.clientSecret, |
||||||
|
) |
||||||
|
if (!req._access_token) { |
||||||
|
res.status(500).send({ reason: "Unable to get Spotify auth token." }) |
||||||
|
} |
||||||
|
req.headers["Authorization"] = "Bearer " + req._access_token; |
||||||
|
return proxySpotifyCC(req, res, next); |
||||||
|
} |
||||||
|
default: { |
||||||
|
res.status(500).send({ reason: "Unsupported integration type " + req._integration.type }) |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// // First add a layer which creates a token and saves it in the request.
|
||||||
|
// app.use((req: any, res: any, next: any) => {
|
||||||
|
// updateToken('c3e5e605e7814cdf94cd86eeba6f4c4f', '5d870c84a3c34aa3a4cf803aa95cb96a')
|
||||||
|
// .then(() => {
|
||||||
|
// req._access_token = authToken;
|
||||||
|
// next();
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// app.use(
|
||||||
|
// '/spotifycc',
|
||||||
|
// createProxyMiddleware({
|
||||||
|
// target: 'https://api.spotify.com/',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// onProxyReq: onProxyReq,
|
||||||
|
// logLevel: 'debug',
|
||||||
|
// pathRewrite: { '^/spotifycc': '' },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
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'); |
||||||
|
} |
||||||
|
|
@ -0,0 +1,127 @@ |
|||||||
|
const chai = require('chai'); |
||||||
|
const chaiHttp = require('chai-http'); |
||||||
|
const express = require('express'); |
||||||
|
import { SetupApp } from '../../../app'; |
||||||
|
import * as helpers from './helpers'; |
||||||
|
import { sha512 } from 'js-sha512'; |
||||||
|
import { IntegrationType } from '../../../../client/src/api'; |
||||||
|
|
||||||
|
async function init() { |
||||||
|
chai.use(chaiHttp); |
||||||
|
const app = express(); |
||||||
|
const knex = await helpers.initTestDB(); |
||||||
|
|
||||||
|
// Add test users.
|
||||||
|
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
||||||
|
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
||||||
|
|
||||||
|
SetupApp(app, knex, ''); |
||||||
|
|
||||||
|
// Login as a test user.
|
||||||
|
var agent = chai.request.agent(app); |
||||||
|
await agent |
||||||
|
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
||||||
|
.send({}); |
||||||
|
return agent; |
||||||
|
} |
||||||
|
|
||||||
|
describe('POST /integration with missing or wrong data', () => { |
||||||
|
it('should fail', async done => { |
||||||
|
let agent = await init(); |
||||||
|
let req = agent.keepOpen(); |
||||||
|
try { |
||||||
|
await helpers.createIntegration(req, { type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 400); |
||||||
|
await helpers.createIntegration(req, { name: "A", details: {}, secretDetails: {} }, 400); |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, secretDetails: {} }, 400); |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, }, 400); |
||||||
|
await helpers.createIntegration(req, { name: "A", type: "NonexistentType", details: {}, secretDetails: {} }, 400); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
agent.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('POST /integration with a correct request', () => { |
||||||
|
it('should succeed', async done => { |
||||||
|
let agent = await init(); |
||||||
|
let req = agent.keepOpen(); |
||||||
|
try { |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
agent.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('PUT /integration with a correct request', () => { |
||||||
|
it('should succeed', async done => { |
||||||
|
let agent = await init(); |
||||||
|
let req = agent.keepOpen(); |
||||||
|
try { |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
||||||
|
await helpers.modifyIntegration(req, 1, { name: "B", type: IntegrationType.SpotifyClientCredentials, details: { secret: 'cat' }, secretDetails: {} }, 200); |
||||||
|
await helpers.checkIntegration(req, 1, 200, { name: "B", type: IntegrationType.SpotifyClientCredentials, details: { secret: 'cat' } }) |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
agent.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('PUT /integration with wrong data', () => { |
||||||
|
it('should fail', async done => { |
||||||
|
let agent = await init(); |
||||||
|
let req = agent.keepOpen(); |
||||||
|
try { |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
||||||
|
await helpers.modifyIntegration(req, 1, { name: "B", type: "UnknownType", details: {}, secretDetails: {} }, 400); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
agent.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('DELETE /integration with a correct request', () => { |
||||||
|
it('should succeed', async done => { |
||||||
|
let agent = await init(); |
||||||
|
let req = agent.keepOpen(); |
||||||
|
try { |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
||||||
|
await helpers.checkIntegration(req, 1, 200, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {} }) |
||||||
|
await helpers.deleteIntegration(req, 1, 200); |
||||||
|
await helpers.checkIntegration(req, 1, 404); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
agent.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('GET /integration list with a correct request', () => { |
||||||
|
it('should succeed', async done => { |
||||||
|
let agent = await init(); |
||||||
|
let req = agent.keepOpen(); |
||||||
|
try { |
||||||
|
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
||||||
|
await helpers.createIntegration(req, { name: "B", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 2 }); |
||||||
|
await helpers.createIntegration(req, { name: "C", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 3 }); |
||||||
|
await helpers.listIntegrations(req, 200, [ |
||||||
|
{ id: 1, name: "A", type: IntegrationType.SpotifyClientCredentials, details: {} }, |
||||||
|
{ id: 2, name: "B", type: IntegrationType.SpotifyClientCredentials, details: {} }, |
||||||
|
{ id: 3, name: "C", type: IntegrationType.SpotifyClientCredentials, details: {} }, |
||||||
|
]); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
agent.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue