Got all APIs working in Knex, except query.

pull/10/head
Sander Vocke 5 years ago
parent 5dcc8451bf
commit ddbfb3a52c
  1. 118
      client/src/api.ts
  2. 2
      server/.gitignore
  3. 37
      server/app.ts
  4. 20
      server/config/config.json
  5. 59
      server/endpoints/AlbumDetailsEndpointHandler.ts
  6. 38
      server/endpoints/ArtistDetailsEndpointHandler.ts
  7. 92
      server/endpoints/CreateAlbumEndpointHandler.ts
  8. 54
      server/endpoints/CreateArtistEndpointHandler.ts
  9. 124
      server/endpoints/CreateSongEndpointHandler.ts
  10. 58
      server/endpoints/CreateTagEndpointHandler.ts
  11. 160
      server/endpoints/ModifyAlbumEndpointHandler.ts
  12. 101
      server/endpoints/ModifyArtistEndpointHandler.ts
  13. 207
      server/endpoints/ModifySongEndpointHandler.ts
  14. 71
      server/endpoints/ModifyTagEndpointHandler.ts
  15. 324
      server/endpoints/QueryEndpointHandler.ts
  16. 63
      server/endpoints/SongDetailsEndpointHandler.ts
  17. 41
      server/endpoints/TagDetailsEndpointHandler.ts
  18. 4
      server/endpoints/types.ts
  19. 3
      server/knex/knex.ts
  20. 44
      server/knexfile.ts
  21. 118
      server/migrations/20200828124218_init_db.ts
  22. 14
      server/models/album.js
  23. 14
      server/models/artist.js
  24. 37
      server/models/index.js
  25. 13
      server/models/ranking.js
  26. 15
      server/models/song.js
  27. 14
      server/models/tag.js
  28. 3
      server/package.json
  29. 15
      server/server.ts
  30. 4
      server/test/integration/flows/AlbumFlow.js
  31. 27
      server/test/integration/flows/ArtistFlow.js
  32. 4
      server/test/integration/flows/QueryFlow.js
  33. 4
      server/test/integration/flows/SongFlow.js
  34. 4
      server/test/integration/flows/TagFlow.js
  35. 6
      server/test/integration/flows/helpers.js
  36. 1326
      server/yarn.lock

