Further back-end work, add editor for integrations in front-end.

pull/34/head
Sander Vocke 5 years ago
parent d3a901e826
commit 5a073fb3b8
  1. 22
      client/src/api.ts
  2. 1
      client/src/assets/spotify_icon.svg
  3. 5
      client/src/components/MainWindow.tsx
  4. 8
      client/src/components/appbar/AppBar.tsx
  5. 4
      client/src/components/common/StoreLinkIcon.tsx
  6. 6
      client/src/components/windows/Windows.tsx
  7. 287
      client/src/components/windows/settings/IntegrationSettingsEditor.tsx
  8. 59
      client/src/components/windows/settings/SettingsWindow.tsx
  9. 63
      client/src/lib/backend/integrations.tsx
  10. 72
      server/app.ts
  11. 160
      server/endpoints/Album.ts
  12. 64
      server/endpoints/AlbumDetails.ts
  13. 112
      server/endpoints/Artist.ts
  14. 41
      server/endpoints/ArtistDetails.ts
  15. 96
      server/endpoints/CreateAlbum.ts
  16. 70
      server/endpoints/CreateArtist.ts
  17. 43
      server/endpoints/CreateIntegration.ts
  18. 118
      server/endpoints/CreateSong.ts
  19. 62
      server/endpoints/CreateTag.ts
  20. 49
      server/endpoints/DeleteIntegration.ts
  21. 78
      server/endpoints/DeleteTag.ts
  22. 201
      server/endpoints/Integration.ts
  23. 34
      server/endpoints/IntegrationDetails.ts
  24. 78
      server/endpoints/MergeTag.ts
  25. 52
      server/endpoints/ModifyIntegration.ts
  26. 191
      server/endpoints/ModifySong.ts
  27. 66
      server/endpoints/ModifyTag.ts
  28. 2
      server/endpoints/Query.ts
  29. 2
      server/endpoints/RegisterUser.ts
  30. 372
      server/endpoints/Song.ts
  31. 68
      server/endpoints/SongDetails.ts
  32. 306
      server/endpoints/Tag.ts
  33. 34
      server/endpoints/TagDetails.ts
  34. 21
      server/test/integration/flows/IntegrationFlow.js
  35. 14
      server/test/integration/flows/helpers.js

