parent
d3a901e826
commit
5a073fb3b8
35 changed files with 1662 additions and 1199 deletions
After Width: | Height: | Size: 907 B |
@ -0,0 +1,287 @@ |
||||
import React, { useState, useEffect, useCallback } from 'react'; |
||||
import { useAuth } from '../../../lib/useAuth'; |
||||
import { Box, CircularProgress, IconButton, Typography, FormControl, Select, MenuItem, TextField, Menu } from '@material-ui/core'; |
||||
import { IntegrationDetails } from '../../../api'; |
||||
import { getIntegrations, createIntegration, modifyIntegration, deleteIntegration } from '../../../lib/backend/integrations'; |
||||
import AddIcon from '@material-ui/icons/Add'; |
||||
import EditIcon from '@material-ui/icons/Edit'; |
||||
import CheckIcon from '@material-ui/icons/Check'; |
||||
import DeleteIcon from '@material-ui/icons/Delete'; |
||||
import * as serverApi from '../../../api'; |
||||
import StoreLinkIcon, { ExternalStore } from '../../common/StoreLinkIcon'; |
||||
import { v4 as genUuid } from 'uuid'; |
||||
let _ = require('lodash') |
||||
|
||||
interface EditIntegrationProps { |
||||
integration: serverApi.IntegrationDetailsResponse, |
||||
original: serverApi.IntegrationDetailsResponse, |
||||
editing: boolean, |
||||
submitting: boolean, |
||||
onChange: (p: serverApi.IntegrationDetailsResponse, editing: boolean) => void, |
||||
onSubmit: () => void, |
||||
onDelete: () => void, |
||||
} |
||||
|
||||
function EditIntegration(props: EditIntegrationProps) { |
||||
let IntegrationHeaders: Record<any, any> = { |
||||
[serverApi.IntegrationType.spotify]: <Box display="flex" alignItems="center"> |
||||
<StoreLinkIcon |
||||
style={{ height: '40px', width: '40px' }} |
||||
whichStore={ExternalStore.Spotify} |
||||
/> |
||||
<Typography>Spotify</Typography> |
||||
</Box> |
||||
} |
||||
|
||||
return <Box display="flex" flexDirection="column" border={1}> |
||||
{IntegrationHeaders[props.integration.type]} |
||||
<Box display="flex" alignItems="center"> |
||||
<Typography>Name:</Typography> |
||||
{props.editing ? |
||||
<TextField |
||||
variant="outlined" |
||||
value={props.integration.name || ""} |
||||
label="Name" |
||||
onChange={(e: any) => props.onChange({ |
||||
...props.integration, |
||||
name: e.target.value, |
||||
}, props.editing)} |
||||
/> : |
||||
<Typography>{props.integration.name}</Typography>} |
||||
</Box> |
||||
{props.integration.type === serverApi.IntegrationType.spotify && <> |
||||
<Box display="flex" alignItems="center"> |
||||
<Typography>Client id:</Typography> |
||||
{props.editing ? |
||||
<TextField |
||||
variant="outlined" |
||||
value={props.integration.details.clientId || ""} |
||||
label="Client Id:" |
||||
onChange={(e: any) => props.onChange({ |
||||
...props.integration, |
||||
details: { |
||||
...props.integration.details, |
||||
clientId: e.target.value, |
||||
} |
||||
}, props.editing)} |
||||
/> : |
||||
<Typography>{props.integration.details.clientId}</Typography>} |
||||
</Box> |
||||
<Box display="flex" alignItems="center"> |
||||
<Typography>Client secret:</Typography> |
||||
{props.editing ? |
||||
<TextField |
||||
variant="outlined" |
||||
value={props.integration.details.clientSecret || ""} |
||||
label="Client secret:" |
||||
onChange={(e: any) => props.onChange({ |
||||
...props.integration, |
||||
details: { |
||||
...props.integration.details, |
||||
clientSecret: e.target.value, |
||||
} |
||||
}, props.editing)} |
||||
/> : |
||||
<Typography>{props.integration.details.clientSecret}</Typography>} |
||||
</Box> |
||||
{!props.editing && !props.submitting && <IconButton |
||||
onClick={() => { props.onChange(props.integration, true); }} |
||||
><EditIcon /></IconButton>} |
||||
{props.editing && !props.submitting && <IconButton |
||||
onClick={() => { props.onSubmit(); }} |
||||
><CheckIcon /></IconButton>} |
||||
{!props.submitting && <IconButton |
||||
onClick={() => { props.onDelete(); }} |
||||
><DeleteIcon /></IconButton>} |
||||
{props.submitting && <CircularProgress />} |
||||
</>} |
||||
</Box > |
||||
} |
||||
|
||||
function AddIntegrationMenu(props: { |
||||
position: null | number[], |
||||
open: boolean, |
||||
onClose: () => void, |
||||
onAdd: (type: serverApi.IntegrationType) => void, |
||||
}) { |
||||
const pos = props.open && props.position ? |
||||
{ left: props.position[0], top: props.position[1] } |
||||
: { left: 0, top: 0 } |
||||
|
||||
return <Menu |
||||
open={props.open} |
||||
anchorReference="anchorPosition" |
||||
anchorPosition={pos} |
||||
keepMounted |
||||
onClose={props.onClose} |
||||
> |
||||
<MenuItem |
||||
onClick={() => { |
||||
props.onAdd(serverApi.IntegrationType.spotify); |
||||
props.onClose(); |
||||
}} |
||||
>Spotify</MenuItem> |
||||
</Menu> |
||||
} |
||||
|
||||
export default function IntegrationSettingsEditor(props: {}) { |
||||
interface EditorState { |
||||
id: string, //uniquely identifies this editor in the window.
|
||||
upstreamId: number | null, //back-end ID for this integration if any.
|
||||
integration: serverApi.IntegrationDetailsResponse, |
||||
original: serverApi.IntegrationDetailsResponse, |
||||
editing: boolean, |
||||
submitting: boolean, |
||||
} |
||||
let [editors, setEditors] = useState<EditorState[] | null>(null); |
||||
const [addMenuPos, setAddMenuPos] = React.useState<null | number[]>(null); |
||||
|
||||
const onOpenAddMenu = (e: any) => { |
||||
setAddMenuPos([e.clientX, e.clientY]) |
||||
}; |
||||
const onCloseAddMenu = () => { |
||||
setAddMenuPos(null); |
||||
}; |
||||
|
||||
const submitEditor = (state: EditorState) => { |
||||
let integration = state.integration; |
||||
|
||||
if (state.upstreamId === null) { |
||||
createIntegration(integration).then((response: any) => { |
||||
if (!response.id) { |
||||
throw new Error('failed to submit integration.') |
||||
} |
||||
let cpy = _.cloneDeep(editors); |
||||
cpy.forEach((s: any) => { |
||||
if (s.id === state.id) { |
||||
s.submitting = false; |
||||
s.editing = false; |
||||
s.upstreamId = response.id; |
||||
} |
||||
}) |
||||
setEditors(cpy); |
||||
}) |
||||
} else { |
||||
modifyIntegration(state.upstreamId, integration).then(() => { |
||||
let cpy = _.cloneDeep(editors); |
||||
cpy.forEach((s: any) => { |
||||
if (s.id === state.id) { |
||||
s.submitting = false; |
||||
s.editing = false; |
||||
} |
||||
}) |
||||
setEditors(cpy); |
||||
}) |
||||
} |
||||
} |
||||
|
||||
const deleteEditor = (state: EditorState) => { |
||||
if(!state.upstreamId) { |
||||
throw new Error('Cannot delete integration: has no upstream') |
||||
} |
||||
deleteIntegration(state.upstreamId).then((response: any) => { |
||||
let cpy = _.cloneDeep(editors).filter( |
||||
(e: any) => e.id !== state.id |
||||
); |
||||
setEditors(cpy); |
||||
}) |
||||
} |
||||
|
||||
useEffect(() => { |
||||
getIntegrations() |
||||
.then((integrations: serverApi.ListIntegrationsResponse) => { |
||||
setEditors(integrations.map((i: any, idx: any) => { |
||||
return { |
||||
integration: { ...i }, |
||||
original: { ...i }, |
||||
id: genUuid(), |
||||
editing: false, |
||||
submitting: false, |
||||
upstreamId: i.id, |
||||
} |
||||
})); |
||||
}); |
||||
}, []); |
||||
|
||||
// FIXME: add button should show a drop-down to choose a fixed integration type.
|
||||
// Otherwise we need dynamic switching of the type's fields.
|
||||
return <> |
||||
<Box> |
||||
{editors === null && <CircularProgress />} |
||||
{editors && <> |
||||
{editors.map((state: EditorState) => <EditIntegration |
||||
integration={state.integration} |
||||
original={state.original} |
||||
editing={state.editing} |
||||
submitting={state.submitting} |
||||
onChange={(p: serverApi.IntegrationDetailsResponse, editing: boolean) => { |
||||
if (!editors) { |
||||
throw new Error('cannot change editors before loading integrations.') |
||||
} |
||||
let cpy: EditorState[] = _.cloneDeep(editors); |
||||
cpy.forEach((s: any) => { |
||||
if (s.id === state.id) { |
||||
s.integration = p; |
||||
s.editing = editing; |
||||
} |
||||
}) |
||||
setEditors(cpy); |
||||
}} |
||||
onSubmit={() => { |
||||
if (!editors) { |
||||
throw new Error('cannot submit editors before loading integrations.') |
||||
} |
||||
let cpy: EditorState[] = _.cloneDeep(editors); |
||||
cpy.forEach((s: any) => { |
||||
if (s.id === state.id) { |
||||
s.submitting = true; |
||||
} |
||||
}) |
||||
setEditors(cpy); |
||||
submitEditor(state); |
||||
}} |
||||
onDelete={() => { |
||||
if (!editors) { |
||||
throw new Error('cannot submit editors before loading integrations.') |
||||
} |
||||
let cpy: EditorState[] = _.cloneDeep(editors); |
||||
cpy.forEach((s: any) => { |
||||
if (s.id === state.id) { |
||||
s.submitting = true; |
||||
} |
||||
}) |
||||
setEditors(cpy); |
||||
deleteEditor(state); |
||||
}} |
||||
/>)} |
||||
<IconButton onClick={onOpenAddMenu}> |
||||
<AddIcon /> |
||||
</IconButton> |
||||
</>} |
||||
</Box> |
||||
<AddIntegrationMenu |
||||
position={addMenuPos} |
||||
open={addMenuPos !== null} |
||||
onClose={onCloseAddMenu} |
||||
onAdd={(type: serverApi.IntegrationType) => { |
||||
let cpy = _.cloneDeep(editors); |
||||
cpy.push({ |
||||
integration: { |
||||
type: serverApi.IntegrationType.spotify, |
||||
details: { |
||||
clientId: '', |
||||
clientSecret: '', |
||||
}, |
||||
name: '', |
||||
}, |
||||
original: null, |
||||
id: genUuid(), |
||||
editing: true, |
||||
submitting: false, |
||||
upstreamId: null, |
||||
}) |
||||
setEditors(cpy); |
||||
}} |
||||
/> |
||||
</>; |
||||
} |
@ -0,0 +1,59 @@ |
||||
import React, { useReducer } from 'react'; |
||||
import { WindowState } from "../Windows"; |
||||
import { Box, Paper, Typography, TextField, Button } from "@material-ui/core"; |
||||
import { useHistory } from 'react-router'; |
||||
import { useAuth, Auth } from '../../../lib/useAuth'; |
||||
import Alert from '@material-ui/lab/Alert'; |
||||
import { Link } from 'react-router-dom'; |
||||
import IntegrationSettingsEditor from './IntegrationSettingsEditor'; |
||||
|
||||
export enum SettingsTab { |
||||
Integrations = 0, |
||||
} |
||||
|
||||
export interface SettingsWindowState extends WindowState { |
||||
activeTab: SettingsTab, |
||||
} |
||||
export enum SettingsWindowStateActions { |
||||
SetActiveTab = "SetActiveTab", |
||||
} |
||||
export function SettingsWindowReducer(state: SettingsWindowState, action: any) { |
||||
switch (action.type) { |
||||
case SettingsWindowStateActions.SetActiveTab: |
||||
return { ...state, activeTab: action.value } |
||||
default: |
||||
throw new Error("Unimplemented SettingsWindow state update.") |
||||
} |
||||
} |
||||
|
||||
export default function SettingsWindow(props: {}) { |
||||
const [state, dispatch] = useReducer(SettingsWindowReducer, { |
||||
activeTab: SettingsTab.Integrations, |
||||
}); |
||||
|
||||
return <SettingsWindowControlled state={state} dispatch={dispatch} /> |
||||
} |
||||
|
||||
export function SettingsWindowControlled(props: { |
||||
state: SettingsWindowState, |
||||
dispatch: (action: any) => void, |
||||
}) { |
||||
let history: any = useHistory(); |
||||
let auth: Auth = useAuth(); |
||||
|
||||
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
||||
<Box |
||||
m={1} |
||||
mt={4} |
||||
width="60%" |
||||
> |
||||
<Paper> |
||||
<Box p={3}> |
||||
<Box mb={3}><Typography variant="h5">User Settings</Typography></Box> |
||||
<Typography variant="h6">Integrations</Typography> |
||||
<IntegrationSettingsEditor/> |
||||
</Box> |
||||
</Paper> |
||||
</Box> |
||||
</Box> |
||||
} |
@ -0,0 +1,63 @@ |
||||
import * as serverApi from '../../api'; |
||||
|
||||
export async function createIntegration(details: serverApi.CreateIntegrationRequest) { |
||||
const requestOpts = { |
||||
method: 'POST', |
||||
headers: { 'Content-Type': 'application/json' }, |
||||
body: JSON.stringify(details), |
||||
}; |
||||
|
||||
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.CreateIntegrationEndpoint, requestOpts) |
||||
if (!response.ok) { |
||||
throw new Error("Response to integration creation not OK: " + JSON.stringify(response)); |
||||
} |
||||
return await response.json(); |
||||
} |
||||
|
||||
export async function modifyIntegration(id: number, details: serverApi.ModifyIntegrationRequest) { |
||||
const requestOpts = { |
||||
method: 'PUT', |
||||
headers: { 'Content-Type': 'application/json' }, |
||||
body: JSON.stringify(details), |
||||
}; |
||||
|
||||
const response = await fetch( |
||||
(process.env.REACT_APP_BACKEND || "") + serverApi.ModifyIntegrationEndpoint.replace(':id', id.toString()), |
||||
requestOpts |
||||
); |
||||
if (!response.ok) { |
||||
throw new Error("Response to integration modification not OK: " + JSON.stringify(response)); |
||||
} |
||||
} |
||||
|
||||
export async function deleteIntegration(id: number) { |
||||
const requestOpts = { |
||||
method: 'DELETE', |
||||
}; |
||||
|
||||
const response = await fetch( |
||||
(process.env.REACT_APP_BACKEND || "") + serverApi.DeleteIntegrationEndpoint.replace(':id', id.toString()), |
||||
requestOpts |
||||
); |
||||
if (!response.ok) { |
||||
throw new Error("Response to integration deletion not OK: " + JSON.stringify(response)); |
||||
} |
||||
} |
||||
|
||||
export async function getIntegrations() { |
||||
const requestOpts = { |
||||
method: 'GET', |
||||
}; |
||||
|
||||
const response = await fetch( |
||||
(process.env.REACT_APP_BACKEND || "") + serverApi.ListIntegrationsEndpoint, |
||||
requestOpts |
||||
); |
||||
|
||||
if (!response.ok) { |
||||
throw new Error("Response to integration list not OK: " + JSON.stringify(response)); |
||||
} |
||||
|
||||
let json = await response.json(); |
||||
return json; |
||||
} |
@ -1,64 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
import asJson from '../lib/asJson'; |
||||
|
||||
export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkAlbumDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid AlbumDetails request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
// Start transfers for songs, tags and artists.
|
||||
// Also request the album itself.
|
||||
const tagIdsPromise = knex.select('tagId') |
||||
.from('albums_tags') |
||||
.where({ 'albumId': req.params.id }) |
||||
.then((tags: any) => { |
||||
return tags.map((tag: any) => tag['tagId']) |
||||
}); |
||||
const songIdsPromise = knex.select('songId') |
||||
.from('songs_albums') |
||||
.where({ 'albumId': req.params.id }) |
||||
.then((songs: any) => { |
||||
return songs.map((song: any) => song['songId']) |
||||
}); |
||||
const artistIdsPromise = knex.select('artistId') |
||||
.from('artists_albums') |
||||
.where({ 'albumId': req.params.id }) |
||||
.then((artists: any) => { |
||||
return artists.map((artist: any) => artist['artistId']) |
||||
}); |
||||
const albumPromise = knex.select('name', 'storeLinks') |
||||
.from('albums') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((albums: any) => albums[0]); |
||||
|
||||
// Wait for the requests to finish.
|
||||
const [album, tags, songs, artists] = |
||||
await Promise.all([albumPromise, tagIdsPromise, songIdsPromise, artistIdsPromise]); |
||||
|
||||
// Respond to the request.
|
||||
if (album) { |
||||
const response: api.AlbumDetailsResponse = { |
||||
name: album['name'], |
||||
artistIds: artists, |
||||
tagIds: tags, |
||||
songIds: songs, |
||||
storeLinks: asJson(album['storeLinks']), |
||||
}; |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
} |
||||
} |
@ -1,41 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
import asJson from '../lib/asJson'; |
||||
|
||||
export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkArtistDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid ArtistDetails request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const tagIds = Array.from(new Set((await knex.select('tagId') |
||||
.from('artists_tags') |
||||
.where({ 'artistId': req.params.id }) |
||||
).map((tag: any) => tag['tagId']))); |
||||
|
||||
const results = await knex.select(['id', 'name', 'storeLinks']) |
||||
.from('artists') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }); |
||||
|
||||
if (results[0]) { |
||||
const response: api.ArtistDetailsResponse = { |
||||
name: results[0].name, |
||||
tagIds: tagIds, |
||||
storeLinks: asJson(results[0].storeLinks), |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
@ -1,96 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateAlbumRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid CreateAlbum request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateAlbumRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Create Album ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving artists.
|
||||
const artistIdsPromise = reqObject.artistIds ? |
||||
trx.select('id') |
||||
.from('artists') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.artistIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving tags.
|
||||
const tagIdsPromise = reqObject.tagIds ? |
||||
trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.tagIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [artists, tags] = await Promise.all([artistIdsPromise, tagIdsPromise]);; |
||||
|
||||
// Check that we found all artists and tags we need.
|
||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateAlbum request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Create the album.
|
||||
const albumId = (await trx('albums') |
||||
.insert({ |
||||
name: reqObject.name, |
||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
||||
user: userId, |
||||
}) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Link the artists via the linking table.
|
||||
if (artists && artists.length) { |
||||
await trx('artists_albums').insert( |
||||
artists.map((artistId: number) => { |
||||
return { |
||||
artistId: artistId, |
||||
albumId: albumId, |
||||
} |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Link the tags via the linking table.
|
||||
if (tags && tags.length) { |
||||
await trx('albums_tags').insert( |
||||
tags.map((tagId: number) => { |
||||
return { |
||||
albumId: albumId, |
||||
tagId: tagId, |
||||
} |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateSongResponse = { |
||||
id: albumId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,70 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateArtistRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid CreateArtist request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateArtistRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Create artist ", reqObject) |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Retrieve tag instances to link the artist to.
|
||||
const tags: number[] = reqObject.tagIds ? |
||||
Array.from(new Set( |
||||
(await trx.select('id').from('tags') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.tagIds)) |
||||
.map((tag: any) => tag['id']) |
||||
)) |
||||
: []; |
||||
|
||||
if (reqObject.tagIds && tags && tags.length !== reqObject.tagIds.length) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Create the artist.
|
||||
const artistId = (await trx('artists') |
||||
.insert({ |
||||
name: reqObject.name, |
||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
||||
user: userId, |
||||
}) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Link the tags via the linking table.
|
||||
if (tags && tags.length) { |
||||
await trx('artists_tags').insert( |
||||
tags.map((tagId: number) => { |
||||
return { |
||||
artistId: artistId, |
||||
tagId: tagId, |
||||
} |
||||
}) |
||||
) |
||||
} |
||||
|
||||
const responseObject: api.CreateSongResponse = { |
||||
id: artistId |
||||
}; |
||||
await res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}); |
||||
} |
@ -1,43 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const CreateIntegrationEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid CreateIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Create Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Create the new integration.
|
||||
var integration: any = { |
||||
name: reqObject.name, |
||||
user: userId, |
||||
type: reqObject.type, |
||||
details: JSON.stringify(reqObject.details), |
||||
} |
||||
const integrationId = (await trx('integrations') |
||||
.insert(integration) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateIntegrationResponse = { |
||||
id: integrationId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,118 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateSongRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid CreateSong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateSongRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Create Song ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving artists.
|
||||
const artistIdsPromise = reqObject.artistIds ? |
||||
trx.select('id') |
||||
.from('artists') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.artistIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving tags.
|
||||
const tagIdsPromise = reqObject.tagIds ? |
||||
trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.tagIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving albums.
|
||||
const albumIdsPromise = reqObject.albumIds ? |
||||
trx.select('id') |
||||
.from('albums') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.albumIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [artists, tags, albums] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdsPromise]);; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
||||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Create the song.
|
||||
const songId = (await trx('songs') |
||||
.insert({ |
||||
title: reqObject.title, |
||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
||||
user: userId, |
||||
}) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Link the artists via the linking table.
|
||||
if (artists && artists.length) { |
||||
await Promise.all( |
||||
artists.map((artistId: number) => { |
||||
return trx('songs_artists').insert({ |
||||
artistId: artistId, |
||||
songId: songId, |
||||
}) |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Link the tags via the linking table.
|
||||
if (tags && tags.length) { |
||||
await Promise.all( |
||||
tags.map((tagId: number) => { |
||||
return trx('songs_tags').insert({ |
||||
songId: songId, |
||||
tagId: tagId, |
||||
}) |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Link the albums via the linking table.
|
||||
if (albums && albums.length) { |
||||
await Promise.all( |
||||
albums.map((albumId: number) => { |
||||
return trx('songs_albums').insert({ |
||||
songId: songId, |
||||
albumId: albumId, |
||||
}) |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateSongResponse = { |
||||
id: songId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,62 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid CreateTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Create Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// If applicable, retrieve the parent tag.
|
||||
const maybeParent: number | undefined = |
||||
reqObject.parentId ? |
||||
(await trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': reqObject.parentId }))[0]['id'] : |
||||
undefined; |
||||
|
||||
// Check if the parent was found, if applicable.
|
||||
if (reqObject.parentId && maybeParent !== reqObject.parentId) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Could not find parent tag for CreateTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Create the new tag.
|
||||
var tag: any = { |
||||
name: reqObject.name, |
||||
user: userId, |
||||
}; |
||||
if (maybeParent) { |
||||
tag['parentId'] = maybeParent; |
||||
} |
||||
const tagId = (await trx('tags') |
||||
.insert(tag) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateTagResponse = { |
||||
id: tagId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,49 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const DeleteIntegrationEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkDeleteIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid DeleteIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Delete Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the integration itself.
|
||||
const integrationId = await trx.select('id') |
||||
.from('integrations') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!integrationId) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Integration does not exist for DeleteIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 404 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Delete the integration.
|
||||
await trx('integrations') |
||||
.where({ 'user': userId, 'id': integrationId }) |
||||
.del(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,78 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
async function getChildrenRecursive(id: number, userId: number, trx: any) { |
||||
const directChildren = (await trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'parentId': id })).map((r: any) => r.id); |
||||
|
||||
const indirectChildrenPromises = directChildren.map( |
||||
(child: number) => getChildrenRecursive(child, userId, trx) |
||||
); |
||||
const indirectChildrenNested = await Promise.all(indirectChildrenPromises); |
||||
const indirectChildren = indirectChildrenNested.flat(); |
||||
|
||||
return [ |
||||
...directChildren, |
||||
...indirectChildren, |
||||
] |
||||
} |
||||
|
||||
export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkDeleteTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid DeleteTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Delete Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving any child tags.
|
||||
const childTagsPromise =
|
||||
getChildrenRecursive(req.params.id, userId, trx); |
||||
|
||||
// Start retrieving the tag itself.
|
||||
const tagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [tag, children] = await Promise.all([tagPromise, childTagsPromise]); |
||||
|
||||
// Merge all IDs.
|
||||
const toDelete = [ tag, ...children ]; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!tag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Tag or parent does not exist for DeleteTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Delete the tag and its children.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', toDelete) |
||||
.del(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -0,0 +1,201 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
import asJson from '../lib/asJson'; |
||||
|
||||
export const PostIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Post Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Create the new integration.
|
||||
var integration: any = { |
||||
name: reqObject.name, |
||||
user: userId, |
||||
type: reqObject.type, |
||||
details: JSON.stringify(reqObject.details), |
||||
} |
||||
const integrationId = (await trx('integrations') |
||||
.insert(integration) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateIntegrationResponse = { |
||||
id: integrationId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export const GetIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkIntegrationDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid GetIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const integration = (await knex.select(['id', 'name', 'type', 'details']) |
||||
.from('integrations') |
||||
.where({ 'user': userId, 'id': req.params.id }))[0]; |
||||
|
||||
if (integration) { |
||||
const response: api.IntegrationDetailsResponse = { |
||||
name: integration.name, |
||||
type: integration.type, |
||||
details: asJson(integration.details), |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
||||
|
||||
export const ListIntegrations: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkIntegrationDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid ListIntegrations request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const integrations: api.ListIntegrationsResponse = ( |
||||
await knex.select(['id', 'name', 'type', 'details']) |
||||
.from('integrations') |
||||
.where({ user: userId }) |
||||
).map((object: any) => { |
||||
return { |
||||
id: object.id, |
||||
name: object.name, |
||||
type: object.type, |
||||
details: asJson(object.details), |
||||
} |
||||
}) |
||||
|
||||
await res.send(integrations); |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
||||
|
||||
export const DeleteIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkDeleteIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid DeleteIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Delete Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the integration itself.
|
||||
const integrationId = await trx.select('id') |
||||
.from('integrations') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!integrationId) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Integration does not exist for DeleteIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 404 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Delete the integration.
|
||||
await trx('integrations') |
||||
.where({ 'user': userId, 'id': integrationId }) |
||||
.del(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the integration.
|
||||
const integrationId = await trx.select('id') |
||||
.from('integrations') |
||||
.where({ 'user': userId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!integrationId) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Integration does not exist for ModifyIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 404 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the integration.
|
||||
var update: any = {}; |
||||
if ("name" in reqObject) { update["name"] = reqObject.name; } |
||||
if ("details" in reqObject) { update["details"] = JSON.stringify(reqObject.details); } |
||||
if ("type" in reqObject) { update["type"] = reqObject.type; } |
||||
await trx('integrations') |
||||
.where({ 'user': userId, 'id': req.params.id }) |
||||
.update(update) |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,34 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const IntegrationDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkIntegrationDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid IntegrationDetails request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const integration = (await knex.select(['id', 'name', 'type', 'details']) |
||||
.from('integrations') |
||||
.where({ 'user': userId, 'id': req.params.id }))[0]; |
||||
|
||||
if (integration) { |
||||
const response: api.IntegrationDetailsResponse = { |
||||
name: integration.name, |
||||
type: integration.type, |
||||
details: JSON.parse(integration.details), |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
@ -1,78 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkMergeTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Merge Tag ", reqObject); |
||||
const fromId = req.params.id; |
||||
const toId = req.params.toId; |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the "from" tag.
|
||||
const fromTagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: fromId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Start retrieving the "to" tag.
|
||||
const toTagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: toId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!fromTag || !toTag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Assign new tag ID to any objects referencing the to-be-merged tag.
|
||||
const cPromise = trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'parentId': fromId }) |
||||
.update({ 'parentId': toId }); |
||||
const sPromise = trx('songs_tags') |
||||
.where({ 'tagId': fromId }) |
||||
.update({ 'tagId': toId }); |
||||
const arPromise = trx('artists_tags') |
||||
.where({ 'tagId': fromId }) |
||||
.update({ 'tagId': toId }); |
||||
const alPromise = trx('albums_tags') |
||||
.where({ 'tagId': fromId }) |
||||
.update({ 'tagId': toId }); |
||||
await Promise.all([sPromise, arPromise, alPromise, cPromise]); |
||||
|
||||
// Delete the original tag.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': fromId }) |
||||
.del(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,52 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const ModifyIntegrationEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyIntegrationRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid ModifyIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyIntegrationRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Modify Integration ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the integration.
|
||||
const integrationId = await trx.select('id') |
||||
.from('integrations') |
||||
.where({ 'user': userId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!integrationId) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Integration does not exist for ModifyIntegration request: ' + JSON.stringify(req.body), |
||||
httpStatus: 404 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the integration.
|
||||
var update: any = {}; |
||||
if ("name" in reqObject) { update["name"] = reqObject.name; } |
||||
if ("details" in reqObject) { update["details"] = JSON.stringify(reqObject.details); } |
||||
if ("type" in reqObject) { update["type"] = reqObject.type; } |
||||
await trx('integrations') |
||||
.where({ 'user': userId, 'id': req.params.id }) |
||||
.update(update) |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,191 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifySongRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid ModifySong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifySongRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Modify Song ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Retrieve the song to be modified itself.
|
||||
const songPromise = trx.select('id') |
||||
.from('songs') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Start retrieving artists.
|
||||
const artistIdsPromise = reqObject.artistIds ? |
||||
trx.select('artistId') |
||||
.from('songs_artists') |
||||
.whereIn('id', reqObject.artistIds) |
||||
.then((as: any) => as.map((a: any) => a['artistId'])) : |
||||
(async () => { return undefined })(); |
||||
|
||||
// Start retrieving tags.
|
||||
const tagIdsPromise = reqObject.tagIds ? |
||||
trx.select('id') |
||||
.from('songs_tags') |
||||
.whereIn('id', reqObject.tagIds) |
||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||
(async () => { return undefined })(); |
||||
|
||||
// Start retrieving albums.
|
||||
const albumIdsPromise = reqObject.albumIds ? |
||||
trx.select('id') |
||||
.from('songs_albums') |
||||
.whereIn('id', reqObject.albumIds) |
||||
.then((as: any) => as.map((a: any) => a['albumId'])) : |
||||
(async () => { return undefined })(); |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [song, artists, tags, albums] = |
||||
await Promise.all([songPromise, artistIdsPromise, tagIdsPromise, albumIdsPromise]);; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
||||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length) || |
||||
!song) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifySong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the song.
|
||||
var update: any = {}; |
||||
if ("title" in reqObject) { update["title"] = reqObject.title; } |
||||
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } |
||||
const modifySongPromise = trx('songs') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.update(update) |
||||
|
||||
// Remove unlinked artists.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedArtists = artists ? trx('songs_artists') |
||||
.where({ 'songId': req.params.id }) |
||||
.whereNotIn('artistId', reqObject.artistIds || []) |
||||
.delete() : undefined; |
||||
|
||||
// Remove unlinked tags.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedTags = tags ? trx('songs_tags') |
||||
.where({ 'songId': req.params.id }) |
||||
.whereNotIn('tagId', reqObject.tagIds || []) |
||||
.delete() : undefined; |
||||
|
||||
// Remove unlinked albums.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedAlbums = albums ? trx('songs_albums') |
||||
.where({ 'songId': req.params.id }) |
||||
.whereNotIn('albumId', reqObject.albumIds || []) |
||||
.delete() : undefined; |
||||
|
||||
// Link new artists.
|
||||
// TODO: test this!
|
||||
const addArtists = artists ? trx('songs_artists') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((as: any) => as.map((a: any) => a['artistId'])) |
||||
.then((doneArtistIds: number[]) => { |
||||
// Get the set of artists that are not yet linked
|
||||
const toLink = artists.filter((id: number) => { |
||||
return !doneArtistIds.includes(id); |
||||
}); |
||||
const insertObjects = toLink.map((artistId: number) => { |
||||
return { |
||||
artistId: artistId, |
||||
songId: req.params.id, |
||||
} |
||||
}) |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('songs_artists').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Link new tags.
|
||||
// TODO: test this!
|
||||
const addTags = tags ? trx('songs_tags') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
||||
.then((doneTagIds: number[]) => { |
||||
// Get the set of tags that are not yet linked
|
||||
const toLink = tags.filter((id: number) => { |
||||
return !doneTagIds.includes(id); |
||||
}); |
||||
const insertObjects = toLink.map((tagId: number) => { |
||||
return { |
||||
tagId: tagId, |
||||
songId: req.params.id, |
||||
} |
||||
}) |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('songs_tags').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Link new albums.
|
||||
// TODO: test this!
|
||||
const addAlbums = albums ? trx('songs_albums') |
||||
.where({ 'albumId': req.params.id }) |
||||
.then((as: any) => as.map((a: any) => a['albumId'])) |
||||
.then((doneAlbumIds: number[]) => { |
||||
// Get the set of albums that are not yet linked
|
||||
const toLink = albums.filter((id: number) => { |
||||
return !doneAlbumIds.includes(id); |
||||
}); |
||||
const insertObjects = toLink.map((albumId: number) => { |
||||
return { |
||||
albumId: albumId, |
||||
songId: req.params.id, |
||||
} |
||||
}) |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('songs_albums').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Wait for all operations to finish.
|
||||
await Promise.all([ |
||||
modifySongPromise, |
||||
removeUnlinkedArtists, |
||||
removeUnlinkedTags, |
||||
removeUnlinkedAlbums, |
||||
addArtists, |
||||
addTags, |
||||
addAlbums, |
||||
]); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,66 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid ModifyTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Modify Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the parent tag.
|
||||
const parentTagPromise = reqObject.parentId ? |
||||
trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': reqObject.parentId }) |
||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving the tag itself.
|
||||
const tagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [tag, parent] = await Promise.all([tagPromise, parentTagPromise]);; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if ((reqObject.parentId && !parent) || |
||||
!tag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Tag or parent does not exist for ModifyTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the tag.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.update({ |
||||
name: reqObject.name, |
||||
parentId: reqObject.parentId || null, |
||||
}) |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -0,0 +1,372 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
import asJson from '../lib/asJson'; |
||||
|
||||
export const PostSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateSongRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostSong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateSongRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Post Song ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving artists.
|
||||
const artistIdsPromise = reqObject.artistIds ? |
||||
trx.select('id') |
||||
.from('artists') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.artistIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving tags.
|
||||
const tagIdsPromise = reqObject.tagIds ? |
||||
trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.tagIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving albums.
|
||||
const albumIdsPromise = reqObject.albumIds ? |
||||
trx.select('id') |
||||
.from('albums') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', reqObject.albumIds) |
||||
.then((as: any) => as.map((a: any) => a['id'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [artists, tags, albums] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdsPromise]);; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
||||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Create the song.
|
||||
const songId = (await trx('songs') |
||||
.insert({ |
||||
title: reqObject.title, |
||||
storeLinks: JSON.stringify(reqObject.storeLinks || []), |
||||
user: userId, |
||||
}) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Link the artists via the linking table.
|
||||
if (artists && artists.length) { |
||||
await Promise.all( |
||||
artists.map((artistId: number) => { |
||||
return trx('songs_artists').insert({ |
||||
artistId: artistId, |
||||
songId: songId, |
||||
}) |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Link the tags via the linking table.
|
||||
if (tags && tags.length) { |
||||
await Promise.all( |
||||
tags.map((tagId: number) => { |
||||
return trx('songs_tags').insert({ |
||||
songId: songId, |
||||
tagId: tagId, |
||||
}) |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Link the albums via the linking table.
|
||||
if (albums && albums.length) { |
||||
await Promise.all( |
||||
albums.map((albumId: number) => { |
||||
return trx('songs_albums').insert({ |
||||
songId: songId, |
||||
albumId: albumId, |
||||
}) |
||||
}) |
||||
) |
||||
} |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateSongResponse = { |
||||
id: songId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export const GetSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkSongDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid GetSong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const tagIdsPromise: Promise<number[]> = knex.select('tagId') |
||||
.from('songs_tags') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((ts: any) => { |
||||
return Array.from(new Set( |
||||
ts.map((tag: any) => tag['tagId']) |
||||
)); |
||||
}) |
||||
|
||||
const albumIdsPromise: Promise<number[]> = knex.select('albumId') |
||||
.from('songs_albums') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((as: any) => { |
||||
return Array.from(new Set( |
||||
as.map((album: any) => album['albumId']) |
||||
)); |
||||
}) |
||||
|
||||
const artistIdsPromise: Promise<number[]> = knex.select('artistId') |
||||
.from('songs_artists') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((as: any) => { |
||||
return Array.from(new Set( |
||||
as.map((artist: any) => artist['artistId']) |
||||
)); |
||||
}) |
||||
const songPromise = await knex.select(['id', 'title', 'storeLinks']) |
||||
.from('songs') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.then((ss: any) => ss[0]) |
||||
|
||||
const [tags, albums, artists, song] = |
||||
await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]); |
||||
|
||||
if (song) { |
||||
const response: api.SongDetailsResponse = { |
||||
title: song.title, |
||||
tagIds: tags, |
||||
artistIds: artists, |
||||
albumIds: albums, |
||||
storeLinks: asJson(song.storeLinks), |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
||||
|
||||
|
||||
export const PutSong: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifySongRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutSong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifySongRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Song ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Retrieve the song to be modified itself.
|
||||
const songPromise = trx.select('id') |
||||
.from('songs') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Start retrieving artists.
|
||||
const artistIdsPromise = reqObject.artistIds ? |
||||
trx.select('artistId') |
||||
.from('songs_artists') |
||||
.whereIn('id', reqObject.artistIds) |
||||
.then((as: any) => as.map((a: any) => a['artistId'])) : |
||||
(async () => { return undefined })(); |
||||
|
||||
// Start retrieving tags.
|
||||
const tagIdsPromise = reqObject.tagIds ? |
||||
trx.select('id') |
||||
.from('songs_tags') |
||||
.whereIn('id', reqObject.tagIds) |
||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||
(async () => { return undefined })(); |
||||
|
||||
// Start retrieving albums.
|
||||
const albumIdsPromise = reqObject.albumIds ? |
||||
trx.select('id') |
||||
.from('songs_albums') |
||||
.whereIn('id', reqObject.albumIds) |
||||
.then((as: any) => as.map((a: any) => a['albumId'])) : |
||||
(async () => { return undefined })(); |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [song, artists, tags, albums] = |
||||
await Promise.all([songPromise, artistIdsPromise, tagIdsPromise, albumIdsPromise]);; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) || |
||||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) || |
||||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length) || |
||||
!song) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifySong request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the song.
|
||||
var update: any = {}; |
||||
if ("title" in reqObject) { update["title"] = reqObject.title; } |
||||
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } |
||||
const modifySongPromise = trx('songs') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.update(update) |
||||
|
||||
// Remove unlinked artists.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedArtists = artists ? trx('songs_artists') |
||||
.where({ 'songId': req.params.id }) |
||||
.whereNotIn('artistId', reqObject.artistIds || []) |
||||
.delete() : undefined; |
||||
|
||||
// Remove unlinked tags.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedTags = tags ? trx('songs_tags') |
||||
.where({ 'songId': req.params.id }) |
||||
.whereNotIn('tagId', reqObject.tagIds || []) |
||||
.delete() : undefined; |
||||
|
||||
// Remove unlinked albums.
|
||||
// TODO: test this!
|
||||
const removeUnlinkedAlbums = albums ? trx('songs_albums') |
||||
.where({ 'songId': req.params.id }) |
||||
.whereNotIn('albumId', reqObject.albumIds || []) |
||||
.delete() : undefined; |
||||
|
||||
// Link new artists.
|
||||
// TODO: test this!
|
||||
const addArtists = artists ? trx('songs_artists') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((as: any) => as.map((a: any) => a['artistId'])) |
||||
.then((doneArtistIds: number[]) => { |
||||
// Get the set of artists that are not yet linked
|
||||
const toLink = artists.filter((id: number) => { |
||||
return !doneArtistIds.includes(id); |
||||
}); |
||||
const insertObjects = toLink.map((artistId: number) => { |
||||
return { |
||||
artistId: artistId, |
||||
songId: req.params.id, |
||||
} |
||||
}) |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('songs_artists').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Link new tags.
|
||||
// TODO: test this!
|
||||
const addTags = tags ? trx('songs_tags') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) |
||||
.then((doneTagIds: number[]) => { |
||||
// Get the set of tags that are not yet linked
|
||||
const toLink = tags.filter((id: number) => { |
||||
return !doneTagIds.includes(id); |
||||
}); |
||||
const insertObjects = toLink.map((tagId: number) => { |
||||
return { |
||||
tagId: tagId, |
||||
songId: req.params.id, |
||||
} |
||||
}) |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('songs_tags').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Link new albums.
|
||||
// TODO: test this!
|
||||
const addAlbums = albums ? trx('songs_albums') |
||||
.where({ 'albumId': req.params.id }) |
||||
.then((as: any) => as.map((a: any) => a['albumId'])) |
||||
.then((doneAlbumIds: number[]) => { |
||||
// Get the set of albums that are not yet linked
|
||||
const toLink = albums.filter((id: number) => { |
||||
return !doneAlbumIds.includes(id); |
||||
}); |
||||
const insertObjects = toLink.map((albumId: number) => { |
||||
return { |
||||
albumId: albumId, |
||||
songId: req.params.id, |
||||
} |
||||
}) |
||||
|
||||
// Link them
|
||||
return Promise.all( |
||||
insertObjects.map((obj: any) => |
||||
trx('songs_albums').insert(obj) |
||||
) |
||||
); |
||||
}) : undefined; |
||||
|
||||
// Wait for all operations to finish.
|
||||
await Promise.all([ |
||||
modifySongPromise, |
||||
removeUnlinkedArtists, |
||||
removeUnlinkedTags, |
||||
removeUnlinkedAlbums, |
||||
addArtists, |
||||
addTags, |
||||
addAlbums, |
||||
]); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,68 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
import asJson from '../lib/asJson'; |
||||
|
||||
export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkSongDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid SongDetails request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const tagIdsPromise: Promise<number[]> = knex.select('tagId') |
||||
.from('songs_tags') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((ts: any) => { |
||||
return Array.from(new Set( |
||||
ts.map((tag: any) => tag['tagId']) |
||||
)); |
||||
}) |
||||
|
||||
const albumIdsPromise: Promise<number[]> = knex.select('albumId') |
||||
.from('songs_albums') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((as: any) => { |
||||
return Array.from(new Set( |
||||
as.map((album: any) => album['albumId']) |
||||
)); |
||||
}) |
||||
|
||||
const artistIdsPromise: Promise<number[]> = knex.select('artistId') |
||||
.from('songs_artists') |
||||
.where({ 'songId': req.params.id }) |
||||
.then((as: any) => { |
||||
return Array.from(new Set( |
||||
as.map((artist: any) => artist['artistId']) |
||||
)); |
||||
}) |
||||
const songPromise = await knex.select(['id', 'title', 'storeLinks']) |
||||
.from('songs') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.then((ss: any) => ss[0]) |
||||
|
||||
const [tags, albums, artists, song] = |
||||
await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]); |
||||
|
||||
if (song) { |
||||
const response: api.SongDetailsResponse = { |
||||
title: song.title, |
||||
tagIds: tags, |
||||
artistIds: artists, |
||||
albumIds: albums, |
||||
storeLinks: asJson(song.storeLinks), |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
@ -0,0 +1,306 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const PostTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkCreateTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PostTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.CreateTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Post Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// If applicable, retrieve the parent tag.
|
||||
const maybeParent: number | undefined = |
||||
reqObject.parentId ? |
||||
(await trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': reqObject.parentId }))[0]['id'] : |
||||
undefined; |
||||
|
||||
// Check if the parent was found, if applicable.
|
||||
if (reqObject.parentId && maybeParent !== reqObject.parentId) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Could not find parent tag for CreateTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Create the new tag.
|
||||
var tag: any = { |
||||
name: reqObject.name, |
||||
user: userId, |
||||
}; |
||||
if (maybeParent) { |
||||
tag['parentId'] = maybeParent; |
||||
} |
||||
const tagId = (await trx('tags') |
||||
.insert(tag) |
||||
.returning('id') // Needed for Postgres
|
||||
)[0]; |
||||
|
||||
// Respond to the request.
|
||||
const responseObject: api.CreateTagResponse = { |
||||
id: tagId |
||||
}; |
||||
res.status(200).send(responseObject); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
async function getChildrenRecursive(id: number, userId: number, trx: any) { |
||||
const directChildren = (await trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'parentId': id })).map((r: any) => r.id); |
||||
|
||||
const indirectChildrenPromises = directChildren.map( |
||||
(child: number) => getChildrenRecursive(child, userId, trx) |
||||
); |
||||
const indirectChildrenNested = await Promise.all(indirectChildrenPromises); |
||||
const indirectChildren = indirectChildrenNested.flat(); |
||||
|
||||
return [ |
||||
...directChildren, |
||||
...indirectChildren, |
||||
] |
||||
} |
||||
|
||||
export const DeleteTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkDeleteTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid DeleteTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Delete Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving any child tags.
|
||||
const childTagsPromise =
|
||||
getChildrenRecursive(req.params.id, userId, trx); |
||||
|
||||
// Start retrieving the tag itself.
|
||||
const tagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [tag, children] = await Promise.all([tagPromise, childTagsPromise]); |
||||
|
||||
// Merge all IDs.
|
||||
const toDelete = [ tag, ...children ]; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!tag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Tag or parent does not exist for DeleteTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Delete the tag and its children.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.whereIn('id', toDelete) |
||||
.del(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export const GetTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkTagDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid GetTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const results = await knex.select(['id', 'name', 'parentId']) |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }); |
||||
|
||||
if (results[0]) { |
||||
const response: api.TagDetailsResponse = { |
||||
name: results[0].name, |
||||
parentId: results[0].parentId || undefined, |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
||||
|
||||
export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkModifyTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid PutTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.ModifyTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Put Tag ", reqObject); |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the parent tag.
|
||||
const parentTagPromise = reqObject.parentId ? |
||||
trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': reqObject.parentId }) |
||||
.then((ts: any) => ts.map((t: any) => t['tagId'])) : |
||||
(async () => { return [] })(); |
||||
|
||||
// Start retrieving the tag itself.
|
||||
const tagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: req.params.id }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [tag, parent] = await Promise.all([tagPromise, parentTagPromise]);; |
||||
|
||||
// Check that we found all objects we need.
|
||||
if ((reqObject.parentId && !parent) || |
||||
!tag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Tag or parent does not exist for ModifyTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Modify the tag.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }) |
||||
.update({ |
||||
name: reqObject.name, |
||||
parentId: reqObject.parentId || null, |
||||
}) |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
export const MergeTag: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkMergeTagRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
const reqObject: api.DeleteTagRequest = req.body; |
||||
const { id: userId } = req.user; |
||||
|
||||
console.log("User ", userId, ": Merge Tag ", reqObject); |
||||
const fromId = req.params.id; |
||||
const toId = req.params.toId; |
||||
|
||||
await knex.transaction(async (trx) => { |
||||
try { |
||||
// Start retrieving the "from" tag.
|
||||
const fromTagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: fromId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Start retrieving the "to" tag.
|
||||
const toTagPromise = trx.select('id') |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ id: toId }) |
||||
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) |
||||
|
||||
// Wait for the requests to finish.
|
||||
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]); |
||||
|
||||
// Check that we found all objects we need.
|
||||
if (!fromTag || !toTag) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
// Assign new tag ID to any objects referencing the to-be-merged tag.
|
||||
const cPromise = trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'parentId': fromId }) |
||||
.update({ 'parentId': toId }); |
||||
const sPromise = trx('songs_tags') |
||||
.where({ 'tagId': fromId }) |
||||
.update({ 'tagId': toId }); |
||||
const arPromise = trx('artists_tags') |
||||
.where({ 'tagId': fromId }) |
||||
.update({ 'tagId': toId }); |
||||
const alPromise = trx('albums_tags') |
||||
.where({ 'tagId': fromId }) |
||||
.update({ 'tagId': toId }); |
||||
await Promise.all([sPromise, arPromise, alPromise, cPromise]); |
||||
|
||||
// Delete the original tag.
|
||||
await trx('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': fromId }) |
||||
.del(); |
||||
|
||||
// Respond to the request.
|
||||
res.status(200).send(); |
||||
|
||||
} catch (e) { |
||||
catchUnhandledErrors(e); |
||||
trx.rollback(); |
||||
} |
||||
}) |
||||
} |
@ -1,34 +0,0 @@ |
||||
import * as api from '../../client/src/api'; |
||||
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; |
||||
import Knex from 'knex'; |
||||
|
||||
export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { |
||||
if (!api.checkTagDetailsRequest(req)) { |
||||
const e: EndpointError = { |
||||
internalMessage: 'Invalid TagDetails request: ' + JSON.stringify(req.body), |
||||
httpStatus: 400 |
||||
}; |
||||
throw e; |
||||
} |
||||
|
||||
const { id: userId } = req.user; |
||||
|
||||
try { |
||||
const results = await knex.select(['id', 'name', 'parentId']) |
||||
.from('tags') |
||||
.where({ 'user': userId }) |
||||
.where({ 'id': req.params.id }); |
||||
|
||||
if (results[0]) { |
||||
const response: api.TagDetailsResponse = { |
||||
name: results[0].name, |
||||
parentId: results[0].parentId || undefined, |
||||
} |
||||
await res.send(response); |
||||
} else { |
||||
await res.status(404).send({}); |
||||
} |
||||
} catch (e) { |
||||
catchUnhandledErrors(e) |
||||
} |
||||
} |
Loading…
Reference in new issue