@ -15,38 +15,38 @@ export enum ItemType {
} }
export interface ArtistDetails { export interface ArtistDetails {
artistId: Number, artistId: number,
name: String, name: string,
storeLinks?: String[], storeLinks?: string[],
} }
export function isArtistDetails(q: any): q is ArtistDetails { export function isArtistDetails(q: any): q is ArtistDetails {
return 'artistId' in q; return 'artistId' in q;
} }
export interface TagDetails { export interface TagDetails {
tagId: Number, tagId: number,
name: String, name: string,
parent?: TagDetails, parent?: TagDetails,
storeLinks?: String[], storeLinks?: string[],
} }
export function isTagDetails(q: any): q is TagDetails { export function isTagDetails(q: any): q is TagDetails {
return 'tagId' in q; return 'tagId' in q;
} }
export interface RankingDetails { export interface RankingDetails {
rankingId: Number, rankingId: number,
type: ItemType, // The item type being ranked type: ItemType, // The item type being ranked
rankedId: Number, // The item being ranked rankedId: number, // The item being ranked
context: ArtistDetails | TagDetails, context: ArtistDetails | TagDetails,
value: Number, // The ranking (higher = better) value: number, // The ranking (higher = better)
} }
export function isRankingDetails(q: any): q is RankingDetails { export function isRankingDetails(q: any): q is RankingDetails {
return 'rankingId' in q; return 'rankingId' in q;
} }
export interface SongDetails { export interface SongDetails {
songId: Number, songId: number,
title: String, title: string,
artists?: ArtistDetails[], artists?: ArtistDetails[],
tags?: TagDetails[], tags?: TagDetails[],
storeLinks?: String[], storeLinks?: string[],
rankings?: RankingDetails[], rankings?: RankingDetails[],
} }
export function isSongDetails(q: any): q is SongDetails { export function isSongDetails(q: any): q is SongDetails {
@ -134,11 +134,11 @@ export function checkQueryRequest(req: any): boolean {
export const SongDetailsEndpoint = '/song/:id'; export const SongDetailsEndpoint = '/song/:id';
export interface SongDetailsRequest { } export interface SongDetailsRequest { }
export interface SongDetailsResponse { export interface SongDetailsResponse {
title: String, title: string,
storeLinks: String[], storeLinks: string[],
artistIds: Number[], artistIds: number[],
albumIds: Number[], albumIds: number[],
tagIds: Number[], tagIds: number[],
} }
export function checkSongDetailsRequest(req: any): boolean { export function checkSongDetailsRequest(req: any): boolean {
return true; return true;
@ -148,9 +148,9 @@ export function checkSongDetailsRequest(req: any): boolean {
export const ArtistDetailsEndpoint = '/artist/:id'; export const ArtistDetailsEndpoint = '/artist/:id';
export interface ArtistDetailsRequest { } export interface ArtistDetailsRequest { }
export interface ArtistDetailsResponse { export interface ArtistDetailsResponse {
name: String, name: string,
tagIds: Number[], tagIds: number[],
storeLinks: String[], storeLinks: string[],
} }
export function checkArtistDetailsRequest(req: any): boolean { export function checkArtistDetailsRequest(req: any): boolean {
return true; return true;
@ -159,14 +159,14 @@ export function checkArtistDetailsRequest(req: any): boolean {
// Create a new song (POST). // Create a new song (POST).
export const CreateSongEndpoint = '/song'; export const CreateSongEndpoint = '/song';
export interface CreateSongRequest { export interface CreateSongRequest {
title: String; title: string;
artistIds?: Number[]; artistIds?: number[];
albumIds?: Number[]; albumIds?: number[];
tagIds?: Number[]; tagIds?: number[];
storeLinks?: String[]; storeLinks?: string[];
} }
export interface CreateSongResponse { export interface CreateSongResponse {
id: Number; id: number;
} }
export function checkCreateSongRequest(req: any): boolean { export function checkCreateSongRequest(req: any): boolean {
return "body" in req && return "body" in req &&
@ -176,11 +176,11 @@ export function checkCreateSongRequest(req: any): boolean {
// Modify an existing song (PUT). // Modify an existing song (PUT).
export const ModifySongEndpoint = '/song/:id'; export const ModifySongEndpoint = '/song/:id';
export interface ModifySongRequest { export interface ModifySongRequest {
title?: String; title?: string;
artistIds?: Number[]; artistIds?: number[];
albumIds?: Number[]; albumIds?: number[];
tagIds?: Number[]; tagIds?: number[];
storeLinks?: String[]; storeLinks?: string[];
} }
export interface ModifySongResponse { } export interface ModifySongResponse { }
export function checkModifySongRequest(req: any): boolean { export function checkModifySongRequest(req: any): boolean {
@ -190,13 +190,13 @@ export function checkModifySongRequest(req: any): boolean {
// Create a new album (POST). // Create a new album (POST).
export const CreateAlbumEndpoint = '/album'; export const CreateAlbumEndpoint = '/album';
export interface CreateAlbumRequest { export interface CreateAlbumRequest {
name: String; name: string;
tagIds?: Number[]; tagIds?: number[];
artistIds?: Number[]; artistIds?: number[];
storeLinks?: String[]; storeLinks?: string[];
} }
export interface CreateAlbumResponse { export interface CreateAlbumResponse {
id: Number; id: number;
} }
export function checkCreateAlbumRequest(req: any): boolean { export function checkCreateAlbumRequest(req: any): boolean {
return "body" in req && return "body" in req &&
@ -206,10 +206,10 @@ export function checkCreateAlbumRequest(req: any): boolean {
// Modify an existing album (PUT). // Modify an existing album (PUT).
export const ModifyAlbumEndpoint = '/album/:id'; export const ModifyAlbumEndpoint = '/album/:id';
export interface ModifyAlbumRequest { export interface ModifyAlbumRequest {
name?: String; name?: string;
tagIds?: Number[]; tagIds?: number[];
artistIds?: Number[]; artistIds?: number[];
storeLinks?: String[]; storeLinks?: string[];
} }
export interface ModifyAlbumResponse { } export interface ModifyAlbumResponse { }
export function checkModifyAlbumRequest(req: any): boolean { export function checkModifyAlbumRequest(req: any): boolean {
@ -220,11 +220,11 @@ export function checkModifyAlbumRequest(req: any): boolean {
export const AlbumDetailsEndpoint = '/album/:id'; export const AlbumDetailsEndpoint = '/album/:id';
export interface AlbumDetailsRequest { } export interface AlbumDetailsRequest { }
export interface AlbumDetailsResponse { export interface AlbumDetailsResponse {
name: String; name: string;
tagIds: Number[]; tagIds: number[];
artistIds: Number[]; artistIds: number[];
songIds: Number[]; songIds: number[];
storeLinks: String[]; storeLinks: string[];
} }
export function checkAlbumDetailsRequest(req: any): boolean { export function checkAlbumDetailsRequest(req: any): boolean {
return true; return true;
@ -233,12 +233,12 @@ export function checkAlbumDetailsRequest(req: any): boolean {
// Create a new artist (POST). // Create a new artist (POST).
export const CreateArtistEndpoint = '/artist'; export const CreateArtistEndpoint = '/artist';
export interface CreateArtistRequest { export interface CreateArtistRequest {
name: String; name: string;
tagIds?: Number[]; tagIds?: number[];
storeLinks?: String[]; storeLinks?: string[];
} }
export interface CreateArtistResponse { export interface CreateArtistResponse {
id: Number; id: number;
} }
export function checkCreateArtistRequest(req: any): boolean { export function checkCreateArtistRequest(req: any): boolean {
return "body" in req && return "body" in req &&
@ -248,9 +248,9 @@ export function checkCreateArtistRequest(req: any): boolean {
// Modify an existing artist (PUT). // Modify an existing artist (PUT).
export const ModifyArtistEndpoint = '/artist/:id'; export const ModifyArtistEndpoint = '/artist/:id';
export interface ModifyArtistRequest { export interface ModifyArtistRequest {
name?: String, name?: string,
tagIds?: Number[]; tagIds?: number[];
storeLinks?: String[], storeLinks?: string[],
} }
export interface ModifyArtistResponse { } export interface ModifyArtistResponse { }
export function checkModifyArtistRequest(req: any): boolean { export function checkModifyArtistRequest(req: any): boolean {
@ -260,11 +260,11 @@ export function checkModifyArtistRequest(req: any): boolean {
// Create a new tag (POST). // Create a new tag (POST).
export const CreateTagEndpoint = '/tag'; export const CreateTagEndpoint = '/tag';
export interface CreateTagRequest { export interface CreateTagRequest {
name: String; name: string;
parentId?: Number; parentId?: number;
} }
export interface CreateTagResponse { export interface CreateTagResponse {
id: Number; id: number;
} }
export function checkCreateTagRequest(req: any): boolean { export function checkCreateTagRequest(req: any): boolean {
return "body" in req && return "body" in req &&
@ -274,8 +274,8 @@ export function checkCreateTagRequest(req: any): boolean {
// Modify an existing tag (PUT). // Modify an existing tag (PUT).
export const ModifyTagEndpoint = '/tag/:id'; export const ModifyTagEndpoint = '/tag/:id';
export interface ModifyTagRequest { export interface ModifyTagRequest {
name?: String, name?: string,
parentId?: Number; parentId?: number;
} }
export interface ModifyTagResponse { } export interface ModifyTagResponse { }
export function checkModifyTagRequest(req: any): boolean { export function checkModifyTagRequest(req: any): boolean {
@ -286,8 +286,8 @@ export function checkModifyTagRequest(req: any): boolean {
export const TagDetailsEndpoint = '/tag/:id'; export const TagDetailsEndpoint = '/tag/:id';
export interface TagDetailsRequest { } export interface TagDetailsRequest { }
export interface TagDetailsResponse { export interface TagDetailsResponse {
name: String, name: string,
parentId?: Number, parentId?: number,
} }
export function checkTagDetailsRequest(req: any): boolean { export function checkTagDetailsRequest(req: any): boolean {
return true; return true;

2
server/.gitignore vendored

@ -21,4 +21,4 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
db_dev.sqlite3 dev.sqlite3

@ -1,5 +1,6 @@
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
import * as api from '../client/src/api'; import * as api from '../client/src/api';
import Knex from 'knex';
import { CreateSongEndpointHandler } from './endpoints/CreateSongEndpointHandler'; import { CreateSongEndpointHandler } from './endpoints/CreateSongEndpointHandler';
import { CreateArtistEndpointHandler } from './endpoints/CreateArtistEndpointHandler'; import { CreateArtistEndpointHandler } from './endpoints/CreateArtistEndpointHandler';
@ -16,10 +17,10 @@ import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandl
import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler'; import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler';
import * as endpointTypes from './endpoints/types'; import * as endpointTypes from './endpoints/types';
const invokeHandler = (handler:endpointTypes.EndpointHandler) => { const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => {
return async (req: any, res: any) => { return async (req: any, res: any) => {
console.log("Incoming", req.method, " @ ", req.url); console.log("Incoming", req.method, " @ ", req.url);
await handler(req, res) await handler(req, res, knex)
.catch(endpointTypes.catchUnhandledErrors) .catch(endpointTypes.catchUnhandledErrors)
.catch((_e:endpointTypes.EndpointError) => { .catch((_e:endpointTypes.EndpointError) => {
let e:endpointTypes.EndpointError = _e; let e:endpointTypes.EndpointError = _e;
@ -30,24 +31,28 @@ const invokeHandler = (handler:endpointTypes.EndpointHandler) => {
}; };
} }
const SetupApp = (app: any) => { const SetupApp = (app: any, knex: Knex) => {
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
const invokeWithKnex = (handler: endpointTypes.EndpointHandler) => {
return invokeHandler(handler, knex);
}
// Set up REST API endpoints // Set up REST API endpoints
app.post(api.CreateSongEndpoint, invokeHandler(CreateSongEndpointHandler)); app.post(api.CreateSongEndpoint, invokeWithKnex(CreateSongEndpointHandler));
app.post(api.QueryEndpoint, invokeHandler(QueryEndpointHandler)); app.post(api.QueryEndpoint, invokeWithKnex(QueryEndpointHandler));
app.post(api.CreateArtistEndpoint, invokeHandler(CreateArtistEndpointHandler)); app.post(api.CreateArtistEndpoint, invokeWithKnex(CreateArtistEndpointHandler));
app.put(api.ModifyArtistEndpoint, invokeHandler(ModifyArtistEndpointHandler)); app.put(api.ModifyArtistEndpoint, invokeWithKnex(ModifyArtistEndpointHandler));
app.put(api.ModifySongEndpoint, invokeHandler(ModifySongEndpointHandler)); app.put(api.ModifySongEndpoint, invokeWithKnex(ModifySongEndpointHandler));
app.get(api.SongDetailsEndpoint, invokeHandler(SongDetailsEndpointHandler)); app.get(api.SongDetailsEndpoint, invokeWithKnex(SongDetailsEndpointHandler));
app.get(api.ArtistDetailsEndpoint, invokeHandler(ArtistDetailsEndpointHandler)); app.get(api.ArtistDetailsEndpoint, invokeWithKnex(ArtistDetailsEndpointHandler));
app.post(api.CreateTagEndpoint, invokeHandler(CreateTagEndpointHandler)); app.post(api.CreateTagEndpoint, invokeWithKnex(CreateTagEndpointHandler));
app.put(api.ModifyTagEndpoint, invokeHandler(ModifyTagEndpointHandler)); app.put(api.ModifyTagEndpoint, invokeWithKnex(ModifyTagEndpointHandler));
app.get(api.TagDetailsEndpoint, invokeHandler(TagDetailsEndpointHandler)); app.get(api.TagDetailsEndpoint, invokeWithKnex(TagDetailsEndpointHandler));
app.post(api.CreateAlbumEndpoint, invokeHandler(CreateAlbumEndpointHandler)); app.post(api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler));
app.put(api.ModifyAlbumEndpoint, invokeHandler(ModifyAlbumEndpointHandler)); app.put(api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler));
app.get(api.AlbumDetailsEndpoint, invokeHandler(AlbumDetailsEndpointHandler)); app.get(api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler));
} }
export { SetupApp } export { SetupApp }

@ -1,20 +0,0 @@
{
"development": {
"storage": "db_dev.sqlite3",
"dialect": "sqlite"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}

@ -1,8 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkAlbumDetailsRequest(req)) { if (!api.checkAlbumDetailsRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid AlbumDetails request: ' + JSON.stringify(req.body), internalMessage: 'Invalid AlbumDetails request: ' + JSON.stringify(req.body),
@ -12,27 +12,44 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res
} }
try { try {
const albums = await models.Album.findAll({ // Start transfers for songs, tags and artists.
include: [models.Artist, models.Tag, models.Song], // Also request the album itself.
where: { const tagIdsPromise = knex.select('tagId')
id: req.params.id .from('albums_tags')
} .where({ 'albumId': req.params.id })
.then((tags: any) => {
return tags.map((tag: any) => tag['tagId'])
}); });
if (albums.length != 1) { const songIdsPromise = knex.select('songId')
const e: EndpointError = { .from('songs_albums')
internalMessage: 'There is no album with id ' + req.params.id + '.', .where({ 'albumId': req.params.id })
httpStatus: 400 .then((songs: any) => {
}; return songs.map((song: any) => song['songId'])
throw e; });
} const artistIdsPromise = knex.select('artistId')
let album = albums[0]; .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({ 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.
const response: api.AlbumDetailsResponse = { const response: api.AlbumDetailsResponse = {
name: album.name, name: album['name'],
artistIds: album.Artists.map((artist: any) => artist.id), artistIds: artists,
tagIds: album.Tags.map((tag: any) => tag.id), tagIds: tags,
songIds: album.Songs.map((song: any) => song.id), songIds: songs,
storeLinks: album.storeLinks, storeLinks: JSON.parse(album['storeLinks']),
} };
await res.send(response); await res.send(response);
} catch (e) { } catch (e) {
catchUnhandledErrors(e); catchUnhandledErrors(e);

@ -1,8 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkArtistDetailsRequest(req)) { if (!api.checkArtistDetailsRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ArtistDetails request: ' + JSON.stringify(req.body), internalMessage: 'Invalid ArtistDetails request: ' + JSON.stringify(req.body),
@ -12,27 +12,21 @@ export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, re
} }
try { try {
const artists: any[] = await models.Artist.findAll({ const tagIds = Array.from(new Set((await knex.select('tagId')
where: { .from('artists_tags')
id: req.params.id .where({ 'artistId': req.params.id })
}, ).map((tag: any) => tag['tagId'])));
include: [models.Tag]
}); const results = await knex.select(['id', 'name', 'storeLinks'])
if (artists.length != 1) { .from('artists')
const e: EndpointError = { .where({ 'id': req.params.id });
internalMessage: 'There is no artist with id ' + req.params.id + '.',
httpStatus: 400
};
throw e;
}
let artist = artists[0];
const storeLinks = Array.isArray(artist.storeLinks) ? artist.storeLinks :
(artist.storeLinks ? [artist.storeLinks] : []);
const response: api.ArtistDetailsResponse = { const response: api.ArtistDetailsResponse = {
name: artist.name, name: results[0].name,
tagIds: artist.Tags.map((tag: any) => tag.id), tagIds: tagIds,
storeLinks: storeLinks, storeLinks: JSON.parse(results[0].storeLinks),
}; }
await res.send(response); await res.send(response);
} catch (e) { } catch (e) {
catchUnhandledErrors(e) catchUnhandledErrors(e)

@ -1,9 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize"); import Knex from 'knex';
export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateAlbumRequest(req)) { if (!api.checkCreateAlbumRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid CreateAlbum request: ' + JSON.stringify(req.body), internalMessage: 'Invalid CreateAlbum request: ' + JSON.stringify(req.body),
@ -13,29 +12,30 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res:
} }
const reqObject: api.CreateAlbumRequest = req.body; const reqObject: api.CreateAlbumRequest = req.body;
// Start retrieving the artist instances to link the album to. console.log("Create Album:", reqObject);
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
});
// Start retrieving the tag instances to link the album to. await knex.transaction(async (trx) => {
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({ try {
where: { // Start retrieving artists.
id: { const artistIdsPromise = reqObject.artistIds ?
[Op.in]: reqObject.tagIds trx.select('id')
} .from('artists')
} .whereIn('id', reqObject.artistIds)
}); .then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Upon finish retrieving artists and tags, create the album and associate it. // Start retrieving tags.
await Promise.all([artistInstancesPromise, tagInstancesPromise]) const tagIdsPromise = reqObject.tagIds ?
.then((values: any) => { trx.select('id')
var [artists, tags] = values; .from('tags')
.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) || if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) { (reqObject.tagIds && tags.length !== reqObject.tagIds.length)) {
const e: EndpointError = { const e: EndpointError = {
@ -45,19 +45,47 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res:
throw e; throw e;
} }
var album = models.Album.build({ // Create the album.
const albumId = (await trx('albums')
.insert({
name: reqObject.name, name: reqObject.name,
storeLinks: reqObject.storeLinks || [], storeLinks: JSON.stringify(reqObject.storeLinks || []),
}); })
artists && album.addArtists(artists); )[0];
tags && album.addTags(tags);
return album.save(); // 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,
}
}) })
.then((album: any) => { )
}
// 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 = { const responseObject: api.CreateSongResponse = {
id: album.id id: albumId
}; };
res.status(200).send(responseObject); res.status(200).send(responseObject);
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
}) })
.catch(catchUnhandledErrors);
} }

@ -1,9 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize"); import Knex from 'knex';
export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateArtistRequest(req)) { if (!api.checkCreateArtistRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid CreateArtist request: ' + JSON.stringify(req.body), internalMessage: 'Invalid CreateArtist request: ' + JSON.stringify(req.body),
@ -15,20 +14,20 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res
console.log("Create artist:", reqObject) console.log("Create artist:", reqObject)
await knex.transaction(async (trx) => {
try { try {
// Retrieve tag instances to link the artist to.
// Start retrieving the tag instances to link the artist to. const tags: number[] = reqObject.tagIds ?
const tags = reqObject.tagIds && await models.Tag.findAll({ Array.from(new Set(
where: { (await trx.select('id').from('tags')
id: { .whereIn('id', reqObject.tagIds))
[Op.in]: reqObject.tagIds .map((tag: any) => tag['id'])
} ))
} : [];
});
console.log("Found artist tags:", tags) console.log("Found artist tags:", tags)
if (reqObject.tagIds && tags.length !== reqObject.tagIds.length) { if (reqObject.tagIds && tags && tags.length !== reqObject.tagIds.length) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body), internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
@ -36,17 +35,34 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res
throw e; throw e;
} }
var artist = models.Artist.build({ // Create the artist.
const artistId = (await trx('artists')
.insert({
name: reqObject.name, name: reqObject.name,
storeLinks: reqObject.storeLinks || [], storeLinks: JSON.stringify(reqObject.storeLinks || []),
}); })
tags && artist.addTags(tags); )[0];
await artist.save();
// 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 = { const responseObject: api.CreateSongResponse = {
id: artist.id id: artistId
}; };
await res.status(200).send(responseObject); await res.status(200).send(responseObject);
} catch (e) { } catch (e) {
catchUnhandledErrors(e); catchUnhandledErrors(e);
trx.rollback();
} }
});
} }

@ -1,9 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize"); import Knex from 'knex';
export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateSongRequest(req)) { if (!api.checkCreateSongRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid CreateSong request: ' + JSON.stringify(req.body), internalMessage: 'Invalid CreateSong request: ' + JSON.stringify(req.body),
@ -13,41 +12,41 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
} }
const reqObject: api.CreateSongRequest = req.body; const reqObject: api.CreateSongRequest = req.body;
// Start retrieving the artist instances to link the song to. console.log("Create Song:", reqObject);
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
});
// Start retrieving the album instances to link the song to. await knex.transaction(async (trx) => {
var albumInstancesPromise = reqObject.albumIds && models.Album.findAll({ try {
where: { // Start retrieving artists.
id: { const artistIdsPromise = reqObject.artistIds ?
[Op.in]: reqObject.albumIds trx.select('id')
} .from('artists')
} .whereIn('id', reqObject.artistIds)
}); .then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Start retrieving the tag instances to link the song to. // Start retrieving tags.
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({ const tagIdsPromise = reqObject.tagIds ?
where: { trx.select('id')
id: { .from('tags')
[Op.in]: reqObject.tagIds .whereIn('id', reqObject.tagIds)
} .then((as: any) => as.map((a: any) => a['id'])) :
} (async () => { return [] })();
});
// Upon finish retrieving dependents, create the song and associate it. // Start retrieving albums.
await Promise.all([artistInstancesPromise, albumInstancesPromise, tagInstancesPromise]) const albumIdsPromise = reqObject.albumIds ?
.then((values: any) => { trx.select('id')
var [artists, albums, tags] = values; .from('albums')
.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) || if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length) || (reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) { (reqObject.albumIds && albums.length !== reqObject.albumIds.length)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body), internalMessage: 'Not all albums and/or artists and/or tags exist for CreateSong request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
@ -55,20 +54,59 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res:
throw e; throw e;
} }
var song = models.Song.build({ // Create the song.
const songId = (await trx('songs')
.insert({
title: reqObject.title, title: reqObject.title,
storeLinks: reqObject.storeLinks || [], storeLinks: JSON.stringify(reqObject.storeLinks || []),
}); })
artists && song.addArtists(artists); )[0];
albums && song.addAlbums(albums);
tags && song.addTags(tags); // Link the artists via the linking table.
return song.save(); 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,
}) })
.then((song: any) => { })
)
}
// Respond to the request.
const responseObject: api.CreateSongResponse = { const responseObject: api.CreateSongResponse = {
id: song.id id: songId
}; };
res.status(200).send(responseObject); res.status(200).send(responseObject);
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
}) })
.catch(catchUnhandledErrors);
} }

@ -1,8 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateTagRequest(req)) { if (!api.checkCreateTagRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid CreateTag request: ' + JSON.stringify(req.body), internalMessage: 'Invalid CreateTag request: ' + JSON.stringify(req.body),
@ -12,37 +12,45 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a
} }
const reqObject: api.CreateTagRequest = req.body; const reqObject: api.CreateTagRequest = req.body;
const getTag = async (id: Number) => { console.log("Create Tag: ", reqObject);
const tag = await models.Tag.findAll({
where: { await knex.transaction(async (trx) => {
id: id try {
} // If applicable, retrieve the parent tag.
}); const maybeParent: number | undefined =
if (tag.length != 1) { reqObject.parentId ?
(await trx.select('id')
.from('tags')
.where({ 'id': reqObject.parentId }))[0]['id'] :
undefined;
// Check if the parent was found, if applicable.
if (reqObject.parentId && maybeParent !== reqObject.parentId) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'There is no tag with id ' + id + '.', internalMessage: 'Could not find parent tag for CreateTag request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
}; };
throw e; throw e;
} }
return tag[0];
}
// If applicable, start retrieving the new parent tag.
const maybeNewParentPromise: Promise<any> | Promise<undefined> =
(reqObject.parentId) ? getTag(reqObject.parentId) : (async () => { return undefined })();
(async () => { // Create the new tag.
const maybeParent = await maybeNewParentPromise; var tag: any = {
const tag = await models.Tag.create({
name: reqObject.name name: reqObject.name
}); };
reqObject.parentId && await tag.setParent(maybeParent); if (maybeParent) {
await tag.save(); tag['parentId'] = maybeParent;
}
const tagId = (await trx('tags').insert(tag))[0];
// Respond to the request.
const responseObject: api.CreateTagResponse = { const responseObject: api.CreateTagResponse = {
id: tag.id id: tagId
}; };
res.status(200).send(responseObject); res.status(200).send(responseObject);
})()
.catch(catchUnhandledErrors); } catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
} }

@ -1,9 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize"); import Knex from 'knex';
export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyAlbumRequest(req)) { if (!api.checkModifyAlbumRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ModifyAlbum request: ' + JSON.stringify(req.body), internalMessage: 'Invalid ModifyAlbum request: ' + JSON.stringify(req.body),
@ -13,50 +12,133 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res:
} }
const reqObject: api.ModifyAlbumRequest = req.body; const reqObject: api.ModifyAlbumRequest = req.body;
// Start retrieving the artist instances to link the album to. console.log("Modify Album:", reqObject);
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
});
// Start retrieving the tag instances to link the album to. await knex.transaction(async (trx) => {
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({ try {
where: { // Start retrieving artists.
id: { const artistIdsPromise = reqObject.artistIds ?
[Op.in]: reqObject.tagIds trx.select('artistId')
} .from('artists_albums')
} .whereIn('id', reqObject.artistIds)
}); .then((as: any) => as.map((a: any) => a['artistId'])) :
(async () => { return [] })();
// Start retrieving the album to modify. // Start retrieving tags.
var albumInstancePromise = models.Album.findOne({ const tagIdsPromise = reqObject.tagIds ?
where: { trx.select('id')
id: req.params.id .from('albums_tags')
} .whereIn('id', reqObject.tagIds)
}); .then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return [] })();
// Upon finish retrieving artists and albums, modify the album. // Start retrieving the album itself.
await Promise.all([artistInstancesPromise, tagInstancesPromise, albumInstancePromise]) const albumPromise = trx.select('id')
.then(async (values: any) => { .from('albums')
var [artists, tags, album] = values; .where({ id: req.params.id })
if (!album) { .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Wait for the requests to finish.
var [album, artists, tags] = await Promise.all([albumPromise, artistIdsPromise, tagIdsPromise]);;
// Check that we found all objects we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
!album) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'There is no album with id ' + req.params.id + '.', internalMessage: 'Not all albums and/or artists and/or tags exist for ModifyAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
}; };
throw e; throw e;
} }
if (reqObject.artistIds) { album.setArtists(artists) };
if (reqObject.tagIds) { album.setTags(tags) }; // Modify the album.
if (reqObject.name) { album.name = reqObject.name }; const modifyAlbumPromise = trx('albums')
if (reqObject.storeLinks) { album.setStoreIds(reqObject.storeLinks) }; .where({ 'id': req.params.id })
await album.save(); .update({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
}) })
.then(() => {
res.status(200).send({}); // Remove unlinked artists.
// TODO: test this!
const removeUnlinkedArtists = trx('artists_albums')
.where({ 'albumId': req.params.id })
.whereNotIn('artistId', reqObject.artistIds || [])
.delete();
// Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = trx('albums_tags')
.where({ 'albumId': req.params.id })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete();
// Link new artists.
// TODO: test this!
const addArtists = trx('artists_albums')
.where({ 'albumId': req.params.id })
.then((as: any) => as.map((a: any) => a['artistId']))
.then((doneArtistIds: number[]) => {
// Get the set of artists that are not yet linked
const toLink = artists.filter((id: number) => {
return !doneArtistIds.includes(id);
});
const insertObjects = toLink.map((artistId: number) => {
return {
artistId: artistId,
albumId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('artists_albums').insert(obj)
)
);
})
// Link new tags.
// TODO: test this!
const addTags = trx('albums_tags')
.where({ 'albumId': req.params.id })
.then((ts: any) => ts.map((t: any) => t['tagId']))
.then((doneTagIds: number[]) => {
// Get the set of tags that are not yet linked
const toLink = tags.filter((id: number) => {
return !doneTagIds.includes(id);
});
const insertObjects = toLink.map((tagId: number) => {
return {
tagId: tagId,
albumId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('albums_tags').insert(obj)
)
);
})
// Wait for all operations to finish.
await Promise.all([
modifyAlbumPromise,
removeUnlinkedArtists,
removeUnlinkedTags,
addArtists,
addTags
]);
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
}) })
.catch(catchUnhandledErrors);
} }