@ -362,12 +362,19 @@ export enum IntegrationType {
spotify = "spotify", spotify = "spotify",
} }
export interface SpotifyIntegrationDetails {
clientId: string,
clientSecret: string,
}
export type IntegrationDetails = SpotifyIntegrationDetails;
// Create a new integration (POST). // Create a new integration (POST).
export const CreateIntegrationEndpoint = '/integration'; export const CreateIntegrationEndpoint = '/integration';
export interface CreateIntegrationRequest { export interface CreateIntegrationRequest {
name: string, name: string,
type: IntegrationType, type: IntegrationType,
details: any, details: IntegrationDetails,
} }
export interface CreateIntegrationResponse { export interface CreateIntegrationResponse {
id: number; id: number;
@ -385,7 +392,7 @@ export const ModifyIntegrationEndpoint = '/integration/:id';
export interface ModifyIntegrationRequest { export interface ModifyIntegrationRequest {
name?: string, name?: string,
type?: IntegrationType, type?: IntegrationType,
details?: any, details?: IntegrationDetails,
} }
export interface ModifyIntegrationResponse { } export interface ModifyIntegrationResponse { }
export function checkModifyIntegrationRequest(req: any): boolean { export function checkModifyIntegrationRequest(req: any): boolean {
@ -399,12 +406,21 @@ export interface IntegrationDetailsRequest { }
export interface IntegrationDetailsResponse { export interface IntegrationDetailsResponse {
name: string, name: string,
type: IntegrationType, type: IntegrationType,
details: any, details: IntegrationDetails,
} }
export function checkIntegrationDetailsRequest(req: any): boolean { export function checkIntegrationDetailsRequest(req: any): boolean {
return true; return true;
} }
// List integrations (GET).
export const ListIntegrationsEndpoint = '/integration';
export interface ListIntegrationsRequest { }
export interface ListIntegrationsItem extends IntegrationDetailsResponse { id: number }
export type ListIntegrationsResponse = ListIntegrationsItem[];
export function checkListIntegrationsRequest(req: any): boolean {
return true;
}
// Delete integration (DELETE). // Delete integration (DELETE).
export const DeleteIntegrationEndpoint = '/integration/:id'; export const DeleteIntegrationEndpoint = '/integration/:id';
export interface DeleteIntegrationRequest { } export interface DeleteIntegrationRequest { }

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2931 2931" width="2931" height="2931"><style>.st0{fill:#2ebd59}</style><path class="st0" d="M1465.5 0C656.1 0 0 656.1 0 1465.5S656.1 2931 1465.5 2931 2931 2274.9 2931 1465.5C2931 656.2 2274.9.1 1465.5 0zm672.1 2113.6c-26.3 43.2-82.6 56.7-125.6 30.4-344.1-210.3-777.3-257.8-1287.4-141.3-49.2 11.3-98.2-19.5-109.4-68.7-11.3-49.2 19.4-98.2 68.7-109.4C1242.1 1697.1 1721 1752 2107.3 1988c43 26.5 56.7 82.6 30.3 125.6zm179.3-398.9c-33.1 53.8-103.5 70.6-157.2 37.6-393.8-242.1-994.4-312.2-1460.3-170.8-60.4 18.3-124.2-15.8-142.6-76.1-18.2-60.4 15.9-124.1 76.2-142.5 532.2-161.5 1193.9-83.3 1646.2 194.7 53.8 33.1 70.8 103.4 37.7 157.1zm15.4-415.6c-472.4-280.5-1251.6-306.3-1702.6-169.5-72.4 22-149-18.9-170.9-91.3-21.9-72.4 18.9-149 91.4-171 517.7-157.1 1378.2-126.8 1922 196 65.1 38.7 86.5 122.8 47.9 187.8-38.5 65.2-122.8 86.7-187.8 48z"/></svg>

After

Width:  |  Height:  |  Size: 907 B

@ -12,6 +12,7 @@ import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
import LoginWindow from './windows/login/LoginWindow'; import LoginWindow from './windows/login/LoginWindow';
import { useAuth } from '../lib/useAuth'; import { useAuth } from '../lib/useAuth';
import RegisterWindow from './windows/register/RegisterWindow'; import RegisterWindow from './windows/register/RegisterWindow';
import SettingsWindow from './windows/settings/SettingsWindow';
const darkTheme = createMuiTheme({ const darkTheme = createMuiTheme({
palette: { palette: {
@ -57,6 +58,10 @@ export default function MainWindow(props: any) {
<AppBar selectedTab={null} /> <AppBar selectedTab={null} />
<RegisterWindow /> <RegisterWindow />
</Route> </Route>
<PrivateRoute path="/settings">
<AppBar selectedTab={null} />
<SettingsWindow />
</PrivateRoute>
<PrivateRoute path="/query"> <PrivateRoute path="/query">
<AppBar selectedTab={AppBarTab.Query} /> <AppBar selectedTab={AppBarTab.Query} />
<QueryWindow /> <QueryWindow />

@ -28,6 +28,8 @@ export function UserMenu(props: {
onClose: () => void, onClose: () => void,
}) { }) {
let auth = useAuth(); let auth = useAuth();
let history = useHistory();
const pos = props.open && props.position ? const pos = props.open && props.position ?
{ left: props.position[0], top: props.position[1] } { left: props.position[0], top: props.position[1] }
: { left: 0, top: 0 } : { left: 0, top: 0 }
@ -41,6 +43,12 @@ export function UserMenu(props: {
> >
<Box p={2}> <Box p={2}>
{auth.user?.email || "Unknown user"} {auth.user?.email || "Unknown user"}
<MenuItem
onClick={() => {
props.onClose();
history.replace('/settings')
}}
>User Settings</MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
props.onClose(); props.onClose();

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { ReactComponent as GPMIcon } from '../../assets/googleplaymusic_icon.svg'; import { ReactComponent as GPMIcon } from '../../assets/googleplaymusic_icon.svg';
import { ReactComponent as SpotifyIcon } from '../../assets/spotify_icon.svg';
export enum ExternalStore { export enum ExternalStore {
GooglePlayMusic = "GPM", GooglePlayMusic = "GPM",
Spotify = "Spotify",
} }
export interface IProps { export interface IProps {
@ -22,6 +24,8 @@ export default function StoreLinkIcon(props: any) {
switch(whichStore) { switch(whichStore) {
case ExternalStore.GooglePlayMusic: case ExternalStore.GooglePlayMusic:
return <GPMIcon {...restProps}/>; return <GPMIcon {...restProps}/>;
case ExternalStore.Spotify:
return <SpotifyIcon {...restProps}/>;
default: default:
throw new Error("Unknown external store: " + whichStore) throw new Error("Unknown external store: " + whichStore)
} }

@ -14,6 +14,7 @@ import { songGetters } from '../../lib/songGetters';
import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow'; import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
import { RegisterWindowReducer } from './register/RegisterWindow'; import { RegisterWindowReducer } from './register/RegisterWindow';
import { LoginWindowReducer } from './login/LoginWindow'; import { LoginWindowReducer } from './login/LoginWindow';
import { SettingsWindowReducer } from './settings/SettingsWindow';
export enum WindowType { export enum WindowType {
Query = "Query", Query = "Query",
@ -24,6 +25,7 @@ export enum WindowType {
ManageTags = "ManageTags", ManageTags = "ManageTags",
Login = "Login", Login = "Login",
Register = "Register", Register = "Register",
Settings = "Settings",
} }
export interface WindowState { } export interface WindowState { }
@ -37,6 +39,7 @@ export const newWindowReducer = {
[WindowType.ManageTags]: ManageTagsWindowReducer, [WindowType.ManageTags]: ManageTagsWindowReducer,
[WindowType.Login]: LoginWindowReducer, [WindowType.Login]: LoginWindowReducer,
[WindowType.Register]: RegisterWindowReducer, [WindowType.Register]: RegisterWindowReducer,
[WindowType.Settings]: SettingsWindowReducer,
} }
export const newWindowState = { export const newWindowState = {
@ -94,4 +97,7 @@ export const newWindowState = {
[WindowType.Register]: () => { [WindowType.Register]: () => {
return {} return {}
}, },
[WindowType.Settings]: () => {
return {}
},
} }

@ -0,0 +1,287 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../../../lib/useAuth';
import { Box, CircularProgress, IconButton, Typography, FormControl, Select, MenuItem, TextField, Menu } from '@material-ui/core';
import { IntegrationDetails } from '../../../api';
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 * as serverApi from '../../../api';
import StoreLinkIcon, { ExternalStore } from '../../common/StoreLinkIcon';
import { v4 as genUuid } from 'uuid';
let _ = require('lodash')
interface EditIntegrationProps {
integration: serverApi.IntegrationDetailsResponse,
original: serverApi.IntegrationDetailsResponse,
editing: boolean,
submitting: boolean,
onChange: (p: serverApi.IntegrationDetailsResponse, editing: boolean) => void,
onSubmit: () => void,
onDelete: () => void,
}
function EditIntegration(props: EditIntegrationProps) {
let IntegrationHeaders: Record<any, any> = {
[serverApi.IntegrationType.spotify]: <Box display="flex" alignItems="center">
<StoreLinkIcon
style={{ height: '40px', width: '40px' }}
whichStore={ExternalStore.Spotify}
/>
<Typography>Spotify</Typography>
</Box>
}
return <Box display="flex" flexDirection="column" border={1}>
{IntegrationHeaders[props.integration.type]}
<Box display="flex" alignItems="center">
<Typography>Name:</Typography>
{props.editing ?
<TextField
variant="outlined"
value={props.integration.name || ""}
label="Name"
onChange={(e: any) => props.onChange({
...props.integration,
name: e.target.value,
}, props.editing)}
/> :
<Typography>{props.integration.name}</Typography>}
</Box>
{props.integration.type === serverApi.IntegrationType.spotify && <>
<Box display="flex" alignItems="center">
<Typography>Client id:</Typography>
{props.editing ?
<TextField
variant="outlined"
value={props.integration.details.clientId || ""}
label="Client Id:"
onChange={(e: any) => props.onChange({
...props.integration,
details: {
...props.integration.details,
clientId: e.target.value,
}
}, props.editing)}
/> :
<Typography>{props.integration.details.clientId}</Typography>}
</Box>
<Box display="flex" alignItems="center">
<Typography>Client secret:</Typography>
{props.editing ?
<TextField
variant="outlined"
value={props.integration.details.clientSecret || ""}
label="Client secret:"
onChange={(e: any) => props.onChange({
...props.integration,
details: {
...props.integration.details,
clientSecret: e.target.value,
}
}, props.editing)}
/> :
<Typography>{props.integration.details.clientSecret}</Typography>}
</Box>
{!props.editing && !props.submitting && <IconButton
onClick={() => { props.onChange(props.integration, true); }}
><EditIcon /></IconButton>}
{props.editing && !props.submitting && <IconButton
onClick={() => { props.onSubmit(); }}
><CheckIcon /></IconButton>}
{!props.submitting && <IconButton
onClick={() => { props.onDelete(); }}
><DeleteIcon /></IconButton>}
{props.submitting && <CircularProgress />}
</>}
</Box >
}
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(serverApi.IntegrationType.spotify);
props.onClose();
}}
>Spotify</MenuItem>
</Menu>
}
export default function IntegrationSettingsEditor(props: {}) {
interface EditorState {
id: string, //uniquely identifies this editor in the window.
upstreamId: number | null, //back-end ID for this integration if any.
integration: serverApi.IntegrationDetailsResponse,
original: serverApi.IntegrationDetailsResponse,
editing: boolean,
submitting: boolean,
}
let [editors, setEditors] = useState<EditorState[] | null>(null);
const [addMenuPos, setAddMenuPos] = React.useState<null | number[]>(null);
const onOpenAddMenu = (e: any) => {
setAddMenuPos([e.clientX, e.clientY])
};
const onCloseAddMenu = () => {
setAddMenuPos(null);
};
const submitEditor = (state: EditorState) => {
let integration = state.integration;
if (state.upstreamId === null) {
createIntegration(integration).then((response: any) => {
if (!response.id) {
throw new Error('failed to submit integration.')
}
let cpy = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = false;
s.editing = false;
s.upstreamId = response.id;
}
})
setEditors(cpy);
})
} else {
modifyIntegration(state.upstreamId, integration).then(() => {
let cpy = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = false;
s.editing = false;
}
})
setEditors(cpy);
})
}
}
const deleteEditor = (state: EditorState) => {
if(!state.upstreamId) {
throw new Error('Cannot delete integration: has no upstream')
}
deleteIntegration(state.upstreamId).then((response: any) => {
let cpy = _.cloneDeep(editors).filter(
(e: any) => e.id !== state.id
);
setEditors(cpy);
})
}
useEffect(() => {
getIntegrations()
.then((integrations: serverApi.ListIntegrationsResponse) => {
setEditors(integrations.map((i: any, idx: any) => {
return {
integration: { ...i },
original: { ...i },
id: genUuid(),
editing: false,
submitting: false,
upstreamId: i.id,
}
}));
});
}, []);
// FIXME: add button should show a drop-down to choose a fixed integration type.
// Otherwise we need dynamic switching of the type's fields.
return <>
<Box>
{editors === null && <CircularProgress />}
{editors && <>
{editors.map((state: EditorState) => <EditIntegration
integration={state.integration}
original={state.original}
editing={state.editing}
submitting={state.submitting}
onChange={(p: serverApi.IntegrationDetailsResponse, editing: boolean) => {
if (!editors) {
throw new Error('cannot change editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.integration = p;
s.editing = editing;
}
})
setEditors(cpy);
}}
onSubmit={() => {
if (!editors) {
throw new Error('cannot submit editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = true;
}
})
setEditors(cpy);
submitEditor(state);
}}
onDelete={() => {
if (!editors) {
throw new Error('cannot submit editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: any) => {
if (s.id === state.id) {
s.submitting = true;
}
})
setEditors(cpy);
deleteEditor(state);
}}
/>)}
<IconButton onClick={onOpenAddMenu}>
<AddIcon />
</IconButton>
</>}
</Box>
<AddIntegrationMenu
position={addMenuPos}
open={addMenuPos !== null}
onClose={onCloseAddMenu}
onAdd={(type: serverApi.IntegrationType) => {
let cpy = _.cloneDeep(editors);
cpy.push({
integration: {
type: serverApi.IntegrationType.spotify,
details: {
clientId: '',
clientSecret: '',
},
name: '',
},
original: null,
id: genUuid(),
editing: true,
submitting: false,
upstreamId: null,
})
setEditors(cpy);
}}
/>
</>;
}

@ -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 './IntegrationSettingsEditor';
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,63 @@
import * as serverApi from '../../api';
export async function createIntegration(details: serverApi.CreateIntegrationRequest) {
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(details),
};
const response = await fetch((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 fetch(
(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 fetch(
(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 fetch(
(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;
}

@ -2,32 +2,15 @@ const bodyParser = require('body-parser');
import * as api from '../client/src/api'; import * as api from '../client/src/api';
import Knex from 'knex'; import Knex from 'knex';
import { QueryEndpointHandler } from './endpoints/Query'; import { Query } from './endpoints/Query';
import { CreateArtistEndpointHandler } from './endpoints/CreateArtist'; import { PostArtist, PutArtist, GetArtist } from './endpoints/Artist';
import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetails' import { PostAlbum, PutAlbum, GetAlbum } from './endpoints/Album';
import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtist'; import { PostSong, PutSong, GetSong } from './endpoints/Song';
import { PostTag, PutTag, GetTag, DeleteTag, MergeTag } from './endpoints/Tag';
import { PostIntegration, PutIntegration, GetIntegration, DeleteIntegration, ListIntegrations } from './endpoints/Integration';
import { ModifySongEndpointHandler } from './endpoints/ModifySong'; import { RegisterUser } from './endpoints/RegisterUser';
import { SongDetailsEndpointHandler } from './endpoints/SongDetails';
import { CreateSongEndpointHandler } from './endpoints/CreateSong';
import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbum';
import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbum';
import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetails';
import { CreateTagEndpointHandler } from './endpoints/CreateTag';
import { ModifyTagEndpointHandler } from './endpoints/ModifyTag';
import { DeleteTagEndpointHandler } from './endpoints/DeleteTag';
import { MergeTagEndpointHandler } from './endpoints/MergeTag';
import { TagDetailsEndpointHandler } from './endpoints/TagDetails';
import { RegisterUserEndpointHandler } from './endpoints/RegisterUser';
import { CreateIntegrationEndpointHandler } from './endpoints/CreateIntegration';
import { ModifyIntegrationEndpointHandler } from './endpoints/ModifyIntegration';
import { DeleteIntegrationEndpointHandler } from './endpoints/DeleteIntegration';
import { IntegrationDetailsEndpointHandler } from './endpoints/IntegrationDetails';
import * as endpointTypes from './endpoints/types'; import * as endpointTypes from './endpoints/types';
import { sha512 } from 'js-sha512'; import { sha512 } from 'js-sha512';
@ -118,32 +101,33 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
} }
// Set up REST API endpoints // Set up REST API endpoints
app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(CreateSongEndpointHandler)); app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(PostSong));
app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(ModifySongEndpointHandler)); app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(PutSong));
app.get(apiBaseUrl + api.SongDetailsEndpoint, checkLogin(), _invoke(SongDetailsEndpointHandler)); app.get(apiBaseUrl + api.SongDetailsEndpoint, checkLogin(), _invoke(GetSong));
app.post(apiBaseUrl + api.QueryEndpoint, checkLogin(), _invoke(QueryEndpointHandler)); app.post(apiBaseUrl + api.QueryEndpoint, checkLogin(), _invoke(Query));
app.post(apiBaseUrl + api.CreateArtistEndpoint, checkLogin(), _invoke(CreateArtistEndpointHandler)); app.post(apiBaseUrl + api.CreateArtistEndpoint, checkLogin(), _invoke(PostArtist));
app.put(apiBaseUrl + api.ModifyArtistEndpoint, checkLogin(), _invoke(ModifyArtistEndpointHandler)); app.put(apiBaseUrl + api.ModifyArtistEndpoint, checkLogin(), _invoke(PutArtist));
app.get(apiBaseUrl + api.ArtistDetailsEndpoint, checkLogin(), _invoke(ArtistDetailsEndpointHandler)); app.get(apiBaseUrl + api.ArtistDetailsEndpoint, checkLogin(), _invoke(GetArtist));
app.post(apiBaseUrl + api.CreateAlbumEndpoint, checkLogin(), _invoke(CreateAlbumEndpointHandler)); app.post(apiBaseUrl + api.CreateAlbumEndpoint, checkLogin(), _invoke(PostAlbum));
app.put(apiBaseUrl + api.ModifyAlbumEndpoint, checkLogin(), _invoke(ModifyAlbumEndpointHandler)); app.put(apiBaseUrl + api.ModifyAlbumEndpoint, checkLogin(), _invoke(PutAlbum));
app.get(apiBaseUrl + api.AlbumDetailsEndpoint, checkLogin(), _invoke(AlbumDetailsEndpointHandler)); app.get(apiBaseUrl + api.AlbumDetailsEndpoint, checkLogin(), _invoke(GetAlbum));
app.post(apiBaseUrl + api.CreateTagEndpoint, checkLogin(), _invoke(CreateTagEndpointHandler)); app.post(apiBaseUrl + api.CreateTagEndpoint, checkLogin(), _invoke(PostTag));
app.put(apiBaseUrl + api.ModifyTagEndpoint, checkLogin(), _invoke(ModifyTagEndpointHandler)); app.put(apiBaseUrl + api.ModifyTagEndpoint, checkLogin(), _invoke(PutTag));
app.get(apiBaseUrl + api.TagDetailsEndpoint, checkLogin(), _invoke(TagDetailsEndpointHandler)); app.get(apiBaseUrl + api.TagDetailsEndpoint, checkLogin(), _invoke(GetTag));
app.delete(apiBaseUrl + api.DeleteTagEndpoint, checkLogin(), _invoke(DeleteTagEndpointHandler)); app.delete(apiBaseUrl + api.DeleteTagEndpoint, checkLogin(), _invoke(DeleteTag));
app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTagEndpointHandler)); app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTag));
app.post(apiBaseUrl + api.CreateIntegrationEndpoint, checkLogin(), _invoke(CreateIntegrationEndpointHandler)); app.post(apiBaseUrl + api.CreateIntegrationEndpoint, checkLogin(), _invoke(PostIntegration));
app.put(apiBaseUrl + api.ModifyIntegrationEndpoint, checkLogin(), _invoke(ModifyIntegrationEndpointHandler)); app.put(apiBaseUrl + api.ModifyIntegrationEndpoint, checkLogin(), _invoke(PutIntegration));
app.get(apiBaseUrl + api.IntegrationDetailsEndpoint, checkLogin(), _invoke(IntegrationDetailsEndpointHandler)); app.get(apiBaseUrl + api.IntegrationDetailsEndpoint, checkLogin(), _invoke(GetIntegration));
app.delete(apiBaseUrl + api.DeleteIntegrationEndpoint, checkLogin(), _invoke(DeleteIntegrationEndpointHandler)); app.delete(apiBaseUrl + api.DeleteIntegrationEndpoint, checkLogin(), _invoke(DeleteIntegration));
app.get(apiBaseUrl + api.ListIntegrationsEndpoint, checkLogin(), _invoke(ListIntegrations));
app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUserEndpointHandler)); app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUser));
app.post(apiBaseUrl + api.LoginEndpoint, passport.authenticate('local'), (req: any, res: any) => { app.post(apiBaseUrl + api.LoginEndpoint, passport.authenticate('local'), (req: any, res: any) => {
res.status(200).send({ userId: req.user.id }); res.status(200).send({ userId: req.user.id });
}); });

