diff --git a/client/package-lock.json b/client/package-lock.json
index b8c35fe..74d3f63 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -11286,6 +11286,14 @@
"resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz",
"integrity": "sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q="
},
+ "react-error-boundary": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.0.2.tgz",
+ "integrity": "sha512-KVzCusRTFpUYG0OFJbzbdRuxNQOBiGXVCqyNpBXM9z5NFsFLzMjUXMjx8gTja6M6WH+D2PvP3yKz4d8gD1PRaA==",
+ "requires": {
+ "@babel/runtime": "^7.11.2"
+ }
+ },
"react-error-overlay": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
diff --git a/client/package.json b/client/package.json
index 1fb3ac0..7c82c01 100644
--- a/client/package.json
+++ b/client/package.json
@@ -24,6 +24,7 @@
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^16.13.1",
+ "react-error-boundary": "^3.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "^3.4.3",
"typescript": "~3.7.2",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 08a6107..45f7fa5 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -5,13 +5,12 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import MainWindow from './components/MainWindow';
import { ProvideAuth } from './lib/useAuth';
+import { ProvideIntegrations } from './lib/integration/useIntegrations';
function App() {
return (
-
-
-
+
);
}
diff --git a/client/src/api.ts b/client/src/api.ts
index d1cb334..92333f1 100644
--- a/client/src/api.ts
+++ b/client/src/api.ts
@@ -356,4 +356,82 @@ export function checkRegisterUserRequest(req: any): boolean {
// Note: Login is handled by Passport.js, so it is not explicitly written here.
export const LoginEndpoint = "/login";
-export const LogoutEndpoint = "/logout";
\ No newline at end of file
+export const LogoutEndpoint = "/logout";
+
+export enum IntegrationType {
+ SpotifyClientCredentials = "SpotifyClientCredentials",
+}
+
+export interface SpotifyClientCredentialsDetails {
+ clientId: string,
+}
+
+export interface SpotifyClientCredentialsSecretDetails {
+ clientSecret: string,
+}
+
+export type IntegrationDetails = SpotifyClientCredentialsDetails;
+export type IntegrationSecretDetails = SpotifyClientCredentialsSecretDetails;
+
+// Create a new integration (POST).
+export const CreateIntegrationEndpoint = '/integration';
+export interface CreateIntegrationRequest {
+ name: string,
+ type: IntegrationType,
+ details: IntegrationDetails,
+ secretDetails: IntegrationSecretDetails,
+}
+export interface CreateIntegrationResponse {
+ id: number;
+}
+export function checkCreateIntegrationRequest(req: any): boolean {
+ return "body" in req &&
+ "name" in req.body &&
+ "type" in req.body &&
+ "details" in req.body &&
+ "secretDetails" in req.body &&
+ (req.body.type in IntegrationType);
+}
+
+// Modify an existing integration (PUT).
+export const ModifyIntegrationEndpoint = '/integration/:id';
+export interface ModifyIntegrationRequest {
+ name?: string,
+ type?: IntegrationType,
+ details?: IntegrationDetails,
+ secretDetails?: IntegrationSecretDetails,
+}
+export interface ModifyIntegrationResponse { }
+export function checkModifyIntegrationRequest(req: any): boolean {
+ if("type" in req.body && !(req.body.type in IntegrationType)) return false;
+ return true;
+}
+
+// Get integration details (GET).
+export const IntegrationDetailsEndpoint = '/integration/:id';
+export interface IntegrationDetailsRequest { }
+export interface IntegrationDetailsResponse {
+ name: string,
+ type: IntegrationType,
+ details: IntegrationDetails,
+}
+export function checkIntegrationDetailsRequest(req: any): boolean {
+ return true;
+}
+
+// List integrations (GET).
+export const ListIntegrationsEndpoint = '/integration';
+export interface ListIntegrationsRequest { }
+export interface ListIntegrationsItem extends IntegrationDetailsResponse { id: number }
+export type ListIntegrationsResponse = ListIntegrationsItem[];
+export function checkListIntegrationsRequest(req: any): boolean {
+ return true;
+}
+
+// Delete integration (DELETE).
+export const DeleteIntegrationEndpoint = '/integration/:id';
+export interface DeleteIntegrationRequest { }
+export interface DeleteIntegrationResponse { }
+export function checkDeleteIntegrationRequest(req: any): boolean {
+ return true;
+}
\ No newline at end of file
diff --git a/client/src/assets/spotify_icon.svg b/client/src/assets/spotify_icon.svg
new file mode 100644
index 0000000..cfc993b
--- /dev/null
+++ b/client/src/assets/spotify_icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx
index 2241615..ff2110a 100644
--- a/client/src/components/MainWindow.tsx
+++ b/client/src/components/MainWindow.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core';
import { grey } from '@material-ui/core/colors';
import AppBar, { AppBarTab } from './appbar/AppBar';
@@ -8,10 +8,13 @@ import AlbumWindow from './windows/album/AlbumWindow';
import TagWindow from './windows/tag/TagWindow';
import SongWindow from './windows/song/SongWindow';
import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow';
-import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
+import { BrowserRouter, Switch, Route, Redirect, useHistory } from 'react-router-dom';
import LoginWindow from './windows/login/LoginWindow';
-import { useAuth } from '../lib/useAuth';
+import { useAuth, ProvideAuth } from '../lib/useAuth';
import RegisterWindow from './windows/register/RegisterWindow';
+import SettingsWindow from './windows/settings/SettingsWindow';
+import { ErrorBoundary } from 'react-error-boundary';
+import { ProvideIntegrations } from '../lib/integration/useIntegrations';
const darkTheme = createMuiTheme({
palette: {
@@ -44,44 +47,52 @@ function PrivateRoute(props: any) {
export default function MainWindow(props: any) {
return
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
}
\ No newline at end of file
diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx
index b39601f..7b6f394 100644
--- a/client/src/components/appbar/AppBar.tsx
+++ b/client/src/components/appbar/AppBar.tsx
@@ -28,6 +28,8 @@ export function UserMenu(props: {
onClose: () => void,
}) {
let auth = useAuth();
+ let history = useHistory();
+
const pos = props.open && props.position ?
{ left: props.position[0], top: props.position[1] }
: { left: 0, top: 0 }
@@ -41,6 +43,12 @@ export function UserMenu(props: {
>
{auth.user?.email || "Unknown user"}
+
diff --git a/client/src/components/windows/artist/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx
index f9008cc..513a556 100644
--- a/client/src/components/windows/artist/ArtistWindow.tsx
+++ b/client/src/components/windows/artist/ArtistWindow.tsx
@@ -12,6 +12,8 @@ import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
+import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request';
+import { useAuth } from '../../../lib/useAuth';
export type ArtistMetadata = serverApi.ArtistDetails;
export type ArtistMetadataChanges = serverApi.ModifyArtistRequest;
@@ -82,6 +84,7 @@ export function ArtistWindowControlled(props: {
}) {
let { metadata, id: artistId, pendingChanges, songsByArtist } = props.state;
let { dispatch } = props;
+ let auth = useAuth();
// Effect to get the artist's metadata.
useEffect(() => {
@@ -92,6 +95,7 @@ export function ArtistWindowControlled(props: {
value: m
});
})
+ .catch((e: NotLoggedInError) => { handleNotLoggedIn(auth, e) })
}, [artistId, dispatch]);
// Effect to get the artist's songs.
@@ -107,7 +111,8 @@ export function ArtistWindowControlled(props: {
},
offset: 0,
limit: -1,
- });
+ })
+ .catch((e: NotLoggedInError) => { handleNotLoggedIn(auth, e) });
dispatch({
type: ArtistWindowStateActions.SetSongs,
value: songs,
@@ -158,6 +163,7 @@ export function ArtistWindowControlled(props: {
type: ArtistWindowStateActions.Reload
})
})
+ .catch((e: NotLoggedInError) => { handleNotLoggedIn(auth, e) })
}} />
{applying && }
diff --git a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx
index 9e4dd7e..e6e6c62 100644
--- a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx
+++ b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx
@@ -11,6 +11,8 @@ import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid';
import Alert from '@material-ui/lab/Alert';
import { useHistory } from 'react-router';
+import { NotLoggedInError, handleNotLoggedIn } from '../../../lib/backend/request';
+import { useAuth } from '../../../lib/useAuth';
var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState {
@@ -355,6 +357,7 @@ export function ManageTagsWindowControlled(props: {
const [newTagMenuPos, setNewTagMenuPos] = React.useState(null);
let { fetchedTags } = props.state;
let { dispatch } = props;
+ let auth = useAuth();
const onOpenNewTagMenu = (e: any) => {
setNewTagMenuPos([e.clientX, e.clientY])
@@ -422,7 +425,9 @@ export function ManageTagsWindowControlled(props: {
props.dispatch({
type: ManageTagsWindowActions.Reset
});
- }).catch((e: Error) => {
+ })
+ .catch((e: NotLoggedInError) => { handleNotLoggedIn(auth, e) })
+ .catch((e: Error) => {
props.dispatch({
type: ManageTagsWindowActions.SetAlert,
value: Failed to save changes: {e.message},
diff --git a/client/src/components/windows/settings/IntegrationSettings.tsx b/client/src/components/windows/settings/IntegrationSettings.tsx
new file mode 100644
index 0000000..dfb7ade
--- /dev/null
+++ b/client/src/components/windows/settings/IntegrationSettings.tsx
@@ -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
+
+ props.onChangeClientId(e.target.value)}
+ />
+
+
+ {
+ props.onChangeClientSecret(e.target.value)
+ }}
+ onFocus={(e: any) => {
+ if (props.clientSecret === null) {
+ // Change from dots to empty input
+ console.log("Focus!")
+ props.onChangeClientSecret('');
+ }
+ }}
+ />
+
+ ;
+}
+
+// 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 = {
+ [serverApi.IntegrationType.SpotifyClientCredentials]:
+
+
+ {IntegrationClasses[serverApi.IntegrationType.SpotifyClientCredentials].getIcon({
+ style: { height: '40px', width: '40px' }
+ })}
+
+ Spotify (using Client Credentials)
+
+ }
+ let IntegrationDescription: Record = {
+ [serverApi.IntegrationType.SpotifyClientCredentials]:
+
+ 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.
+ 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.
+
+ }
+
+ return
+
+
+
+ {IntegrationDescription[props.integration.type]}
+
+ props.onChange && props.onChange({
+ ...props.integration,
+ name: e.target.value,
+ })}
+ />
+
+ {props.integration.type === serverApi.IntegrationType.SpotifyClientCredentials &&
+ 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}
+
+
+ {props.showEditButton && }
+ {props.showSubmitButton && props.onSubmit && props.onSubmit(props.integration)}
+ >}
+ {props.showDeleteButton && }
+ {props.showCancelButton && }
+ {props.showTestButton && }
+
+
+}
+
+let EditorWithTest = (props: any) => {
+ const [testFlashMessage, setTestFlashMessage] =
+ React.useState(undefined);
+ let { integration, ...rest } = props;
+ return {
+ integration.integration.test({})
+ .then(() => {
+ setTestFlashMessage(
+ Integration is active.
+ )
+ })
+ }}
+ 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
+}
+
+function EditIntegrationDialog(props: {
+ open: boolean,
+ onClose?: () => void,
+ upstreamId?: number,
+ integration: IntegrationState,
+ onSubmit?: (p: serverApi.CreateIntegrationRequest) => void,
+ isNew: boolean,
+}) {
+ let [editingIntegration, setEditingIntegration] =
+ useState(props.integration);
+
+ useEffect(() => { setEditingIntegration(props.integration); }, [props.integration]);
+
+ return
+}
+
+export default function IntegrationSettings(props: {}) {
+ const [addMenuPos, setAddMenuPos] = React.useState(null);
+ const [editingState, setEditingState] = React.useState(null);
+
+ let {
+ state: integrations,
+ addIntegration,
+ modifyIntegration,
+ deleteIntegration,
+ updateFromUpstream,
+ } = useIntegrations();
+
+ const onOpenAddMenu = (e: any) => {
+ setAddMenuPos([e.clientX, e.clientY])
+ };
+ const onCloseAddMenu = () => {
+ setAddMenuPos(null);
+ };
+
+ return <>
+
+ {integrations === null && }
+ {Array.isArray(integrations) &&
+ {integrations.map((state: IntegrationState) =>
+ { setEditingState(state); }}
+ onDelete={() => {
+ deleteIntegration(state.id)
+ .then(updateFromUpstream)
+ }}
+ />
+ )}
+
+
+
+ }
+
+ {
+ let p = makeDefaultIntegrationProperties(type);
+ setEditingState({
+ properties: p,
+ integration: makeIntegration(p, -1),
+ id: -1,
+ })
+ }}
+ />
+ {editingState && { 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)
+ }
+ }}
+ />}
+ >;
+}
\ No newline at end of file
diff --git a/client/src/components/windows/settings/SettingsWindow.tsx b/client/src/components/windows/settings/SettingsWindow.tsx
new file mode 100644
index 0000000..1fe79aa
--- /dev/null
+++ b/client/src/components/windows/settings/SettingsWindow.tsx
@@ -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
+}
+
+export function SettingsWindowControlled(props: {
+ state: SettingsWindowState,
+ dispatch: (action: any) => void,
+}) {
+ let history: any = useHistory();
+ let auth: Auth = useAuth();
+
+ return
+
+
+
+ User Settings
+ Integrations
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/client/src/lib/backend/integrations.tsx b/client/src/lib/backend/integrations.tsx
new file mode 100644
index 0000000..f4468ff
--- /dev/null
+++ b/client/src/lib/backend/integrations.tsx
@@ -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;
+}
\ No newline at end of file
diff --git a/client/src/lib/backend/queries.tsx b/client/src/lib/backend/queries.tsx
index d93ef40..d6c1aa9 100644
--- a/client/src/lib/backend/queries.tsx
+++ b/client/src/lib/backend/queries.tsx
@@ -1,5 +1,6 @@
import * as serverApi from '../../api';
import { QueryElem, toApiQuery } from '../query/Query';
+import backendRequest from './request';
export interface QueryArgs {
query?: QueryElem,
@@ -29,7 +30,7 @@ export async function queryArtists(args: QueryArgs) {
};
return (async () => {
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
return json.artists;
})();
@@ -57,7 +58,7 @@ export async function queryAlbums(args: QueryArgs) {
};
return (async () => {
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
return json.albums;
})();
@@ -85,7 +86,7 @@ export async function querySongs(args: QueryArgs) {
};
return (async () => {
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
return json.songs;
})();
@@ -113,7 +114,7 @@ export async function queryTags(args: QueryArgs) {
};
return (async () => {
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts);
let json: any = await response.json();
const tags = json.tags;
diff --git a/client/src/lib/backend/request.tsx b/client/src/lib/backend/request.tsx
new file mode 100644
index 0000000..b5fa5f5
--- /dev/null
+++ b/client/src/lib/backend/request.tsx
@@ -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 {
+ 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();
+}
\ No newline at end of file
diff --git a/client/src/lib/backend/tags.tsx b/client/src/lib/backend/tags.tsx
index 67146e9..0c3ea72 100644
--- a/client/src/lib/backend/tags.tsx
+++ b/client/src/lib/backend/tags.tsx
@@ -1,4 +1,5 @@
import * as serverApi from '../../api';
+import backendRequest from './request';
export async function createTag(details: serverApi.CreateTagRequest) {
const requestOpts = {
@@ -7,7 +8,7 @@ export async function createTag(details: serverApi.CreateTagRequest) {
body: JSON.stringify(details),
};
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.CreateTagEndpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.CreateTagEndpoint, requestOpts)
if (!response.ok) {
throw new Error("Response to tag creation not OK: " + JSON.stringify(response));
}
@@ -21,7 +22,7 @@ export async function modifyTag(id: number, details: serverApi.ModifyTagRequest)
body: JSON.stringify(details),
};
- const response = await fetch(
+ const response = await backendRequest(
(process.env.REACT_APP_BACKEND || "") + serverApi.ModifyTagEndpoint.replace(':id', id.toString()),
requestOpts
);
@@ -35,7 +36,7 @@ export async function deleteTag(id: number) {
method: 'DELETE',
};
- const response = await fetch(
+ const response = await backendRequest(
(process.env.REACT_APP_BACKEND || "") + serverApi.DeleteTagEndpoint.replace(':id', id.toString()),
requestOpts
);
@@ -49,7 +50,7 @@ export async function mergeTag(fromId: number, toId: number) {
method: 'POST',
};
- const response = await fetch(
+ const response = await backendRequest(
(process.env.REACT_APP_BACKEND || "") + serverApi.MergeTagEndpoint
.replace(':id', fromId.toString())
.replace(':toId', toId.toString()),
diff --git a/client/src/lib/integration/Integration.tsx b/client/src/lib/integration/Integration.tsx
new file mode 100644
index 0000000..441262f
--- /dev/null
+++ b/client/src/lib/integration/Integration.tsx
@@ -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 {}
+
+ // Requires feature: GetSongs
+ async getSongs(getSongsParams: any): Promise { return []; }
+
+ // Requires feature: SearchSongs
+ async searchSong(songProps: IntegrationSong): Promise { return []; }
+
+ // Requires feature: SearchAlbum
+ async searchAlbum(albumProps: IntegrationAlbum): Promise { return []; }
+
+ // Requires feature: SearchArtist
+ async searchArtist(artistProps: IntegrationArtist): Promise { return []; }
+}
\ No newline at end of file
diff --git a/client/src/lib/integration/spotify/SpotifyClientCreds.tsx b/client/src/lib/integration/spotify/SpotifyClientCreds.tsx
new file mode 100644
index 0000000..d211907
--- /dev/null
+++ b/client/src/lib/integration/spotify/SpotifyClientCreds.tsx
@@ -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
+ }
+
+ 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 { return []; }
+ async searchAlbum(albumProps: IntegrationAlbum): Promise { return []; }
+ async searchArtist(artistProps: IntegrationArtist): Promise { return []; }
+
+ async search(query: string, type: SearchType):
+ Promise {
+ 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,
+ },
+ }
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/lib/integration/useIntegrations.tsx b/client/src/lib/integration/useIntegrations.tsx
new file mode 100644
index 0000000..883b772
--- /dev/null
+++ b/client/src/lib/integration/useIntegrations.tsx
@@ -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,
+ deleteIntegration: (id: number) => Promise,
+ modifyIntegration: (id: number, v: serverApi.CreateIntegrationRequest) => Promise,
+ updateFromUpstream: () => Promise,
+};
+
+export const IntegrationClasses: Record = {
+ [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({
+ state: [],
+ addIntegration: async () => 0,
+ modifyIntegration: async () => { },
+ deleteIntegration: async () => { },
+ updateFromUpstream: async () => { },
+});
+
+export function ProvideIntegrations(props: { children: any }) {
+ const integrations = useProvideIntegrations();
+ return {props.children};
+}
+
+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,
+ }
+}
\ No newline at end of file
diff --git a/client/src/lib/saveChanges.tsx b/client/src/lib/saveChanges.tsx
index db2205f..666c5af 100644
--- a/client/src/lib/saveChanges.tsx
+++ b/client/src/lib/saveChanges.tsx
@@ -1,4 +1,5 @@
import * as serverApi from '../api';
+import backendRequest from './backend/request';
export async function saveSongChanges(id: number, change: serverApi.ModifySongRequest) {
const requestOpts = {
@@ -8,7 +9,7 @@ export async function saveSongChanges(id: number, change: serverApi.ModifySongRe
};
const endpoint = serverApi.ModifySongEndpoint.replace(":id", id.toString());
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
if(!response.ok) {
throw new Error("Failed to save song changes: " + response.statusText);
}
@@ -22,7 +23,7 @@ export async function saveTagChanges(id: number, change: serverApi.ModifyTagRequ
};
const endpoint = serverApi.ModifyTagEndpoint.replace(":id", id.toString());
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
if(!response.ok) {
throw new Error("Failed to save tag changes: " + response.statusText);
}
@@ -36,7 +37,7 @@ export async function saveArtistChanges(id: number, change: serverApi.ModifyArti
};
const endpoint = serverApi.ModifyArtistEndpoint.replace(":id", id.toString());
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
if(!response.ok) {
throw new Error("Failed to save artist changes: " + response.statusText);
}
@@ -50,7 +51,7 @@ export async function saveAlbumChanges(id: number, change: serverApi.ModifyAlbum
};
const endpoint = serverApi.ModifyAlbumEndpoint.replace(":id", id.toString());
- const response = await fetch((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
+ const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts)
if(!response.ok) {
throw new Error("Failed to save album changes: " + response.statusText);
}
diff --git a/client/src/lib/useAuth.tsx b/client/src/lib/useAuth.tsx
index d6b4995..f25dff4 100644
--- a/client/src/lib/useAuth.tsx
+++ b/client/src/lib/useAuth.tsx
@@ -38,8 +38,37 @@ export const useAuth = () => {
return useContext(authContext);
};
+function persistAuth(auth: AuthUser | null) {
+ let s = window.sessionStorage;
+
+ if(auth === null) {
+ s.removeItem('userId');
+ s.removeItem('userEmail');
+ return;
+ }
+
+ s.setItem('userId', auth.id.toString());
+ s.setItem('userEmail', auth.email);
+ // TODO icon
+}
+
+function loadAuth(): AuthUser | null {
+ let s = window.sessionStorage;
+ let id = s.getItem('userId');
+ let email = s.getItem('userEmail');
+
+ if (id && email) {
+ return {
+ id: parseInt(id),
+ email: email,
+ icon:
+ }
+ }
+ return null;
+}
+
function useProvideAuth() {
- const [user, setUser] = useState(null);
+ const [user, setUser] = useState(loadAuth());
// TODO: password maybe shouldn't be encoded into the URL.
const signin = (email: string, password: string) => {
@@ -59,6 +88,7 @@ function useProvideAuth() {
icon: ,
}
setUser(user);
+ persistAuth(user);
return user;
})();
};
@@ -89,6 +119,7 @@ function useProvideAuth() {
throw new Error("Failed to log out.");
}
setUser(null);
+ persistAuth(null);
})();
};
diff --git a/server/app.ts b/server/app.ts
index 878a4ab..acab868 100644
--- a/server/app.ts
+++ b/server/app.ts
@@ -2,24 +2,19 @@ const bodyParser = require('body-parser');
import * as api from '../client/src/api';
import Knex from 'knex';
-import { CreateSongEndpointHandler } from './endpoints/CreateSong';
-import { CreateArtistEndpointHandler } from './endpoints/CreateArtist';
-import { QueryEndpointHandler } from './endpoints/Query';
-import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetails'
-import { SongDetailsEndpointHandler } from './endpoints/SongDetails';
-import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtist';
-import { ModifySongEndpointHandler } from './endpoints/ModifySong';
-import { CreateTagEndpointHandler } from './endpoints/CreateTag';
-import { ModifyTagEndpointHandler } from './endpoints/ModifyTag';
-import { TagDetailsEndpointHandler } from './endpoints/TagDetails';
-import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbum';
-import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbum';
-import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetails';
-import { DeleteTagEndpointHandler } from './endpoints/DeleteTag';
-import { MergeTagEndpointHandler } from './endpoints/MergeTag';
-import { RegisterUserEndpointHandler } from './endpoints/RegisterUser';
+import { Query } from './endpoints/Query';
+
+import { PostArtist, PutArtist, GetArtist } from './endpoints/Artist';
+import { PostAlbum, PutAlbum, GetAlbum } from './endpoints/Album';
+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 { RegisterUser } from './endpoints/RegisterUser';
+
import * as endpointTypes from './endpoints/types';
import { sha512 } from 'js-sha512';
+import { createIntegrations } from './integrations/integrations';
// For authentication
var passport = require('passport');
@@ -106,28 +101,41 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
}
}
+ // Set up integration proxies
+ app.use('/integrations', checkLogin(), createIntegrations(knex));
+
// Set up REST API endpoints
- app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(CreateSongEndpointHandler));
- app.post(apiBaseUrl + api.QueryEndpoint, checkLogin(), _invoke(QueryEndpointHandler));
- app.post(apiBaseUrl + api.CreateArtistEndpoint, checkLogin(), _invoke(CreateArtistEndpointHandler));
- app.put(apiBaseUrl + api.ModifyArtistEndpoint, checkLogin(), _invoke(ModifyArtistEndpointHandler));
- app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(ModifySongEndpointHandler));
- app.get(apiBaseUrl + api.SongDetailsEndpoint, checkLogin(), _invoke(SongDetailsEndpointHandler));
- app.get(apiBaseUrl + api.ArtistDetailsEndpoint, checkLogin(), _invoke(ArtistDetailsEndpointHandler));
- app.post(apiBaseUrl + api.CreateTagEndpoint, checkLogin(), _invoke(CreateTagEndpointHandler));
- app.put(apiBaseUrl + api.ModifyTagEndpoint, checkLogin(), _invoke(ModifyTagEndpointHandler));
- app.get(apiBaseUrl + api.TagDetailsEndpoint, checkLogin(), _invoke(TagDetailsEndpointHandler));
- app.post(apiBaseUrl + api.CreateAlbumEndpoint, checkLogin(), _invoke(CreateAlbumEndpointHandler));
- app.put(apiBaseUrl + api.ModifyAlbumEndpoint, checkLogin(), _invoke(ModifyAlbumEndpointHandler));
- app.get(apiBaseUrl + api.AlbumDetailsEndpoint, checkLogin(), _invoke(AlbumDetailsEndpointHandler));
- app.delete(apiBaseUrl + api.DeleteTagEndpoint, checkLogin(), _invoke(DeleteTagEndpointHandler));
- app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTagEndpointHandler));
- app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUserEndpointHandler));
-
- app.post(apiBaseUrl + '/login', passport.authenticate('local'), (req: any, res: any) => {
+ app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(PostSong));
+ app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(PutSong));
+ app.get(apiBaseUrl + api.SongDetailsEndpoint, checkLogin(), _invoke(GetSong));
+
+ app.post(apiBaseUrl + api.QueryEndpoint, checkLogin(), _invoke(Query));
+
+ app.post(apiBaseUrl + api.CreateArtistEndpoint, checkLogin(), _invoke(PostArtist));
+ app.put(apiBaseUrl + api.ModifyArtistEndpoint, checkLogin(), _invoke(PutArtist));
+ app.get(apiBaseUrl + api.ArtistDetailsEndpoint, checkLogin(), _invoke(GetArtist));
+
+ app.post(apiBaseUrl + api.CreateAlbumEndpoint, checkLogin(), _invoke(PostAlbum));
+ app.put(apiBaseUrl + api.ModifyAlbumEndpoint, checkLogin(), _invoke(PutAlbum));
+ app.get(apiBaseUrl + api.AlbumDetailsEndpoint, checkLogin(), _invoke(GetAlbum));
+
+ app.post(apiBaseUrl + api.CreateTagEndpoint, checkLogin(), _invoke(PostTag));
+ app.put(apiBaseUrl + api.ModifyTagEndpoint, checkLogin(), _invoke(PutTag));
+ app.get(apiBaseUrl + api.TagDetailsEndpoint, checkLogin(), _invoke(GetTag));
+ app.delete(apiBaseUrl + api.DeleteTagEndpoint, checkLogin(), _invoke(DeleteTag));
+ app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTag));
+
+ app.post(apiBaseUrl + api.CreateIntegrationEndpoint, checkLogin(), _invoke(PostIntegration));
+ app.put(apiBaseUrl + api.ModifyIntegrationEndpoint, checkLogin(), _invoke(PutIntegration));
+ app.get(apiBaseUrl + api.IntegrationDetailsEndpoint, checkLogin(), _invoke(GetIntegration));
+ app.delete(apiBaseUrl + api.DeleteIntegrationEndpoint, checkLogin(), _invoke(DeleteIntegration));
+ app.get(apiBaseUrl + api.ListIntegrationsEndpoint, checkLogin(), _invoke(ListIntegrations));
+
+ app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUser));
+ app.post(apiBaseUrl + api.LoginEndpoint, passport.authenticate('local'), (req: any, res: any) => {
res.status(200).send({ userId: req.user.id });
});
- app.post(apiBaseUrl + '/logout', function (req: any, res: any) {
+ app.post(apiBaseUrl + api.LogoutEndpoint, function (req: any, res: any) {
req.logout();
res.status(200).send();
});
diff --git a/server/endpoints/ModifyAlbum.ts b/server/endpoints/Album.ts
similarity index 51%
rename from server/endpoints/ModifyAlbum.ts
rename to server/endpoints/Album.ts
index 829f6c2..5442db7 100644
--- a/server/endpoints/ModifyAlbum.ts
+++ b/server/endpoints/Album.ts
@@ -1,11 +1,165 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
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)) {
const e: EndpointError = {
- internalMessage: 'Invalid ModifyAlbum request: ' + JSON.stringify(req.body),
+ internalMessage: 'Invalid PutAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
@@ -13,7 +167,7 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res:
const reqObject: api.ModifyAlbumRequest = req.body;
const { id: userId } = req.user;
- console.log("User ", userId, ": Modify Album ", reqObject);
+ console.log("User ", userId, ": Put Album ", reqObject);
await knex.transaction(async (trx) => {
try {
diff --git a/server/endpoints/AlbumDetails.ts b/server/endpoints/AlbumDetails.ts
deleted file mode 100644
index 635b210..0000000
--- a/server/endpoints/AlbumDetails.ts
+++ /dev/null
@@ -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);
-}
-}
\ No newline at end of file
diff --git a/server/endpoints/ModifyArtist.ts b/server/endpoints/Artist.ts
similarity index 50%
rename from server/endpoints/ModifyArtist.ts
rename to server/endpoints/Artist.ts
index 3d56ee6..0363423 100644
--- a/server/endpoints/ModifyArtist.ts
+++ b/server/endpoints/Artist.ts
@@ -1,11 +1,117 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
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)) {
const e: EndpointError = {
- internalMessage: 'Invalid ModifyArtist request: ' + JSON.stringify(req.body),
+ internalMessage: 'Invalid PutArtist request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
@@ -13,7 +119,7 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res
const reqObject: api.ModifyArtistRequest = req.body;
const { id: userId } = req.user;
- console.log("User ", userId, ": Modify Artist ", reqObject);
+ console.log("User ", userId, ": Put Artist ", reqObject);
await knex.transaction(async (trx) => {
try {
diff --git a/server/endpoints/ArtistDetails.ts b/server/endpoints/ArtistDetails.ts
deleted file mode 100644
index effe19f..0000000
--- a/server/endpoints/ArtistDetails.ts
+++ /dev/null
@@ -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)
- }
-}
\ No newline at end of file
diff --git a/server/endpoints/CreateAlbum.ts b/server/endpoints/CreateAlbum.ts
deleted file mode 100644
index 7172d5b..0000000
--- a/server/endpoints/CreateAlbum.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/CreateArtist.ts b/server/endpoints/CreateArtist.ts
deleted file mode 100644
index 0496c47..0000000
--- a/server/endpoints/CreateArtist.ts
+++ /dev/null
@@ -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();
- }
- });
-}
\ No newline at end of file
diff --git a/server/endpoints/CreateSong.ts b/server/endpoints/CreateSong.ts
deleted file mode 100644
index a3c80c1..0000000
--- a/server/endpoints/CreateSong.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/CreateTag.ts b/server/endpoints/CreateTag.ts
deleted file mode 100644
index 1587bcf..0000000
--- a/server/endpoints/CreateTag.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/DeleteTag.ts b/server/endpoints/DeleteTag.ts
deleted file mode 100644
index 3fd9ae3..0000000
--- a/server/endpoints/DeleteTag.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/Integration.ts b/server/endpoints/Integration.ts
new file mode 100644
index 0000000..6e32c46
--- /dev/null
+++ b/server/endpoints/Integration.ts
@@ -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();
+ }
+ })
+}
\ No newline at end of file
diff --git a/server/endpoints/MergeTag.ts b/server/endpoints/MergeTag.ts
deleted file mode 100644
index ed776c5..0000000
--- a/server/endpoints/MergeTag.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/ModifySong.ts b/server/endpoints/ModifySong.ts
deleted file mode 100644
index 488b72f..0000000
--- a/server/endpoints/ModifySong.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/ModifyTag.ts b/server/endpoints/ModifyTag.ts
deleted file mode 100644
index 6596958..0000000
--- a/server/endpoints/ModifyTag.ts
+++ /dev/null
@@ -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();
- }
- })
-}
\ No newline at end of file
diff --git a/server/endpoints/Query.ts b/server/endpoints/Query.ts
index 60111f3..f705516 100644
--- a/server/endpoints/Query.ts
+++ b/server/endpoints/Query.ts
@@ -258,7 +258,7 @@ async function getFullTag(knex: Knex, userId: number, tag: any): Promise {
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)) {
const e: EndpointError = {
internalMessage: 'Invalid Query request: ' + JSON.stringify(req.body),
diff --git a/server/endpoints/RegisterUser.ts b/server/endpoints/RegisterUser.ts
index 0782d90..1b4d824 100644
--- a/server/endpoints/RegisterUser.ts
+++ b/server/endpoints/RegisterUser.ts
@@ -4,7 +4,7 @@ import Knex from 'knex';
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)) {
const e: EndpointError = {
internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body),
diff --git a/server/endpoints/Song.ts b/server/endpoints/Song.ts
new file mode 100644
index 0000000..0583b5f
--- /dev/null
+++ b/server/endpoints/Song.ts
@@ -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 = 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 = 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 = 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();
+ }
+ })
+}
\ No newline at end of file
diff --git a/server/endpoints/SongDetails.ts b/server/endpoints/SongDetails.ts
deleted file mode 100644
index 24e6267..0000000
--- a/server/endpoints/SongDetails.ts
+++ /dev/null
@@ -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 = 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 = 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 = 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)
- }
-}
\ No newline at end of file
diff --git a/server/endpoints/Tag.ts b/server/endpoints/Tag.ts
new file mode 100644
index 0000000..673ec0d
--- /dev/null
+++ b/server/endpoints/Tag.ts
@@ -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();
+ }
+ })
+}
\ No newline at end of file
diff --git a/server/endpoints/TagDetails.ts b/server/endpoints/TagDetails.ts
deleted file mode 100644
index daacaae..0000000
--- a/server/endpoints/TagDetails.ts
+++ /dev/null
@@ -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)
- }
-}
\ No newline at end of file
diff --git a/server/integrations/integrations.ts b/server/integrations/integrations.ts
new file mode 100644
index 0000000..4b109d6
--- /dev/null
+++ b/server/integrations/integrations.ts
@@ -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': '' },
+ // })
+ // )
+}
\ No newline at end of file
diff --git a/server/migrations/20201113155620_add_integrations.ts b/server/migrations/20201113155620_add_integrations.ts
new file mode 100644
index 0000000..08dbc43
--- /dev/null
+++ b/server/migrations/20201113155620_add_integrations.ts
@@ -0,0 +1,24 @@
+import * as Knex from "knex";
+
+
+export async function up(knex: Knex): Promise {
+ // 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 {
+ await knex.schema.dropTable('integrations');
+}
+
diff --git a/server/package-lock.json b/server/package-lock.json
index 8b45277..7d49ca9 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -24,6 +24,14 @@
"xml2js": "^0.4.19"
},
"dependencies": {
+ "axios": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "requires": {
+ "follow-redirects": "1.5.10"
+ }
+ },
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -69,6 +77,14 @@
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz",
"integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw=="
},
+ "@types/http-proxy": {
+ "version": "1.17.4",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
+ "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/node": {
"version": "14.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.2.tgz",
@@ -304,11 +320,18 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA=="
},
"axios": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
- "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
+ "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
"requires": {
- "follow-redirects": "1.5.10"
+ "follow-redirects": "^1.10.0"
+ },
+ "dependencies": {
+ "follow-redirects": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
+ "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
+ }
}
},
"balanced-match": {
@@ -1034,6 +1057,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
+ "eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+ },
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -1654,6 +1682,68 @@
"toidentifier": "1.0.0"
}
},
+ "http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "requires": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "http-proxy-middleware": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz",
+ "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==",
+ "requires": {
+ "@types/http-proxy": "^1.17.4",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "lodash": "^4.17.20",
+ "micromatch": "^4.0.2"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ }
+ }
+ },
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -2474,6 +2564,11 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
+ "node-fetch": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
+ },
"node-gyp": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
@@ -3052,6 +3147,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
+ "querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+ },
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -3206,6 +3306,11 @@
}
}
},
+ "requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
+ },
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
diff --git a/server/package.json b/server/package.json
index 762d412..a05b69b 100644
--- a/server/package.json
+++ b/server/package.json
@@ -8,11 +8,13 @@
"test": "ts-node node_modules/jasmine/bin/jasmine --config=test/jasmine.json"
},
"dependencies": {
+ "axios": "^0.21.0",
"body-parser": "^1.18.3",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"express": "^4.16.4",
"express-session": "^1.17.1",
+ "http-proxy-middleware": "^1.0.6",
"jasmine": "^3.5.0",
"js-sha512": "^0.8.0",
"knex": "^0.21.5",
@@ -20,11 +22,13 @@
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
+ "node-fetch": "^2.6.1",
"nodemon": "^2.0.4",
"oracledb": "^5.0.0",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"pg": "^8.3.3",
+ "querystring": "^0.2.0",
"sqlite3": "^5.0.0",
"ts-node": "^8.10.2",
"typescript": "~3.7.2"
diff --git a/server/test/integration/flows/IntegrationFlow.js b/server/test/integration/flows/IntegrationFlow.js
new file mode 100644
index 0000000..cd693c0
--- /dev/null
+++ b/server/test/integration/flows/IntegrationFlow.js
@@ -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();
+ }
+ });
+});
\ No newline at end of file
diff --git a/server/test/integration/flows/helpers.js b/server/test/integration/flows/helpers.js
index bbf427e..12677cd 100644
--- a/server/test/integration/flows/helpers.js
+++ b/server/test/integration/flows/helpers.js
@@ -1,5 +1,6 @@
import { expect } from "chai";
import { sha512 } from "js-sha512";
+import { IntegrationType } from "../../../../client/src/api";
export async function initTestDB() {
// Allow different database configs - but fall back to SQLite in memory if necessary.
@@ -28,6 +29,7 @@ export async function createSong(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
});
}
@@ -42,6 +44,7 @@ export async function modifySong(
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
+ return res;
});
}
@@ -56,6 +59,7 @@ export async function checkSong(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
})
}
@@ -71,6 +75,7 @@ export async function createArtist(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
});
}
@@ -85,6 +90,7 @@ export async function modifyArtist(
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
+ return res;
});
}
@@ -99,6 +105,7 @@ export async function checkArtist(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
})
}
@@ -114,6 +121,7 @@ export async function createTag(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
});
}
@@ -128,6 +136,7 @@ export async function modifyTag(
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
+ return res;
});
}
@@ -142,6 +151,7 @@ export async function checkTag(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
})
}
@@ -157,6 +167,7 @@ export async function createAlbum(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
});
}
@@ -171,6 +182,7 @@ export async function modifyAlbum(
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
+ return res;
});
}
@@ -185,6 +197,7 @@ export async function checkAlbum(
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
})
}
@@ -203,6 +216,7 @@ export async function createUser(
});
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
}
export async function login(
@@ -217,6 +231,7 @@ export async function login(
.send({});
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
}
export async function logout(
@@ -229,4 +244,78 @@ export async function logout(
.send({});
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
+}
+
+export async function createIntegration(
+ req,
+ props = { name: "Integration", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} },
+ expectStatus = undefined,
+ expectResponse = undefined
+) {
+ await req
+ .post('/integration')
+ .send(props)
+ .then((res) => {
+ expectStatus && expect(res).to.have.status(expectStatus);
+ expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
+ });
+}
+
+export async function modifyIntegration(
+ req,
+ id = 1,
+ props = { name: "NewIntegration", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} },
+ expectStatus = undefined,
+) {
+ await req
+ .put('/integration/' + id)
+ .send(props)
+ .then((res) => {
+ expectStatus && expect(res).to.have.status(expectStatus);
+ return res;
+ });
+}
+
+export async function checkIntegration(
+ req,
+ id,
+ expectStatus = undefined,
+ expectResponse = undefined,
+) {
+ await req
+ .get('/integration/' + id)
+ .then((res) => {
+ expectStatus && expect(res).to.have.status(expectStatus);
+ expectResponse && expect(res.body).to.deep.equal(expectResponse);
+ return res;
+ })
+}
+
+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(
+ req,
+ id,
+ expectStatus = undefined,
+) {
+ await req
+ .delete('/integration/' + id)
+ .then((res) => {
+ expectStatus && expect(res).to.have.status(expectStatus);
+ return res;
+ })
}
\ No newline at end of file