@ -1,8 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyArtistRequest(req)) { if (!api.checkModifyArtistRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ModifyArtist request: ' + JSON.stringify(req.body), internalMessage: 'Invalid ModifyArtist request: ' + JSON.stringify(req.body),
@ -10,26 +10,95 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res
}; };
throw e; throw e;
} }
const reqObject:api.ModifyArtistRequest = req.body; const reqObject: api.ModifyArtistRequest = req.body;
console.log("Modify Artist:", reqObject);
await models.Artist.findAll({ await knex.transaction(async (trx) => {
where: { id: req.params.id } try {
}) const artistId = parseInt(req.params.id);
.then(async (artists: any[]) => {
if (artists.length != 1) { // Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('artists_tags')
.whereIn('id', reqObject.tagIds)
.then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return [] })();
// Start retrieving the artist itself.
const artistPromise = trx.select('id')
.from('artists')
.where({ id: artistId })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Wait for the requests to finish.
var [artist, tags] = await Promise.all([artistPromise, tagIdsPromise]);;
// Check that we found all objects we need.
if ((reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
!artist) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'There is no artist with id ' + req.params.id + '.', internalMessage: 'Not all artists and/or tags exist for ModifyArtist request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
}; };
throw e; throw e;
} }
let artist = artists[0];
artist.name = reqObject.name; // Modify the artist.
if(reqObject.storeLinks) { artist.setStoreLinks(reqObject.storeLinks) }; const modifyArtistPromise = trx('artists')
await artist.save(); .where({ 'id': artistId })
.update({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
}) })
.then(() => {
res.status(200).send({}); // Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = reqObject.tagIds ?
trx('artists_tags')
.where({ 'artistId': artistId })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete() :
(async () => undefined)();
// Link new tags.
// TODO: test this!
const addTags = trx('artists_tags')
.where({ 'artistId': artistId })
.then((ts: any) => ts.map((t: any) => t['tagId']))
.then((doneTagIds: number[]) => {
// Get the set of tags that are not yet linked
const toLink = tags.filter((id: number) => {
return !doneTagIds.includes(id);
});
const insertObjects = toLink.map((tagId: number) => {
return {
tagId: tagId,
artistId: artistId,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('artists_tags').insert(obj)
)
);
});
// Wait for all operations to finish.
await Promise.all([
modifyArtistPromise,
removeUnlinkedTags,
addTags
]);
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
}) })
.catch(catchUnhandledErrors);
} }