@ -1,11 +1,165 @@
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex'; import Knex from 'knex';
import asJson from '../lib/asJson';
export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { export const GetAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkAlbumDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid GetAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const { id: userId } = req.user;
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);
}
}
export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateAlbumRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid PostAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.CreateAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Post Album ", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('id')
.from('artists')
.where({ 'user': userId })
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('tags')
.where({ 'user': userId })
.whereIn('id', reqObject.tagIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Wait for the requests to finish.
var [artists, tags] = await Promise.all([artistIdsPromise, tagIdsPromise]);;
// Check that we found all artists and tags we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Create the album.
const albumId = (await trx('albums')
.insert({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
user: userId,
})
.returning('id') // Needed for Postgres
)[0];
// Link the artists via the linking table.
if (artists && artists.length) {
await trx('artists_albums').insert(
artists.map((artistId: number) => {
return {
artistId: artistId,
albumId: albumId,
}
})
)
}
// Link the tags via the linking table.
if (tags && tags.length) {
await trx('albums_tags').insert(
tags.map((tagId: number) => {
return {
albumId: albumId,
tagId: tagId,
}
})
)
}
// Respond to the request.
const responseObject: api.CreateSongResponse = {
id: albumId
};
res.status(200).send(responseObject);
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyAlbumRequest(req)) { if (!api.checkModifyAlbumRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ModifyAlbum request: ' + JSON.stringify(req.body), internalMessage: 'Invalid PutAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
}; };
throw e; throw e;
@ -13,7 +167,7 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res:
const reqObject: api.ModifyAlbumRequest = req.body; const reqObject: api.ModifyAlbumRequest = req.body;
const { id: userId } = req.user; const { id: userId } = req.user;
console.log("User ", userId, ": Modify Album ", reqObject); console.log("User ", userId, ": Put Album ", reqObject);
await knex.transaction(async (trx) => { await knex.transaction(async (trx) => {
try { try {

@ -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,11 +1,117 @@
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex'; import Knex from 'knex';
import asJson from '../lib/asJson';
export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { export const GetArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkArtistDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid GetArtist request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const { id: userId } = req.user;
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)
}
}
export const PostArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateArtistRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid PostArtist 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();
}
});
}
export const PutArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyArtistRequest(req)) { if (!api.checkModifyArtistRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ModifyArtist request: ' + JSON.stringify(req.body), internalMessage: 'Invalid PutArtist request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
}; };
throw e; throw e;
@ -13,7 +119,7 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res
const reqObject: api.ModifyArtistRequest = req.body; const reqObject: api.ModifyArtistRequest = req.body;
const { id: userId } = req.user; const { id: userId } = req.user;
console.log("User ", userId, ": Modify Artist ", reqObject); console.log("User ", userId, ": Put Artist ", reqObject);
await knex.transaction(async (trx) => { await knex.transaction(async (trx) => {
try { try {

@ -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,43 +0,0 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const CreateIntegrationEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateIntegrationRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid CreateIntegration request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.CreateIntegrationRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Create 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),
}
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();
}
})
}