@ -1,9 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
const { Op } = require("sequelize"); import Knex from 'knex';
export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifySongRequest(req)) { if (!api.checkModifySongRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ModifySong request: ' + JSON.stringify(req.body), internalMessage: 'Invalid ModifySong request: ' + JSON.stringify(req.body),
@ -13,53 +12,177 @@ export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res:
} }
const reqObject: api.ModifySongRequest = req.body; const reqObject: api.ModifySongRequest = req.body;
// Start retrieving the artist instances to link the song to. console.log("Modify Song:", reqObject);
var artistInstancesPromise = reqObject.artistIds && models.Artist.findAll({
where: {
id: {
[Op.in]: reqObject.artistIds
}
}
});
// Start retrieving the album instances to link the song to. await knex.transaction(async (trx) => {
var albumInstancesPromise = reqObject.albumIds && models.Album.findAll({ try {
where: { // Start retrieving artists.
id: { const artistIdsPromise = reqObject.artistIds ?
[Op.in]: reqObject.albumIds trx.select('artistId')
} .from('songs_artists')
} .whereIn('id', reqObject.artistIds)
}); .then((as: any) => as.map((a: any) => a['artistId'])) :
(async () => { return [] })();
// Start retrieving the tag instances to link the song to. // Start retrieving tags.
var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({ const tagIdsPromise = reqObject.tagIds ?
where: { trx.select('id')
id: { .from('songs_tags')
[Op.in]: reqObject.tagIds .whereIn('id', reqObject.tagIds)
} .then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return [] })();
// 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 [] })();
// Start retrieving the song itself.
const songPromise = trx.select('id')
.from('songs')
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : 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.
const modifySongPromise = trx('songs')
.where({ 'id': req.params.id })
.update({
title: reqObject.title,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
})
// Remove unlinked artists.
// TODO: test this!
const removeUnlinkedArtists = trx('artists_songs')
.where({ 'songId': req.params.id })
.whereNotIn('artistId', reqObject.artistIds || [])
.delete();
// Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = trx('songs_tags')
.where({ 'songId': req.params.id })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete();
// Remove unlinked albums.
// TODO: test this!
const removeUnlinkedAlbums = trx('songs_albums')
.where({ 'songId': req.params.id })
.whereNotIn('albumId', reqObject.albumIds || [])
.delete();
// Link new artists.
// TODO: test this!
const addArtists = trx('artists_songs')
.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,
}
})
// Start retrieving the song to modify. // Link them
var songInstancePromise = models.Song.findAll({ return Promise.all(
where: { insertObjects.map((obj: any) =>
id: req.params.id trx('artists_songs').insert(obj)
)
);
})
// Link new tags.
// TODO: test this!
const addTags = 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)
)
);
})
// Link new albums.
// TODO: test this!
const addAlbums = 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,
}
})
// Upon finish retrieving artists and albums, modify the song. // Link them
await Promise.all([artistInstancesPromise, albumInstancesPromise, tagInstancesPromise, songInstancePromise]) return Promise.all(
.then(async (values: any) => { insertObjects.map((obj: any) =>
var [artists, albums, tags, song] = values; trx('songs_albums').insert(obj)
if (reqObject.artistIds) { song.setArtists(artists) }; )
if (reqObject.albumIds) { song.setAlbums(albums) }; );
if (reqObject.tagIds) { song.setTags(tags) };
if (reqObject.title) { song.setTitle(reqObject.title) };
if (reqObject.storeLinks) { song.setStoreIds(reqObject.storeLinks) };
await song.save();
}) })
.then(() => {
res.status(200).send({}); // 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();
}
}) })
.catch(catchUnhandledErrors);
} }

@ -1,10 +1,9 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import tag from '../models/tag'; import Knex from 'knex';
export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifySongRequest(req)) { if (!api.checkModifyTagRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid ModifyTag request: ' + JSON.stringify(req.body), internalMessage: 'Invalid ModifyTag request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
@ -13,39 +12,51 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a
} }
const reqObject: api.ModifyTagRequest = req.body; const reqObject: api.ModifyTagRequest = req.body;
const getTag = async (id:Number) => { console.log("Modify Tag:", reqObject);
const tag = await models.Tag.findAll({
where: { await knex.transaction(async (trx) => {
id: id try {
} // Start retrieving the parent tag.
}); const parentTagPromise = reqObject.parentId ?
if(tag.length != 1) { trx.select('id')
.from('tags')
.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({ 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 = { const e: EndpointError = {
internalMessage: 'There is no tag with id ' + id + '.', internalMessage: 'Tag or parent does not exist for ModifyTag request: ' + JSON.stringify(req.body),
httpStatus: 400 httpStatus: 400
}; };
throw e; throw e;
} }
return tag[0];
}
// If applicable, start retrieving the new parent tag. // Modify the tag.
const maybeNewParentPromise:Promise<any>|Promise<undefined> = await trx('tags')
(reqObject.parentId) ? getTag(reqObject.parentId) : (async () => { return undefined })(); .where({ 'id': req.params.id })
.update({
name: reqObject.name,
parentId: reqObject.parentId || null,
})
// Start retrieving the tag to modify. // Respond to the request.
var tagInstancePromise: Promise<any> = getTag(req.params.id); res.status(200).send();
// Upon finish retrieving artists and albums, modify the song. } catch (e) {
await Promise.all([maybeNewParentPromise, tagInstancePromise]) catchUnhandledErrors(e);
.then(async (values: any) => { trx.rollback();
var [maybeParent, tag] = values; }
if(reqObject.name) { tag.setName(reqObject.name) };
if(reqObject.parentId) { tag.setParent(maybeParent) };
await tag.save();
})
.then(() => {
res.status(200).send({});
}) })
.catch(catchUnhandledErrors);
} }