@ -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,49 +0,0 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const DeleteIntegrationEndpointHandler: 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();
}
})
}

@ -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,201 @@
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),
}
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;
try {
const integrations: api.ListIntegrationsResponse = (
await knex.select(['id', 'name', 'type', 'details'])
.from('integrations')
.where({ user: userId })
).map((object: any) => {
return {
id: object.id,
name: object.name,
type: object.type,
details: asJson(object.details),
}
})
await 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; }
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,34 +0,0 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const IntegrationDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkIntegrationDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid IntegrationDetails 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: JSON.parse(integration.details),
}
await res.send(response);
} else {
await res.status(404).send({});
}
} catch (e) {
catchUnhandledErrors(e)
}
}

@ -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,52 +0,0 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const ModifyIntegrationEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyIntegrationRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid ModifyIntegration request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.ModifyIntegrationRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Modify 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; }
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,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();
}
})
}

@ -258,7 +258,7 @@ async function getFullTag(knex: Knex, userId: number, tag: any): Promise<any> {
return await resolveTag(tag); return await resolveTag(tag);
} }
export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { export const Query: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkQueryRequest(req.body)) { if (!api.checkQueryRequest(req.body)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid Query request: ' + JSON.stringify(req.body), internalMessage: 'Invalid Query request: ' + JSON.stringify(req.body),

@ -4,7 +4,7 @@ import Knex from 'knex';
import { sha512 } from 'js-sha512'; import { sha512 } from 'js-sha512';
export const RegisterUserEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { export const RegisterUser: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkRegisterUserRequest(req)) { if (!api.checkRegisterUserRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body), internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body),

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

@ -103,3 +103,24 @@ describe('DELETE /integration with a correct request', () => {
} }
}); });
}); });
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.spotify, details: {} }, 200, { id: 1 });
await helpers.createIntegration(req, { name: "B", type: IntegrationType.spotify, details: {} }, 200, { id: 2 });
await helpers.createIntegration(req, { name: "C", type: IntegrationType.spotify, details: {} }, 200, { id: 3 });
await helpers.listIntegrations(req, 200, [
{ id: 1, name: "A", type: IntegrationType.spotify, details: {} },
{ id: 2, name: "B", type: IntegrationType.spotify, details: {} },
{ id: 3, name: "C", type: IntegrationType.spotify, details: {} },
]);
} finally {
req.close();
agent.close();
done();
}
});
});

@ -293,6 +293,20 @@ export async function checkIntegration(
}) })
} }
export async function listIntegrations(
req,
expectStatus = undefined,
expectResponse = undefined,
) {
await req
.get('/integration')
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
return res;
})
}
export async function deleteIntegration( export async function deleteIntegration(
req, req,
id, id,

Loading…
Cancel
Save