@ -1,5 +1,3 @@
const models = require('../models');
const { Op } = require("sequelize");
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
@ -9,87 +7,87 @@ enum QueryType {
Tag, Tag,
} }
const sequelizeOps: any = { // const sequelizeOps: any = {
[api.QueryFilterOp.Eq]: Op.eq, // [api.QueryFilterOp.Eq]: Op.eq,
[api.QueryFilterOp.Ne]: Op.ne, // [api.QueryFilterOp.Ne]: Op.ne,
[api.QueryFilterOp.In]: Op.in, // [api.QueryFilterOp.In]: Op.in,
[api.QueryFilterOp.NotIn]: Op.notIn, // [api.QueryFilterOp.NotIn]: Op.notIn,
[api.QueryFilterOp.Like]: Op.like, // [api.QueryFilterOp.Like]: Op.like,
[api.QueryElemOp.And]: Op.and, // [api.QueryElemOp.And]: Op.and,
[api.QueryElemOp.Or]: Op.or, // [api.QueryElemOp.Or]: Op.or,
}; // };
const sequelizeProps: any = { // const sequelizeProps: any = {
[QueryType.Song]: { // [QueryType.Song]: {
[api.QueryElemProperty.songTitle]: "title", // [api.QueryElemProperty.songTitle]: "title",
[api.QueryElemProperty.songId]: "id", // [api.QueryElemProperty.songId]: "id",
[api.QueryElemProperty.artistName]: "$Artists.name$", // [api.QueryElemProperty.artistName]: "$Artists.name$",
[api.QueryElemProperty.artistId]: "$Artists.id$", // [api.QueryElemProperty.artistId]: "$Artists.id$",
[api.QueryElemProperty.albumName]: "$Albums.name$", // [api.QueryElemProperty.albumName]: "$Albums.name$",
}, // },
[QueryType.Artist]: { // [QueryType.Artist]: {
[api.QueryElemProperty.songTitle]: "$Songs.title$", // [api.QueryElemProperty.songTitle]: "$Songs.title$",
[api.QueryElemProperty.songId]: "$Songs.id$", // [api.QueryElemProperty.songId]: "$Songs.id$",
[api.QueryElemProperty.artistName]: "name", // [api.QueryElemProperty.artistName]: "name",
[api.QueryElemProperty.artistId]: "id", // [api.QueryElemProperty.artistId]: "id",
[api.QueryElemProperty.albumName]: "$Albums.name$", // [api.QueryElemProperty.albumName]: "$Albums.name$",
}, // },
[QueryType.Tag]: { // [QueryType.Tag]: {
[api.QueryElemProperty.songTitle]: "$Songs.title$", // [api.QueryElemProperty.songTitle]: "$Songs.title$",
[api.QueryElemProperty.songId]: "$Songs.id$", // [api.QueryElemProperty.songId]: "$Songs.id$",
[api.QueryElemProperty.artistName]: "$Artists.name$", // [api.QueryElemProperty.artistName]: "$Artists.name$",
[api.QueryElemProperty.artistId]: "$Artists.id$", // [api.QueryElemProperty.artistId]: "$Artists.id$",
[api.QueryElemProperty.albumName]: "$Albums.name$", // [api.QueryElemProperty.albumName]: "$Albums.name$",
} // }
}; // };
const sequelizeOrderColumns: any = { // const sequelizeOrderColumns: any = {
[QueryType.Song]: { // [QueryType.Song]: {
[api.OrderByType.Name]: 'title', // [api.OrderByType.Name]: 'title',
[api.OrderByType.ArtistRanking]: '$Rankings.rank$', // [api.OrderByType.ArtistRanking]: '$Rankings.rank$',
[api.OrderByType.TagRanking]: '$Rankings.rank$', // [api.OrderByType.TagRanking]: '$Rankings.rank$',
}, // },
[QueryType.Artist]: { // [QueryType.Artist]: {
[api.OrderByType.Name]: 'name' // [api.OrderByType.Name]: 'name'
}, // },
[QueryType.Tag]: { // [QueryType.Tag]: {
[api.OrderByType.Name]: 'name' // [api.OrderByType.Name]: 'name'
}, // },
} // }
// Returns the "where" clauses for Sequelize, per object type. // // Returns the "where" clauses for Sequelize, per object type.
const getSequelizeWhere = (queryElem: api.QueryElem, type: QueryType) => { // const getSequelizeWhere = (queryElem: api.QueryElem, type: QueryType) => {
var where: any = { // var where: any = {
[Op.and]: [] // [Op.and]: []
}; // };
if (queryElem.prop && queryElem.propOperator && queryElem.propOperand) { // if (queryElem.prop && queryElem.propOperator && queryElem.propOperand) {
// Visit a filter-like subquery leaf. // // Visit a filter-like subquery leaf.
where[Op.and].push({ // where[Op.and].push({
[sequelizeProps[type][queryElem.prop]]: { // [sequelizeProps[type][queryElem.prop]]: {
[sequelizeOps[queryElem.propOperator]]: queryElem.propOperand // [sequelizeOps[queryElem.propOperator]]: queryElem.propOperand
} // }
}); // });
} // }
if (queryElem.childrenOperator && queryElem.children) { // if (queryElem.childrenOperator && queryElem.children) {
// Recursively visit a nested subquery. // // Recursively visit a nested subquery.
const children = queryElem.children.map((child: api.QueryElem) => getSequelizeWhere(child, type)); // const children = queryElem.children.map((child: api.QueryElem) => getSequelizeWhere(child, type));
where[Op.and].push({ // where[Op.and].push({
[sequelizeOps[queryElem.childrenOperator]]: children // [sequelizeOps[queryElem.childrenOperator]]: children
}); // });
} // }
return where; // return where;
} // }
function getSequelizeOrder(order: api.Ordering, type: QueryType) { // function getSequelizeOrder(order: api.Ordering, type: QueryType) {
const ascstring = order.ascending ? 'ASC' : 'DESC'; // const ascstring = order.ascending ? 'ASC' : 'DESC';
return [ // return [
[ sequelizeOrderColumns[type][order.orderBy.type], ascstring ] // [ sequelizeOrderColumns[type][order.orderBy.type], ascstring ]
]; // ];
} // }
export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkQueryRequest(req.body)) { if (!api.checkQueryRequest(req.body)) {
@ -101,93 +99,95 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any)
} }
const reqObject: api.QueryRequest = req.body; const reqObject: api.QueryRequest = req.body;
try { // try {
const songLimit = reqObject.offsetsLimits.songLimit; // const songLimit = reqObject.offsetsLimits.songLimit;
const songOffset = reqObject.offsetsLimits.songOffset; // const songOffset = reqObject.offsetsLimits.songOffset;
const tagLimit = reqObject.offsetsLimits.tagLimit; // const tagLimit = reqObject.offsetsLimits.tagLimit;
const tagOffset = reqObject.offsetsLimits.tagOffset; // const tagOffset = reqObject.offsetsLimits.tagOffset;
const artistLimit = reqObject.offsetsLimits.artistLimit; // const artistLimit = reqObject.offsetsLimits.artistLimit;
const artistOffset = reqObject.offsetsLimits.artistOffset; // const artistOffset = reqObject.offsetsLimits.artistOffset;
const songs = (songLimit && songLimit > 0) && await models.Song.findAll({ // const songs = (songLimit && songLimit > 0) && await models.Song.findAll({
// NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938. // // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// Custom pagination is implemented before responding. // // Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query, QueryType.Song), // where: getSequelizeWhere(reqObject.query, QueryType.Song),
order: getSequelizeOrder(reqObject.ordering, QueryType.Song), // order: getSequelizeOrder(reqObject.ordering, QueryType.Song),
include: [ models.Artist, models.Album, models.Tag, models.Ranking ], // include: [ models.Artist, models.Album, models.Tag, models.Ranking ],
//limit: reqObject.limit, // //limit: reqObject.limit,
//offset: reqObject.offset, // //offset: reqObject.offset,
}) // })
const artists = (artistLimit && artistLimit > 0) && await models.Artist.findAll({ // const artists = (artistLimit && artistLimit > 0) && await models.Artist.findAll({
// NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938. // // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// Custom pagination is implemented before responding. // // Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query, QueryType.Artist), // where: getSequelizeWhere(reqObject.query, QueryType.Artist),
order: getSequelizeOrder(reqObject.ordering, QueryType.Artist), // order: getSequelizeOrder(reqObject.ordering, QueryType.Artist),
include: [models.Song, models.Album, models.Tag], // include: [models.Song, models.Album, models.Tag],
//limit: reqObject.limit, // //limit: reqObject.limit,
//offset: reqObject.offset, // //offset: reqObject.offset,
}) // })
const tags = (tagLimit && tagLimit > 0) && await models.Tag.findAll({ // const tags = (tagLimit && tagLimit > 0) && await models.Tag.findAll({
// NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938. // // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// Custom pagination is implemented before responding. // // Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query, QueryType.Tag), // where: getSequelizeWhere(reqObject.query, QueryType.Tag),
order: getSequelizeOrder(reqObject.ordering, QueryType.Tag), // order: getSequelizeOrder(reqObject.ordering, QueryType.Tag),
include: [models.Song, models.Album, models.Artist], // include: [models.Song, models.Album, models.Artist],
//limit: reqObject.limit, // //limit: reqObject.limit,
//offset: reqObject.offset, // //offset: reqObject.offset,
}) // })
const response: api.QueryResponse = { // const response: api.QueryResponse = {
songs: ((songLimit || -1) <= 0) ? [] : await Promise.all(songs.map(async (song: any) => { // songs: ((songLimit || -1) <= 0) ? [] : await Promise.all(songs.map(async (song: any) => {
const artists = song.getArtists(); // const artists = song.getArtists();
const tags = song.getTags(); // const tags = song.getTags();
const rankings = song.getRankings(); // const rankings = song.getRankings();
return <api.SongDetails>{ // return <api.SongDetails>{
songId: song.id, // songId: song.id,
title: song.title, // title: song.title,
storeLinks: song.storeLinks, // storeLinks: song.storeLinks,
artists: (await artists).map((artist: any) => { // artists: (await artists).map((artist: any) => {
return <api.ArtistDetails>{ // return <api.ArtistDetails>{
artistId: artist.id, // artistId: artist.id,
name: artist.name, // name: artist.name,
} // }
}), // }),
tags: (await tags).map((tag: any) => { // tags: (await tags).map((tag: any) => {
return <api.TagDetails>{ // return <api.TagDetails>{
tagId: tag.id, // tagId: tag.id,
name: tag.name, // name: tag.name,
} // }
}), // }),
rankings: await (await rankings).map(async (ranking: any) => { // rankings: await (await rankings).map(async (ranking: any) => {
const maybeTagContext: api.TagDetails | undefined = await ranking.getTagContext(); // const maybeTagContext: api.TagDetails | undefined = await ranking.getTagContext();
const maybeArtistContext: api.ArtistDetails | undefined = await ranking.getArtistContext(); // const maybeArtistContext: api.ArtistDetails | undefined = await ranking.getArtistContext();
const maybeContext = maybeTagContext || maybeArtistContext; // const maybeContext = maybeTagContext || maybeArtistContext;
return <api.RankingDetails>{ // return <api.RankingDetails>{
rankingId: ranking.id, // rankingId: ranking.id,
type: api.ItemType.Song, // type: api.ItemType.Song,
rankedId: song.id, // rankedId: song.id,
context: maybeContext, // context: maybeContext,
value: ranking.value, // value: ranking.value,
} // }
}) // })
}; // };
}).slice(songOffset || 0, (songOffset || 0) + (songLimit || 10))), // }).slice(songOffset || 0, (songOffset || 0) + (songLimit || 10))),
// TODO: custom pagination due to bug mentioned above // // TODO: custom pagination due to bug mentioned above
artists: ((artistLimit || -1) <= 0) ? [] : await Promise.all(artists.map(async (artist: any) => { // artists: ((artistLimit || -1) <= 0) ? [] : await Promise.all(artists.map(async (artist: any) => {
return <api.ArtistDetails>{ // return <api.ArtistDetails>{
artistId: artist.id, // artistId: artist.id,
name: artist.name, // name: artist.name,
}; // };
}).slice(artistOffset || 0, (artistOffset || 0) + (artistLimit || 10))), // }).slice(artistOffset || 0, (artistOffset || 0) + (artistLimit || 10))),
tags: ((tagLimit || -1) <= 0) ? [] : await Promise.all(tags.map(async (tag: any) => { // tags: ((tagLimit || -1) <= 0) ? [] : await Promise.all(tags.map(async (tag: any) => {
return <api.TagDetails>{ // return <api.TagDetails>{
tagId: tag.id, // tagId: tag.id,
name: tag.name, // name: tag.name,
}; // };
}).slice(tagOffset || 0, (tagOffset || 0) + (tagLimit || 10))), // }).slice(tagOffset || 0, (tagOffset || 0) + (tagLimit || 10))),
}; // };
res.send(response); // res.send(response);
} catch (e) { // } catch (e) {
catchUnhandledErrors(e); // catchUnhandledErrors(e);
} // }
throw "NOTIMPLEMENTED";
} }

@ -1,8 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkSongDetailsRequest(req)) { if (!api.checkSongDetailsRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid SongDetails request: ' + JSON.stringify(req.body), internalMessage: 'Invalid SongDetails request: ' + JSON.stringify(req.body),
@ -12,29 +12,50 @@ export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res:
} }
try { try {
const songs = await models.Song.findAll({ const tagIdsPromise: Promise<number[]> = knex.select('tagId')
include: [models.Artist, models.Album, models.Tag], .from('songs_tags')
where: { .where({ 'songId': req.params.id })
id: req.params.id .then((ts: any) => {
} return Array.from(new Set(
}); ts.map((tag: any) => tag['tagId'])
if (songs.length != 1) { ));
const e: EndpointError = { })
internalMessage: 'There is no song with id ' + req.params.id + '.',
httpStatus: 400 const albumIdsPromise: Promise<number[]> = knex.select('albumId')
}; .from('songs_albums')
throw e; .where({ 'songId': req.params.id })
} .then((as: any) => {
let song = songs[0]; 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({ 'id': req.params.id })
.then((ss: any) => ss[0])
const [tags, albums, artists, song] =
await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]);
const response: api.SongDetailsResponse = { const response: api.SongDetailsResponse = {
title: song.title, title: song.title,
artistIds: song.Artists.map((artist: any) => artist.id), tagIds: tags,
albumIds: song.Albums.map((album: any) => album.id), artistIds: artists,
tagIds: song.Tags.map((tag: any) => tag.id), albumIds: albums,
storeLinks: song.storeLinks, storeLinks: JSON.parse(song.storeLinks),
} }
await res.send(response); await res.send(response);
} catch (e) { } catch (e) {
catchUnhandledErrors(e); catchUnhandledErrors(e)
} }
} }

@ -1,8 +1,8 @@
const models = require('../models');
import * as api from '../../client/src/api'; import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => { export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkTagDetailsRequest(req)) { if (!api.checkTagDetailsRequest(req)) {
const e: EndpointError = { const e: EndpointError = {
internalMessage: 'Invalid TagDetails request: ' + JSON.stringify(req.body), internalMessage: 'Invalid TagDetails request: ' + JSON.stringify(req.body),
@ -11,29 +11,18 @@ export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res:
throw e; throw e;
} }
await models.Tag.findAll({ try {
where: { const results = await knex.select(['id', 'name', 'parentId'])
id: req.params.id .from('tags')
}, .where({ 'id': req.params.id });
include: [{
model: models.Tag, const response: api.TagDetailsResponse = {
as: 'parent' name: results[0].name,
}] parentId: results[0].parentId,
}) }
.then((tags: any[]) => {
if (tags.length != 1) { await res.send(response);
const e: EndpointError = { } catch (e) {
internalMessage: 'There is no tag with id ' + req.params.id + '.', catchUnhandledErrors(e)
httpStatus: 400
};
throw e;
} }
let tag = tags[0];
var response: api.TagDetailsResponse = {
name: tag.name,
};
if(tag.parent) { response['parentId'] = tag.parent.id; }
res.send(response);
})
.catch(catchUnhandledErrors);
} }

@ -1,4 +1,6 @@
export type EndpointHandler = (req: any, res: any) => Promise<void>; import Knex from 'knex';
export type EndpointHandler = (req: any, res: any, knex: Knex) => Promise<void>;
export interface EndpointError { export interface EndpointError {
internalMessage: String; internalMessage: String;

@ -0,0 +1,3 @@
const environment = process.env.ENVIRONMENT || 'development'
const config = require('../knexfile.js')[environment];
export default require('knex')(config);

@ -0,0 +1,44 @@
// Update with your config settings.
export default {
development: {
client: "sqlite3",
connection: {
filename: "./dev.sqlite3"
}
},
// staging: {
// client: "postgresql",
// connection: {
// database: "my_db",
// user: "username",
// password: "password"
// },
// pool: {
// min: 2,
// max: 10
// },
// migrations: {
// tableName: "knex_migrations"
// }
// },
// production: {
// client: "postgresql",
// connection: {
// database: "my_db",
// user: "username",
// password: "password"
// },
// pool: {
// min: 2,
// max: 10
// },
// migrations: {
// tableName: "knex_migrations"
// }
// }
};

@ -0,0 +1,118 @@
import * as Knex from "knex";
export async function up(knex: Knex): Promise<void> {
// Songs table.
await knex.schema.createTable(
'songs',
(table: any) => {
table.increments('id');
table.string('title');
table.json('storeLinks')
}
)
// Artists table.
await knex.schema.createTable(
'artists',
(table: any) => {
table.increments('id');
table.string('name');
table.json('storeLinks');
}
)
// Albums table.
await knex.schema.createTable(
'albums',
(table: any) => {
table.increments('id');
table.string('name');
table.json('storeLinks');
}
)
// Tags table.
await knex.schema.createTable(
'tags',
(table: any) => {
table.increments('id');
table.string('name');
table.integer('parentId');
}
)
// Songs <-> Artists
await knex.schema.createTable(
'songs_artists',
(table: any) => {
table.increments('id');
table.integer('songId');
table.integer('artistId');
}
)
// Songs <-> Albums
await knex.schema.createTable(
'songs_albums',
(table: any) => {
table.increments('id');
table.integer('songId');
table.integer('albumId');
}
)
// Songs <-> Tags
await knex.schema.createTable(
'songs_tags',
(table: any) => {
table.increments('id');
table.integer('songId');
table.integer('tagId');
}
)
// Artists <-> Tags
await knex.schema.createTable(
'artists_tags',
(table: any) => {
table.increments('id');
table.integer('artistId');
table.integer('tagId');
}
)
// Albums <-> Tags
await knex.schema.createTable(
'albums_tags',
(table: any) => {
table.increments('id');
table.integer('tagId');
table.integer('albumId');
}
)
// Artists <-> Albums
await knex.schema.createTable(
'artists_albums',
(table: any) => {
table.increments('id');
table.integer('artistId');
table.integer('albumId');
}
)
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable('songs');
await knex.schema.dropTable('artists');
await knex.schema.dropTable('albums');
await knex.schema.dropTable('tags');
await knex.schema.dropTable('songs_artists');
await knex.schema.dropTable('songs_albums');
await knex.schema.dropTable('songs_tags');
await knex.schema.dropTable('artists_tags');
await knex.schema.dropTable('albums_tags');
}

@ -1,14 +0,0 @@
module.exports = (sequelize, DataTypes) => {
var Album = sequelize.define('Album', {
name: DataTypes.STRING,
storeLinks: DataTypes.JSON,
});
Album.associate = function (models) {
models.Album.belongsToMany(models.Song, { through: "SongAlbums" });
models.Album.belongsToMany(models.Artist, { through: "AlbumArtists" });
models.Album.belongsToMany(models.Tag, { through: 'AlbumTags' });
};
return Album;
};

@ -1,14 +0,0 @@
module.exports = (sequelize, DataTypes) => {
var Artist = sequelize.define('Artist', {
name: DataTypes.STRING,
storeLinks: DataTypes.JSON,
});
Artist.associate = function (models) {
models.Artist.belongsToMany(models.Song, { through: "SongArtists" });
models.Artist.belongsToMany(models.Album, { through: "AlbumArtists" });
models.Artist.belongsToMany(models.Tag, { through: 'ArtistTags' });
};
return Artist;
};

@ -1,37 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

@ -1,13 +0,0 @@
module.exports = (sequelize, DataTypes) => {
var Ranking = sequelize.define('Ranking', {
rank: DataTypes.DOUBLE
});
Ranking.associate = function (models) {
models.Ranking.hasOne(models.Tag, { as: 'tagContext' });
models.Ranking.hasOne(models.Artist, { as: 'artistContext' });
models.Ranking.belongsToMany(models.Song, { through: 'SongRankings' });
};
return Ranking;
};

@ -1,15 +0,0 @@
module.exports = (sequelize, DataTypes) => {
var Song = sequelize.define('Song', {
title: DataTypes.STRING,
storeLinks: DataTypes.JSON,
});
Song.associate = function (models) {
models.Song.belongsToMany(models.Artist, { through: "SongArtists" });
models.Song.belongsToMany(models.Album, { through: "SongAlbums" });
models.Song.belongsToMany(models.Tag, { through: 'SongTags' });
models.Song.belongsToMany(models.Ranking, { through: 'SongRankings'});
};
return Song;
};

@ -1,14 +0,0 @@
module.exports = (sequelize, DataTypes) => {
var Tag = sequelize.define('Tag', {
name: DataTypes.STRING,
});
Tag.associate = function (models) {
models.Tag.hasOne(models.Tag, { as: 'parent' });
models.Tag.belongsToMany(models.Artist, { through: 'ArtistTags' });
models.Tag.belongsToMany(models.Album, { through: 'AlbumTags' });
models.Tag.belongsToMany(models.Song, { through: 'SongTags' });
};
return Tag;
};

@ -12,8 +12,7 @@
"chai-http": "^4.3.0", "chai-http": "^4.3.0",
"express": "^4.16.4", "express": "^4.16.4",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"sequelize": "^6.3.0", "knex": "^0.21.5",
"sequelize-cli": "^6.2.0",
"sqlite3": "^5.0.0", "sqlite3": "^5.0.0",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "~3.7.2" "typescript": "~3.7.2"

@ -1,17 +1,16 @@
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser');
const models = require('./models'); const environment = process.env.ENVIRONMENT || 'development'
const knexConfig = require('knexfile.js')[environment];
const knex = require('knex')(knexConfig);
import { SetupApp } from './app'; import { SetupApp } from './app';
const app = express(); const app = express();
SetupApp(app); SetupApp(app, knex);
models.sequelize.sync().then(() => { const port = process.env.PORT || 5000;
// TODO: configurable port app.listen(port, () => console.log(`Listening on port ${port}`));
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Listening on port ${port}`));
})
export { } export { }

@ -1,7 +1,6 @@
const chai = require('chai'); const chai = require('chai');
const chaiHttp = require('chai-http'); const chaiHttp = require('chai-http');
const express = require('express'); const express = require('express');
const models = require('../../../models');
import { SetupApp } from '../../../app'; import { SetupApp } from '../../../app';
import { expect } from 'chai'; import { expect } from 'chai';
import * as helpers from './helpers'; import * as helpers from './helpers';
@ -9,8 +8,7 @@ import * as helpers from './helpers';
async function init() { async function init() {
chai.use(chaiHttp); chai.use(chaiHttp);
const app = express(); const app = express();
SetupApp(app); SetupApp(app, await helpers.initTestDB());
await models.sequelize.sync({ force: true });
return app; return app;
} }

@ -1,7 +1,6 @@
const chai = require('chai'); const chai = require('chai');
const chaiHttp = require('chai-http'); const chaiHttp = require('chai-http');
const express = require('express'); const express = require('express');
const models = require('../../../models');
import { SetupApp } from '../../../app'; import { SetupApp } from '../../../app';
import { expect } from 'chai'; import { expect } from 'chai';
import * as helpers from './helpers'; import * as helpers from './helpers';
@ -9,8 +8,7 @@ import * as helpers from './helpers';
async function init() { async function init() {
chai.use(chaiHttp); chai.use(chaiHttp);
const app = express(); const app = express();
SetupApp(app); SetupApp(app, await helpers.initTestDB());
await models.sequelize.sync({ force: true });
return app; return app;
} }
@ -32,32 +30,23 @@ describe('POST /artist with no name', () => {
describe('POST /artist with a correct request', () => { describe('POST /artist with a correct request', () => {
it('should succeed', done => { it('should succeed', done => {
init().then((app) => { init().then((app) => {
chai var req = chai.request(app).keepOpen();
.request(app) helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.post('/artist') .then(() => helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] }))
.send({ .then(req.close)
name: "MyArtist" .then(done);
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
}); });
}); });
}); });
describe('PUT /artist on nonexistent artist', () => { describe('PUT /artist on nonexistent artist', () => {
it('should fail', done => { it('should fail', done => {
init().then((app) => { init().then((app) => {
chai chai
.request(app) .request(app)
.put('/artist/1') .put('/artist/0')
.send({ .send({
id: 1, id: 0,
name: "NewArtistName" name: "NewArtistName"
}) })
.then((res) => { .then((res) => {

@ -1,7 +1,6 @@
const chai = require('chai'); const chai = require('chai');
const chaiHttp = require('chai-http'); const chaiHttp = require('chai-http');
const express = require('express'); const express = require('express');
const models = require('../../../models');
import { SetupApp } from '../../../app'; import { SetupApp } from '../../../app';
import { expect } from 'chai'; import { expect } from 'chai';
import * as helpers from './helpers'; import * as helpers from './helpers';
@ -9,8 +8,7 @@ import * as helpers from './helpers';
async function init() { async function init() {
chai.use(chaiHttp); chai.use(chaiHttp);
const app = express(); const app = express();
SetupApp(app); SetupApp(app, await helpers.initTestDB());;
await models.sequelize.sync({ force: true });
return app; return app;
} }

@ -1,7 +1,6 @@
const chai = require('chai'); const chai = require('chai');
const chaiHttp = require('chai-http'); const chaiHttp = require('chai-http');
const express = require('express'); const express = require('express');
const models = require('../../../models');
import { SetupApp } from '../../../app'; import { SetupApp } from '../../../app';
import { expect } from 'chai'; import { expect } from 'chai';
import * as helpers from './helpers'; import * as helpers from './helpers';
@ -9,8 +8,7 @@ import * as helpers from './helpers';
async function init() { async function init() {
chai.use(chaiHttp); chai.use(chaiHttp);
const app = express(); const app = express();
SetupApp(app); SetupApp(app, await helpers.initTestDB());
await models.sequelize.sync({ force: true });
return app; return app;
} }

@ -1,7 +1,6 @@
const chai = require('chai'); const chai = require('chai');
const chaiHttp = require('chai-http'); const chaiHttp = require('chai-http');
const express = require('express'); const express = require('express');
const models = require('../../../models');
import { SetupApp } from '../../../app'; import { SetupApp } from '../../../app';
import { expect } from 'chai'; import { expect } from 'chai';
import * as helpers from './helpers'; import * as helpers from './helpers';
@ -9,8 +8,7 @@ import * as helpers from './helpers';
async function init() { async function init() {
chai.use(chaiHttp); chai.use(chaiHttp);
const app = express(); const app = express();
SetupApp(app); SetupApp(app, await helpers.initTestDB());
await models.sequelize.sync({ force: true });
return app; return app;
} }

@ -1,5 +1,11 @@
import { expect } from "chai"; import { expect } from "chai";
export async function initTestDB() {
const knex = await require('knex')({ client: 'sqlite3', connection: ':memory:'})
await knex.migrate.latest();
return knex;
}
export async function createSong( export async function createSong(
req, req,
props = { title: "Song" }, props = { title: "Song" },

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save