Finished a refactoring of the resource type system. Runs again. Patch requests fail though.

editsong
Sander Vocke 4 years ago
parent 0758ae2564
commit b27176ae66
  1. 18
      client/src/api/endpoints/data.ts
  2. 14
      client/src/api/endpoints/query.ts
  3. 79
      client/src/api/endpoints/resources.ts
  4. 233
      client/src/api/types/resources.ts
  5. 54
      client/src/components/tables/ResultsTable.tsx
  6. 4
      client/src/components/windows/Windows.tsx
  7. 11
      client/src/components/windows/album/AlbumWindow.tsx
  8. 10
      client/src/components/windows/artist/ArtistWindow.tsx
  9. 9
      client/src/components/windows/query/QueryWindow.tsx
  10. 10
      client/src/components/windows/tag/TagWindow.tsx
  11. 6
      client/src/components/windows/track/TrackWindow.tsx
  12. 3
      client/src/lib/backend/albums.tsx
  13. 3
      client/src/lib/backend/artists.tsx
  14. 8
      client/src/lib/backend/queries.tsx
  15. 3
      client/src/lib/backend/tracks.tsx
  16. 28
      client/src/lib/trackGetters.tsx
  17. 36
      server/db/Album.ts
  18. 38
      server/db/Artist.ts
  19. 28
      server/db/Data.ts
  20. 20
      server/db/Query.ts
  21. 27
      server/db/Tag.ts
  22. 43
      server/db/Track.ts
  23. 1
      server/endpoints/Album.ts

@ -7,17 +7,17 @@
// Upon import, they might be replaced, and upon export, they might be randomly // Upon import, they might be replaced, and upon export, they might be randomly
// generated. // generated.
import { AlbumWithRefsWithId, ArtistWithRefsWithId, isAlbumWithRefs, isArtistWithRefs, isTagWithRefs, isTrackBaseWithRefs, isTrackWithRefs, TagWithRefsWithId, TrackWithRefsWithId } from "../types/resources"; import { Album, Id, AlbumRefs, Artist, ArtistRefs, Tag, TagRefs, Track, TrackRefs, isTrackRefs, isAlbumRefs, isArtistRefs, isTagRefs } from "../types/resources";
// The import/export DB format is just a set of lists of objects. // The import/export DB format is just a set of lists of objects.
// Each object has an ID and references others by ID. // Each object has an ID and references others by ID.
// Any object referenced by ID also has a reverse reference. // Any object referenced by ID also has a reverse reference.
// In other words, if A references B, B must also reference A. // In other words, if A references B, B must also reference A.
export interface DBDataFormat { export interface DBDataFormat {
tracks: TrackWithRefsWithId[], tracks: (Track & Id & TrackRefs)[],
albums: AlbumWithRefsWithId[], albums: (Album & Id & AlbumRefs)[],
artists: ArtistWithRefsWithId[], artists: (Artist & Id & ArtistRefs)[],
tags: TagWithRefsWithId[], tags: (Tag & Id & TagRefs)[],
} }
// Get a full export of a user's database (GET). // Get a full export of a user's database (GET).
@ -40,16 +40,16 @@ export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
'artists' in v && 'artists' in v &&
'tags' in v && 'tags' in v &&
v.tracks.reduce((prev: boolean, cur: any) => { v.tracks.reduce((prev: boolean, cur: any) => {
return prev && isTrackWithRefs(cur); return prev && isTrackRefs(cur);
}, true) && }, true) &&
v.albums.reduce((prev: boolean, cur: any) => { v.albums.reduce((prev: boolean, cur: any) => {
return prev && isAlbumWithRefs(cur); return prev && isAlbumRefs(cur);
}, true) && }, true) &&
v.artists.reduce((prev: boolean, cur: any) => { v.artists.reduce((prev: boolean, cur: any) => {
return prev && isArtistWithRefs(cur); return prev && isArtistRefs(cur);
}, true) && }, true) &&
v.tags.reduce((prev: boolean, cur: any) => { v.tags.reduce((prev: boolean, cur: any) => {
return prev && isTagWithRefs(cur); return prev && isTagRefs(cur);
}, true); }, true);
} }

@ -1,6 +1,6 @@
// Query for items (POST). // Query for items (POST).
import { AlbumWithId, ArtistWithId, TagWithId, TrackWithId } from "../types/resources"; import { Album, Id, Artist, Tag, Track, Name, StoreLinks, TagRefs, AlbumRefs, TrackDetails, ArtistDetails } from "../types/resources";
export const QueryEndpoint = '/query'; export const QueryEndpoint = '/query';
@ -77,11 +77,15 @@ export interface QueryRequest {
} }
// Query response structure // Query response structure
export type QueryResponseTrackDetails = (Track & Name & StoreLinks & TrackDetails & Id);
export type QueryResponseArtistDetails = (Artist & Name & StoreLinks & Id);
export type QueryResponseTagDetails = (Tag & Name & TagRefs & Id);
export type QueryResponseAlbumDetails = (Album & Name & StoreLinks & Id);
export interface QueryResponse { export interface QueryResponse {
tracks: TrackWithId[] | number[] | number, // Details | IDs | count, depending on QueryResponseType tracks: QueryResponseTrackDetails[] | number[] | number, // Details | IDs | count, depending on QueryResponseType
artists: ArtistWithId[] | number[] | number, artists: QueryResponseArtistDetails[] | number[] | number,
tags: TagWithId[] | number[] | number, tags: QueryResponseTagDetails[] | number[] | number,
albums: AlbumWithId[] | number[] | number, albums: QueryResponseAlbumDetails[] | number[] | number,
} }
// Note: use -1 as an infinity limit. // Note: use -1 as an infinity limit.

@ -1,31 +1,28 @@
import { import {
Album, Album,
AlbumBaseWithRefs,
AlbumWithRefs,
Artist, Artist,
ArtistBaseWithRefs,
ArtistWithRefs,
IntegrationData, IntegrationData,
IntegrationDataWithId, IntegrationDataWithId,
IntegrationDataWithSecret, IntegrationDataWithSecret,
isAlbumBaseWithRefs,
isAlbumWithRefs,
isArtistBaseWithRefs,
isArtistWithRefs,
isIntegrationData, isIntegrationData,
isPartialIntegrationData, isPartialIntegrationData,
isTagBaseWithRefs,
isTagWithRefs,
isTrackBaseWithRefs,
isTrackWithRefs,
PartialIntegrationData, PartialIntegrationData,
Tag, Tag,
TagBaseWithRefs,
TagWithRefs,
Track, Track,
TrackBaseWithRefs, TrackRefs,
TrackWithDetails, ArtistRefs,
TrackWithRefs AlbumRefs,
TagRefs,
isTrackRefs,
isAlbumRefs,
isArtistRefs,
isTagRefs,
isName,
Name,
isTrack,
isArtist,
isAlbum,
isTag,
} from "../types/resources"; } from "../types/resources";
// The API supports RESTful access to single API resources: // The API supports RESTful access to single API resources:
@ -68,27 +65,27 @@ export type GetIntegrationResponse = IntegrationData;
// Post new track (POST). // Post new track (POST).
export const PostTrackEndpoint = "/track"; export const PostTrackEndpoint = "/track";
export type PostTrackRequest = TrackWithRefs; export type PostTrackRequest = (Track & TrackRefs & Name);
export interface PostTrackResponse { id: number }; export interface PostTrackResponse { id: number };
export const checkPostTrackRequest: (v: any) => boolean = isTrackWithRefs; export const checkPostTrackRequest: (v: any) => boolean = (v: any) => isTrackRefs(v) && isName(v);
// Post new artist (POST). // Post new artist (POST).
export const PostArtistEndpoint = "/artist"; export const PostArtistEndpoint = "/artist";
export type PostArtistRequest = ArtistWithRefs; export type PostArtistRequest = (Artist & ArtistRefs & Name);
export interface PostArtistResponse { id: number }; export interface PostArtistResponse { id: number };
export const checkPostArtistRequest: (v: any) => boolean = isArtistWithRefs; export const checkPostArtistRequest: (v: any) => boolean = (v: any) => isArtistRefs(v) && isName(v);
// Post new album (POST). // Post new album (POST).
export const PostAlbumEndpoint = "/album"; export const PostAlbumEndpoint = "/album";
export type PostAlbumRequest = AlbumWithRefs; export type PostAlbumRequest = (Album & AlbumRefs & Name);
export interface PostAlbumResponse { id: number }; export interface PostAlbumResponse { id: number };
export const checkPostAlbumRequest: (v: any) => boolean = isAlbumWithRefs; export const checkPostAlbumRequest: (v: any) => boolean = (v: any) => isAlbumRefs(v) && isName(v);
// Post new tag (POST). // Post new tag (POST).
export const PostTagEndpoint = "/tag"; export const PostTagEndpoint = "/tag";
export type PostTagRequest = TagWithRefs; export type PostTagRequest = (Tag & TagRefs & Name);
export interface PostTagResponse { id: number }; export interface PostTagResponse { id: number };
export const checkPostTagRequest: (v: any) => boolean = isTagWithRefs; export const checkPostTagRequest: (v: any) => boolean = (v: any) => isTagRefs(v) && isName(v);
// Post new integration (POST). // Post new integration (POST).
export const PostIntegrationEndpoint = "/integration"; export const PostIntegrationEndpoint = "/integration";
@ -98,27 +95,27 @@ export const checkPostIntegrationRequest: (v: any) => boolean = isIntegrationDat
// Replace track (PUT). // Replace track (PUT).
export const PutTrackEndpoint = "/track/:id"; export const PutTrackEndpoint = "/track/:id";
export type PutTrackRequest = TrackWithRefs; export type PutTrackRequest = (Track & TrackRefs & Name);
export type PutTrackResponse = void; export type PutTrackResponse = void;
export const checkPutTrackRequest: (v: any) => boolean = isTrackWithRefs; export const checkPutTrackRequest: (v: any) => boolean = (v: any) => isTrackRefs(v) && isName(v);
// Replace artist (PUT). // Replace artist (PUT).
export const PutArtistEndpoint = "/artist/:id"; export const PutArtistEndpoint = "/artist/:id";
export type PutArtistRequest = ArtistWithRefs; export type PutArtistRequest = (Artist & ArtistRefs);
export type PutArtistResponse = void; export type PutArtistResponse = void;
export const checkPutArtistRequest: (v: any) => boolean = isArtistWithRefs; export const checkPutArtistRequest: (v: any) => boolean = (v: any) => isArtistRefs(v) && isName(v);;
// Replace album (PUT). // Replace album (PUT).
export const PutAlbumEndpoint = "/album/:id"; export const PutAlbumEndpoint = "/album/:id";
export type PutAlbumRequest = AlbumWithRefs; export type PutAlbumRequest = (Album & AlbumRefs);
export type PutAlbumResponse = void; export type PutAlbumResponse = void;
export const checkPutAlbumRequest: (v: any) => boolean = isAlbumWithRefs; export const checkPutAlbumRequest: (v: any) => boolean = (v: any) => isAlbumRefs(v) && isName(v);;
// Replace tag (PUT). // Replace tag (PUT).
export const PutTagEndpoint = "/tag/:id"; export const PutTagEndpoint = "/tag/:id";
export type PutTagRequest = TagWithRefs; export type PutTagRequest = (Tag & TagRefs);
export type PutTagResponse = void; export type PutTagResponse = void;
export const checkPutTagRequest: (v: any) => boolean = isTagWithRefs; export const checkPutTagRequest: (v: any) => boolean = (v: any) => isTagRefs(v) && isName(v);;
// Replace integration (PUT). // Replace integration (PUT).
export const PutIntegrationEndpoint = "/integration/:id"; export const PutIntegrationEndpoint = "/integration/:id";
@ -128,27 +125,27 @@ export const checkPutIntegrationRequest: (v: any) => boolean = isIntegrationData
// Modify track (PATCH). // Modify track (PATCH).
export const PatchTrackEndpoint = "/track/:id"; export const PatchTrackEndpoint = "/track/:id";
export type PatchTrackRequest = TrackBaseWithRefs; export type PatchTrackRequest = Track;
export type PatchTrackResponse = void; export type PatchTrackResponse = void;
export const checkPatchTrackRequest: (v: any) => boolean = isTrackBaseWithRefs; export const checkPatchTrackRequest: (v: any) => boolean = isTrack;
// Modify artist (PATCH). // Modify artist (PATCH).
export const PatchArtistEndpoint = "/artist/:id"; export const PatchArtistEndpoint = "/artist/:id";
export type PatchArtistRequest = ArtistBaseWithRefs; export type PatchArtistRequest = Artist;
export type PatchArtistResponse = void; export type PatchArtistResponse = void;
export const checkPatchArtistRequest: (v: any) => boolean = isArtistBaseWithRefs; export const checkPatchArtistRequest: (v: any) => boolean = isArtist;
// Modify album (PATCH). // Modify album (PATCH).
export const PatchAlbumEndpoint = "/album/:id"; export const PatchAlbumEndpoint = "/album/:id";
export type PatchAlbumRequest = AlbumBaseWithRefs; export type PatchAlbumRequest = Album;
export type PatchAlbumResponse = void; export type PatchAlbumResponse = void;
export const checkPatchAlbumRequest: (v: any) => boolean = isAlbumBaseWithRefs; export const checkPatchAlbumRequest: (v: any) => boolean = isAlbum;
// Modify tag (PATCH). // Modify tag (PATCH).
export const PatchTagEndpoint = "/tag/:id"; export const PatchTagEndpoint = "/tag/:id";
export type PatchTagRequest = TagBaseWithRefs; export type PatchTagRequest = Tag;
export type PatchTagResponse = void; export type PatchTagResponse = void;
export const checkPatchTagRequest: (v: any) => boolean = isTagBaseWithRefs; export const checkPatchTagRequest: (v: any) => boolean = isTag;
// Modify integration (PATCH). // Modify integration (PATCH).
export const PatchIntegrationEndpoint = "/integration/:id"; export const PatchIntegrationEndpoint = "/integration/:id";

@ -6,195 +6,168 @@ export enum ResourceType {
Tag = "tag" Tag = "tag"
} }
export interface TrackBase { export interface Id {
mbApi_typename: "track", id: number,
}
name?: string, export function isId(q : any) : q is Id {
storeLinks?: string[], return 'id' in q;
albumId?: number | null,
} }
export interface TrackBaseWithRefs extends TrackBase {
artistIds?: number[], export interface Name {
albumId?: number | null, name: string,
tagIds?: number[],
} }
export interface TrackBaseWithDetails extends TrackBase {
artists: ArtistWithId[], export function isName(q : any) : q is Name {
album: AlbumWithId | null, return 'name' in q;
tags: TagWithId[],
} }
export interface TrackWithDetails extends TrackBaseWithDetails {
name: string, export interface StoreLinks {
storeLinks: string[],
} }
export interface TrackWithRefs extends TrackBaseWithRefs {
name: string, export interface TrackRefs {
artistIds: number[],
albumId: number | null, albumId: number | null,
artistIds: number[],
tagIds: number[], tagIds: number[],
} }
export interface Track extends TrackBase {
name: string, export interface TrackDetails {
album: AlbumWithId | null, artists: (Artist & Id)[],
artists: ArtistWithId[], album: (Album & Id) | null,
tags: TagWithId[], tags: (Tag & Id)[],
}
export interface TrackWithRefsWithId extends TrackWithRefs {
id: number,
}
export interface TrackWithDetailsWithId extends TrackWithDetails {
id: number,
} }
export interface TrackWithId extends Track {
id: number, export interface Track {
mbApi_typename: "track",
name?: string,
id?: number,
storeLinks?: string[],
albumId?: number | null,
artistIds?: number[],
tagIds?: number[],
artists?: (Artist & Id & Name)[],
album?: (Album & Id & Name) | null,
tags?: (Tag & Id & Name)[],
} }
export function isTrackBase(q: any): q is TrackBase {
export function isTrack(q: any): q is Track {
return q.mbApi_typename && q.mbApi_typename === "track"; return q.mbApi_typename && q.mbApi_typename === "track";
} }
export function isTrackBaseWithRefs(q: any): q is TrackBaseWithRefs {
return isTrackBase(q); export function isTrackRefs(q: any): q is TrackRefs {
return isTag(q) && 'albumId' in q && 'artistIds' in q && 'tagIds' in q;
} }
export function isTrackWithRefs(q: any): q is TrackWithRefs {
return isTrackBaseWithRefs(q) && "name" in q; export function isTrackDetails(q: any): q is TrackDetails {
return isTag(q) && 'album' in q && 'artists' in q && 'tags' in q;
} }
export interface ArtistRefs {
albumIds: number[],
tagIds: number[],
trackIds: number[],
}
export interface ArtistDetails {
albums?: (Album & Id)[],
tags?: (Tag & Id)[],
tracks?: (Track & Id)[],
}
export interface ArtistBase { export interface Artist {
mbApi_typename: "artist", mbApi_typename: "artist",
name?: string, name?: string,
id?: number,
storeLinks?: string[], storeLinks?: string[],
}
export interface ArtistBaseWithRefs extends ArtistBase {
albumIds?: number[], albumIds?: number[],
tagIds?: number[], tagIds?: number[],
trackIds?: number[], trackIds?: number[],
albums?: (Album & Id)[],
tags?: (Tag & Id)[],
tracks?: (Track & Id)[],
} }
export interface ArtistBaseWithDetails extends ArtistBase {
albums: AlbumWithId[], export function isArtist(q: any): q is Artist {
tags: TagWithId[],
tracks: TrackWithId[],
}
export interface ArtistWithDetails extends ArtistBaseWithDetails {
name: string,
}
export interface ArtistWithRefs extends ArtistBaseWithRefs {
name: string,
albumIds: number[],
tagIds: number[],
trackIds: number[],
}
export interface Artist extends ArtistBase {
name: string,
}
export interface ArtistWithRefsWithId extends ArtistWithRefs {
id: number,
}
export interface ArtistWithDetailsWithId extends ArtistWithDetails {
id: number,
}
export interface ArtistWithId extends Artist {
id: number,
}
export function isArtistBase(q: any): q is ArtistBase {
return q.mbApi_typename && q.mbApi_typename === "artist"; return q.mbApi_typename && q.mbApi_typename === "artist";
} }
export function isArtistBaseWithRefs(q: any): q is ArtistBaseWithRefs {
return isArtistBase(q); export function isArtistRefs(q: any): q is ArtistRefs {
} return isTag(q) && 'albumIds' in q && 'trackIds' in q && 'tagIds' in q;
export function isArtistWithRefs(q: any): q is ArtistWithRefs {
return isArtistBaseWithRefs(q) && "name" in q;
} }
export function isArtistDetails(q: any): q is ArtistDetails {
return isTag(q) && 'albums' in q && 'tracks' in q && 'tags' in q;
}
export interface AlbumBase { export interface Album {
mbApi_typename: "album", mbApi_typename: "album",
name?: string, name?: string,
id?: number,
storeLinks?: string[], storeLinks?: string[],
}
export interface AlbumBaseWithRefs extends AlbumBase {
artistIds?: number[], artistIds?: number[],
trackIds?: number[], trackIds?: number[],
tagIds?: number[], tagIds?: number[],
artists?: (Artist & Id)[],
tracks?: (Track & Id)[],
tags?: (Tag & Id)[],
} }
export interface AlbumBaseWithDetails extends AlbumBase {
artists: ArtistWithId[], export interface AlbumRefs {
tracks: TrackWithId[],
tags: TagWithId[],
}
export interface AlbumWithDetails extends AlbumBaseWithDetails {
name: string,
}
export interface AlbumWithRefs extends AlbumBaseWithRefs {
name: string,
artistIds: number[], artistIds: number[],
trackIds: number[], trackIds: number[],
tagIds: number[], tagIds: number[],
} }
export interface Album extends AlbumBase {
name: string, export interface AlbumDetails {
artists: ArtistWithId[], artists: (Artist & Id)[],
} tracks: (Track & Id)[],
export interface AlbumWithRefsWithId extends AlbumWithRefs { tags: (Tag & Id)[],
id: number,
}
export interface AlbumWithDetailsWithId extends AlbumWithDetails {
id: number,
}
export interface AlbumWithId extends Album {
id: number,
} }
export function isAlbumBase(q: any): q is AlbumBase {
export function isAlbum(q: any): q is Album {
return q.mbApi_typename && q.mbApi_typename === "album"; return q.mbApi_typename && q.mbApi_typename === "album";
} }
export function isAlbumBaseWithRefs(q: any): q is AlbumBaseWithRefs {
return isAlbumBase(q); export function isAlbumRefs(q: any): q is AlbumRefs {
} return isTag(q) && 'artistIds' in q && 'trackIds' in q && 'tagIds' in q;
export function isAlbumWithRefs(q: any): q is AlbumWithRefs {
return isAlbumBaseWithRefs(q) && "name" in q;
} }
export function isAlbumDetails(q: any): q is AlbumDetails {
return isTag(q) && 'artists' in q && 'tracks' in q && 'tags' in q;
}
export interface TagBase { export interface Tag {
mbApi_typename: "tag", mbApi_typename: "tag",
name?: string, name?: string,
} id?: number,
export interface TagBaseWithRefs extends TagBase {
parentId?: number | null, parentId?: number | null,
parent?: (Tag & Id) | null,
} }
export interface TagBaseWithDetails extends TagBase {
parent?: TagWithId | null, export interface TagRefs {
}
export interface TagWithDetails extends TagBaseWithDetails {
name: string,
}
export interface TagWithRefs extends TagBaseWithRefs {
name: string,
parentId: number | null, parentId: number | null,
} }
export interface Tag extends TagBaseWithDetails {
name: string, export interface TagDetails {
} parent: (Tag & Id) | null,
export interface TagWithRefsWithId extends TagWithRefs {
id: number,
}
export interface TagWithDetailsWithId extends TagWithDetails {
id: number,
}
export interface TagWithId extends Tag {
id: number,
} }
export function isTagBase(q: any): q is TagBase {
export function isTag(q: any): q is Tag {
return q.mbApi_typename && q.mbApi_typename === "tag"; return q.mbApi_typename && q.mbApi_typename === "tag";
} }
export function isTagBaseWithRefs(q: any): q is TagBaseWithRefs {
return isTagBase(q); export function isTagRefs(q: any): q is TagRefs {
} return isTag(q) && 'parentId' in q;
export function isTagWithRefs(q: any): q is TagWithRefs {
return isTagBaseWithRefs(q) && "name" in q;
} }
export function isTagDetails(q: any): q is TagDetails {
return isTag(q) && 'parent' in q;
}
// There are several implemented integration solutions, // There are several implemented integration solutions,
// enumerated here. // enumerated here.

@ -2,21 +2,32 @@ import React from 'react';
import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core'; import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core';
import stringifyList from '../../lib/stringifyList'; import stringifyList from '../../lib/stringifyList';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Artist, QueryResponseTrackDetails, Tag, Name } from '../../api/api';
export interface TrackGetters { function getTagNames (track: QueryResponseTrackDetails) : string[][] {
getName: (track: any) => string, // Recursively resolve the name.
getId: (track: any) => number, const resolveTag = (tag: any) => {
getArtistNames: (track: any) => string[], var r = [tag.name];
getArtistIds: (track: any) => number[], if (tag.parent) { r.unshift(resolveTag(tag.parent)); }
getAlbumName: (track: any) => string | undefined, return r;
getAlbumId: (track: any) => number | undefined, }
getTagNames: (track: any) => string[][], // Each tag is represented as a series of strings.
getTagIds: (track: any) => number[][], // Each tag is represented as a series of ids. return track.tags.map((tag: Tag) => resolveTag(tag));
}
function getTagIds (track: QueryResponseTrackDetails) : number[][] {
// Recursively resolve the id.
const resolveTag = (tag: any) => {
var r = [tag.tagId];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); }
return r;
}
return track.tags.map((tag: any) => resolveTag(tag));
} }
export default function TrackTable(props: { export default function TrackTable(props: {
tracks: any[], tracks: QueryResponseTrackDetails[]
trackGetters: TrackGetters,
}) { }) {
const history = useHistory(); const history = useHistory();
@ -44,16 +55,19 @@ export default function TrackTable(props: {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.tracks.map((track: any) => { {props.tracks.map((track: QueryResponseTrackDetails) => {
const name = props.trackGetters.getName(track); const name = track.name;
// TODO: display artists and albums separately! // TODO: display artists and albums separately!
const artistNames = props.trackGetters.getArtistNames(track); const artistNames = track.artists
.filter( (a: Artist) => a.name )
.map( (a: (Artist & Name)) => a.name );
const artist = stringifyList(artistNames); const artist = stringifyList(artistNames);
const mainArtistId = props.trackGetters.getArtistIds(track)[0]; const mainArtistId =
const album = props.trackGetters.getAlbumName(track); (track.artists.length > 0 && track.artists[0].id) || undefined;
const albumId = props.trackGetters.getAlbumId(track); const album = track.album?.name || undefined;
const trackId = props.trackGetters.getId(track); const albumId = track.album?.id || undefined;
const tagIds = props.trackGetters.getTagIds(track); const trackId = track.id;
const tagIds = getTagIds(track);
const onClickArtist = () => { const onClickArtist = () => {
history.push('/artist/' + mainArtistId); history.push('/artist/' + mainArtistId);
@ -71,7 +85,7 @@ export default function TrackTable(props: {
history.push('/tag/' + id); history.push('/tag/' + id);
} }
const tags = props.trackGetters.getTagNames(track).map((tag: string[], i: number) => { const tags = getTagNames(track).map((tag: string[], i: number) => {
const fullTag = stringifyList(tag, undefined, (idx: number, e: string) => { const fullTag = stringifyList(tag, undefined, (idx: number, e: string) => {
return (idx === 0) ? e : " / " + e; return (idx === 0) ? e : " / " + e;
}) })

@ -10,7 +10,6 @@ import LoyaltyIcon from '@material-ui/icons/Loyalty';
import TrackWindow, { TrackWindowReducer } from './track/TrackWindow'; import TrackWindow, { TrackWindowReducer } from './track/TrackWindow';
import AlbumWindow, { AlbumWindowReducer } from './album/AlbumWindow'; import AlbumWindow, { AlbumWindowReducer } from './album/AlbumWindow';
import TagWindow, { TagWindowReducer } from './tag/TagWindow'; import TagWindow, { TagWindowReducer } from './tag/TagWindow';
import { trackGetters } from '../../lib/trackGetters';
import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow'; import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
import { RegisterWindowReducer } from './register/RegisterWindow'; import { RegisterWindowReducer } from './register/RegisterWindow';
import { LoginWindowReducer } from './login/LoginWindow'; import { LoginWindowReducer } from './login/LoginWindow';
@ -59,7 +58,6 @@ export const newWindowState = {
id: 1, id: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
trackGetters: trackGetters,
tracksByArtist: null, tracksByArtist: null,
} }
}, },
@ -68,7 +66,6 @@ export const newWindowState = {
id: 1, id: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
trackGetters: trackGetters,
tracksOnAlbum: null, tracksOnAlbum: null,
} }
}, },
@ -84,7 +81,6 @@ export const newWindowState = {
id: 1, id: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
trackGetters: trackGetters,
tracksWithTag: null, tracksWithTag: null,
} }
}, },

@ -6,16 +6,16 @@ import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable, { TrackGetters } from '../../tables/ResultsTable'; import TrackTable from '../../tables/ResultsTable';
import { modifyAlbum } from '../../../lib/saveChanges'; import { modifyAlbum } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, queryTracks } from '../../../lib/backend/queries'; import { queryAlbums, queryTracks } from '../../../lib/backend/queries';
import { trackGetters } from '../../../lib/trackGetters';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request'; import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request';
import { useAuth } from '../../../lib/useAuth'; import { useAuth } from '../../../lib/useAuth';
import { Album, Name, Id, StoreLinks, AlbumRefs } from '../../../api/api';
export type AlbumMetadata = serverApi.AlbumWithId; export type AlbumMetadata = serverApi.QueryResponseAlbumDetails;
export type AlbumMetadataChanges = serverApi.PatchAlbumRequest; export type AlbumMetadataChanges = serverApi.PatchAlbumRequest;
export interface AlbumWindowState extends WindowState { export interface AlbumWindowState extends WindowState {
@ -23,7 +23,6 @@ export interface AlbumWindowState extends WindowState {
metadata: AlbumMetadata | null, metadata: AlbumMetadata | null,
pendingChanges: AlbumMetadataChanges | null, pendingChanges: AlbumMetadataChanges | null,
tracksOnAlbum: any[] | null, tracksOnAlbum: any[] | null,
trackGetters: TrackGetters,
} }
export enum AlbumWindowStateActions { export enum AlbumWindowStateActions {
@ -48,7 +47,7 @@ export function AlbumWindowReducer(state: AlbumWindowState, action: any) {
} }
} }
export async function getAlbumMetadata(id: number) { export async function getAlbumMetadata(id: number) : Promise<AlbumMetadata> {
let result: any = await queryAlbums( let result: any = await queryAlbums(
{ {
a: QueryLeafBy.AlbumId, a: QueryLeafBy.AlbumId,
@ -65,7 +64,6 @@ export default function AlbumWindow(props: {}) {
id: parseInt(id), id: parseInt(id),
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
trackGetters: trackGetters,
tracksOnAlbum: null, tracksOnAlbum: null,
}); });
@ -198,7 +196,6 @@ export function AlbumWindowControlled(props: {
</Box> </Box>
{props.state.tracksOnAlbum && <TrackTable {props.state.tracksOnAlbum && <TrackTable
tracks={props.state.tracksOnAlbum} tracks={props.state.tracksOnAlbum}
trackGetters={props.state.trackGetters}
/>} />}
{!props.state.tracksOnAlbum && <CircularProgress />} {!props.state.tracksOnAlbum && <CircularProgress />}
</Box> </Box>

@ -6,16 +6,15 @@ import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable, { TrackGetters } from '../../tables/ResultsTable'; import TrackTable from '../../tables/ResultsTable';
import { modifyArtist } from '../../../lib/saveChanges'; import { modifyArtist } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, queryTracks } from '../../../lib/backend/queries'; import { queryArtists, queryTracks } from '../../../lib/backend/queries';
import { trackGetters } from '../../../lib/trackGetters';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request'; import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request';
import { useAuth } from '../../../lib/useAuth'; import { useAuth } from '../../../lib/useAuth';
export type ArtistMetadata = serverApi.ArtistWithId; export type ArtistMetadata = serverApi.QueryResponseArtistDetails;
export type ArtistMetadataChanges = serverApi.PatchArtistRequest; export type ArtistMetadataChanges = serverApi.PatchArtistRequest;
export interface ArtistWindowState extends WindowState { export interface ArtistWindowState extends WindowState {
@ -23,7 +22,6 @@ export interface ArtistWindowState extends WindowState {
metadata: ArtistMetadata | null, metadata: ArtistMetadata | null,
pendingChanges: ArtistMetadataChanges | null, pendingChanges: ArtistMetadataChanges | null,
tracksByArtist: any[] | null, tracksByArtist: any[] | null,
trackGetters: TrackGetters,
} }
export enum ArtistWindowStateActions { export enum ArtistWindowStateActions {
@ -53,7 +51,7 @@ export interface IProps {
dispatch: (action: any) => void, dispatch: (action: any) => void,
} }
export async function getArtistMetadata(id: number) { export async function getArtistMetadata(id: number) : Promise<ArtistMetadata> {
let response: any = await queryArtists( let response: any = await queryArtists(
{ {
a: QueryLeafBy.ArtistId, a: QueryLeafBy.ArtistId,
@ -70,7 +68,6 @@ export default function ArtistWindow(props: {}) {
id: parseInt(id), id: parseInt(id),
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
trackGetters: trackGetters,
tracksByArtist: null, tracksByArtist: null,
}); });
@ -203,7 +200,6 @@ export function ArtistWindowControlled(props: {
</Box> </Box>
{props.state.tracksByArtist && <TrackTable {props.state.tracksByArtist && <TrackTable
tracks={props.state.tracksByArtist} tracks={props.state.tracksByArtist}
trackGetters={props.state.trackGetters}
/>} />}
{!props.state.tracksByArtist && <CircularProgress />} {!props.state.tracksByArtist && <CircularProgress />}
</Box> </Box>

@ -3,10 +3,10 @@ import { Box, LinearProgress } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder'; import QueryBuilder from '../../querybuilder/QueryBuilder';
import TrackTable from '../../tables/ResultsTable'; import TrackTable from '../../tables/ResultsTable';
import { trackGetters } from '../../../lib/trackGetters';
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries'; import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import { QueryResponseType } from '../../../api/api'; import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails} from '../../../api/api';
import { ServerStreamResponseOptions } from 'http2';
var _ = require('lodash'); var _ = require('lodash');
export interface ResultsForQuery { export interface ResultsForQuery {
@ -117,12 +117,12 @@ export function QueryWindowControlled(props: {
const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : [];
const doQuery = useCallback(async (_query: QueryElem) => { const doQuery = useCallback(async (_query: QueryElem) => {
const tracks: any = await queryTracks( const tracks: QueryResponseTrackDetails[] = await queryTracks(
_query, _query,
0, 0,
100, //TODO: pagination 100, //TODO: pagination
QueryResponseType.Details QueryResponseType.Details
); ) as QueryResponseTrackDetails[];
if (_.isEqual(query, _query)) { if (_.isEqual(query, _query)) {
setResultsForQuery({ setResultsForQuery({
@ -164,7 +164,6 @@ export function QueryWindowControlled(props: {
> >
<TrackTable <TrackTable
tracks={showResults} tracks={showResults}
trackGetters={trackGetters}
/> />
{loading && <LinearProgress />} {loading && <LinearProgress />}
</Box> </Box>

@ -6,14 +6,13 @@ import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable, { TrackGetters } from '../../tables/ResultsTable'; import TrackTable from '../../tables/ResultsTable';
import { modifyTag } from '../../../lib/backend/tags'; import { modifyTag } from '../../../lib/backend/tags';
import { queryTags, queryTracks } from '../../../lib/backend/queries'; import { queryTags, queryTracks } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { trackGetters } from '../../../lib/trackGetters';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
export interface FullTagMetadata extends serverApi.TagWithId { export interface FullTagMetadata extends serverApi.QueryResponseTagDetails {
fullName: string[], fullName: string[],
fullId: number[], fullId: number[],
} }
@ -26,7 +25,6 @@ export interface TagWindowState extends WindowState {
metadata: TagMetadata | null, metadata: TagMetadata | null,
pendingChanges: TagMetadataChanges | null, pendingChanges: TagMetadataChanges | null,
tracksWithTag: any[] | null, tracksWithTag: any[] | null,
trackGetters: TrackGetters,
} }
export enum TagWindowStateActions { export enum TagWindowStateActions {
@ -51,7 +49,7 @@ export function TagWindowReducer(state: TagWindowState, action: any) {
} }
} }
export async function getTagMetadata(id: number) { export async function getTagMetadata(id: number) : Promise<FullTagMetadata> {
let tags: any = await queryTags( let tags: any = await queryTags(
{ {
a: QueryLeafBy.TagId, a: QueryLeafBy.TagId,
@ -81,7 +79,6 @@ export default function TagWindow(props: {}) {
id: parseInt(id), id: parseInt(id),
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
trackGetters: trackGetters,
tracksWithTag: null, tracksWithTag: null,
}); });
@ -205,7 +202,6 @@ export function TagWindowControlled(props: {
</Box> </Box>
{props.state.tracksWithTag && <TrackTable {props.state.tracksWithTag && <TrackTable
tracks={props.state.tracksWithTag} tracks={props.state.tracksWithTag}
trackGetters={props.state.trackGetters}
/>} />}
{!props.state.tracksWithTag && <CircularProgress />} {!props.state.tracksWithTag && <CircularProgress />}
</Box> </Box>

@ -16,7 +16,7 @@ import EditIcon from '@material-ui/icons/Edit';
import { modifyTrack } from '../../../lib/saveChanges'; import { modifyTrack } from '../../../lib/saveChanges';
import { getTrack } from '../../../lib/backend/tracks'; import { getTrack } from '../../../lib/backend/tracks';
export type TrackMetadata = serverApi.TrackWithId; export type TrackMetadata = serverApi.QueryResponseTrackDetails;
export interface TrackWindowState extends WindowState { export interface TrackWindowState extends WindowState {
id: number, id: number,
@ -60,7 +60,7 @@ export function TrackWindowControlled(props: {
useEffect(() => { useEffect(() => {
if (metadata === null) { if (metadata === null) {
getTrack(trackId) getTrack(trackId)
.then((m: serverApi.Track) => { .then((m: serverApi.GetTrackResponse) => {
dispatch({ dispatch({
type: TrackWindowStateActions.SetMetadata, type: TrackWindowStateActions.SetMetadata,
value: m value: m
@ -71,7 +71,7 @@ export function TrackWindowControlled(props: {
const title = <Typography variant="h4">{metadata?.name || "(Unknown title)"}</Typography> const title = <Typography variant="h4">{metadata?.name || "(Unknown title)"}</Typography>
const artists = metadata?.artists && metadata?.artists.map((artist: ArtistMetadata) => { const artists = metadata?.artists && metadata?.artists.map((artist: (serverApi.Artist & serverApi.Name)) => {
return <Typography> return <Typography>
{artist.name} {artist.name}
</Typography> </Typography>

@ -3,6 +3,9 @@ import { GetAlbumResponse } from '../../api/api';
import backendRequest from './request'; import backendRequest from './request';
export async function getAlbum(id: number): Promise<GetAlbumResponse> { export async function getAlbum(id: number): Promise<GetAlbumResponse> {
if (isNaN(id)) {
throw new Error("Cannot request a NaN album.");
}
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetAlbumEndpoint.replace(':id', `${id}`)) const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetAlbumEndpoint.replace(':id', `${id}`))
if (!response.ok) { if (!response.ok) {
throw new Error("Response to album request not OK: " + JSON.stringify(response)); throw new Error("Response to album request not OK: " + JSON.stringify(response));

@ -3,6 +3,9 @@ import { GetArtistResponse } from '../../api/api';
import backendRequest from './request'; import backendRequest from './request';
export async function getArtist(id: number): Promise<GetArtistResponse> { export async function getArtist(id: number): Promise<GetArtistResponse> {
if (isNaN(id)) {
throw new Error("Cannot request a NaN artist.");
}
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetArtistEndpoint.replace(':id', `${id}`)) const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetArtistEndpoint.replace(':id', `${id}`))
if (!response.ok) { if (!response.ok) {
throw new Error("Response to artist request not OK: " + JSON.stringify(response)); throw new Error("Response to artist request not OK: " + JSON.stringify(response));

@ -51,7 +51,7 @@ export async function queryArtists(
offset: number | undefined, offset: number | undefined,
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.ArtistWithId[] | number[] | number> { ): Promise<serverApi.QueryResponseArtistDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Artist], query, offset, limit, responseType); let r = await queryItems([serverApi.ResourceType.Artist], query, offset, limit, responseType);
return r.artists; return r.artists;
} }
@ -61,7 +61,7 @@ export async function queryAlbums(
offset: number | undefined, offset: number | undefined,
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.AlbumWithId[] | number[] | number> { ): Promise<serverApi.QueryResponseAlbumDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Album], query, offset, limit, responseType); let r = await queryItems([serverApi.ResourceType.Album], query, offset, limit, responseType);
return r.albums; return r.albums;
} }
@ -71,7 +71,7 @@ export async function queryTracks(
offset: number | undefined, offset: number | undefined,
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.TrackWithId[] | number[] | number> { ): Promise<serverApi.QueryResponseTrackDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Track], query, offset, limit, responseType); let r = await queryItems([serverApi.ResourceType.Track], query, offset, limit, responseType);
return r.tracks; return r.tracks;
} }
@ -81,7 +81,7 @@ export async function queryTags(
offset: number | undefined, offset: number | undefined,
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.TagWithId[] | number[] | number> { ): Promise<serverApi.QueryResponseTagDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Tag], query, offset, limit, responseType); let r = await queryItems([serverApi.ResourceType.Tag], query, offset, limit, responseType);
return r.tags; return r.tags;
} }

@ -2,6 +2,9 @@ import * as serverApi from '../../api/api';
import backendRequest from './request'; import backendRequest from './request';
export async function getTrack(id: number): Promise<serverApi.GetTrackResponse> { export async function getTrack(id: number): Promise<serverApi.GetTrackResponse> {
if (isNaN(id)) {
throw new Error("Cannot request a NaN track.");
}
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetTrackEndpoint.replace(':id', `${id}`)) const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.GetTrackEndpoint.replace(':id', `${id}`))
if (!response.ok) { if (!response.ok) {
throw new Error("Response to track request not OK: " + JSON.stringify(response)); throw new Error("Response to track request not OK: " + JSON.stringify(response));

@ -1,28 +0,0 @@
export const trackGetters = {
getName: (track: any) => track.name,
getId: (track: any) => track.trackId,
getArtistNames: (track: any) => track.artists.map((a: any) => a.name),
getArtistIds: (track: any) => track.artists.map((a: any) => a.artistId),
getAlbumName: (track: any) => track.album ? track.album.name : undefined,
getAlbumId: (track: any) => track.album ? track.album.albumId : undefined,
getTagNames: (track: any) => {
// Recursively resolve the name.
const resolveTag = (tag: any) => {
var r = [tag.name];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); }
return r;
}
return track.tags.map((tag: any) => resolveTag(tag));
},
getTagIds: (track: any) => {
// Recursively resolve the id.
const resolveTag = (tag: any) => {
var r = [tag.tagId];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); }
return r;
}
return track.tags.map((tag: any) => resolveTag(tag));
},
}

@ -1,5 +1,5 @@
import Knex from "knex"; import Knex from "knex";
import { AlbumBaseWithRefs, AlbumWithDetails, AlbumWithRefs } from "../../client/src/api/api"; import { Album, AlbumRefs, Id, Name, AlbumDetails, StoreLinks, Tag, TagRefs, Track, Artist } from "../../client/src/api/api";
import * as api from '../../client/src/api/api'; import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson"; import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types"; import { DBError, DBErrorKind } from "../endpoints/types";
@ -9,12 +9,12 @@ var _ = require('lodash');
// Returns an album with details, or null if not found. // Returns an album with details, or null if not found.
export async function getAlbum(id: number, userId: number, knex: Knex): export async function getAlbum(id: number, userId: number, knex: Knex):
Promise<AlbumWithDetails> { Promise<(Album & AlbumDetails & StoreLinks & Name)> {
// Start transfers for tracks, tags and artists. // Start transfers for tracks, tags and artists.
// Also request the album itself. // Also request the album itself.
const tagsPromise: Promise<api.TagWithId[]> = const tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
knex.select('tagId') knex.select('tagId')
.from('albums_tags') .from('albums_tags')
.where({ 'albumId': id }) .where({ 'albumId': id })
@ -23,15 +23,21 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId']) knex.select(['id', 'name', 'parentId'])
.from('tags') .from('tags')
.whereIn('id', ids) .whereIn('id', ids)
.then((tags: (Id & Name & TagRefs)[]) =>
tags.map((tag : (Id & Name & TagRefs)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
); );
const tracksPromise: Promise<api.TrackWithId[]> = const tracksPromise: Promise<(Track & Id)[]> =
knex.select(['id', 'name', 'storeLinks']) knex.select(['id', 'name', 'storeLinks'])
.from('tracks') .from('tracks')
.where({ 'album': id }) .where({ 'album': id })
.then((tracks: any) => tracks.map((track: any) => track['id'])) .then((tracks: any) => tracks.map((track: any) => {
return { id: track['id'], mbApi_typename: "track" }
}))
const artistsPromise: Promise<api.ArtistWithId[]> = const artistsPromise: Promise<(Artist & Id & Name & StoreLinks)[]> =
knex.select('artistId') knex.select('artistId')
.from('artists_albums') .from('artists_albums')
.where({ 'albumId': id }) .where({ 'albumId': id })
@ -40,14 +46,18 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks']) knex.select(['id', 'name', 'storeLinks'])
.from('artists') .from('artists')
.whereIn('id', ids) .whereIn('id', ids)
.then((artists: (Id & Name & StoreLinks)[]) =>
artists.map((artist : (Id & Name & StoreLinks)) =>
{ return {...artist, mbApi_typename: "artist"}}
))
); );
const albumPromise: Promise<api.Album | undefined> = const albumPromise: Promise<(Album & Name & StoreLinks) | undefined> =
knex.select('name', 'storeLinks') knex.select('name', 'storeLinks')
.from('albums') .from('albums')
.where({ 'user': userId }) .where({ 'user': userId })
.where({ id: id }) .where({ id: id })
.then((albums: any) => albums[0]); .then((albums: any) => { return { ...albums[0], mbApi_typename: 'album' }});
// Wait for the requests to finish. // Wait for the requests to finish.
const [album, tags, tracks, artists] = const [album, tags, tracks, artists] =
@ -57,9 +67,9 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
return { return {
mbApi_typename: 'album', mbApi_typename: 'album',
name: album['name'], name: album['name'],
artists: artists as api.ArtistWithId[], artists: artists || [],
tags: tags as api.TagWithId[], tags: tags || [],
tracks: tracks as api.TrackWithId[], tracks: tracks || [],
storeLinks: asJson(album['storeLinks'] || []), storeLinks: asJson(album['storeLinks'] || []),
}; };
} }
@ -68,7 +78,7 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
} }
// Returns the id of the created album. // Returns the id of the created album.
export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Knex): Promise<number> { export async function createAlbum(userId: number, album: (Album & Name & AlbumRefs), knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => { return await knex.transaction(async (trx) => {
// Start retrieving artists. // Start retrieving artists.
const artistIdsPromise: Promise<number[]> = const artistIdsPromise: Promise<number[]> =
@ -150,7 +160,7 @@ export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Kn
}) })
} }
export async function modifyAlbum(userId: number, albumId: number, album: AlbumBaseWithRefs, knex: Knex): Promise<void> { export async function modifyAlbum(userId: number, albumId: number, album: Album, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => { await knex.transaction(async (trx) => {
// Start retrieving the album itself. // Start retrieving the album itself.
const albumIdPromise: Promise<number | undefined> = const albumIdPromise: Promise<number | undefined> =

@ -1,5 +1,5 @@
import Knex from "knex"; import Knex from "knex";
import { ArtistBaseWithRefs, ArtistWithDetails, ArtistWithRefs } from "../../client/src/api/api"; import { Artist, ArtistDetails, Tag, Track, TagRefs, Id, Name, StoreLinks, Album, ArtistRefs } from "../../client/src/api/api";
import * as api from '../../client/src/api/api'; import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson"; import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types"; import { DBError, DBErrorKind } from "../endpoints/types";
@ -8,10 +8,10 @@ var _ = require('lodash')
// Returns an artist with details, or null if not found. // Returns an artist with details, or null if not found.
export async function getArtist(id: number, userId: number, knex: Knex): export async function getArtist(id: number, userId: number, knex: Knex):
Promise<ArtistWithDetails> { Promise<(Artist & ArtistDetails & Name & StoreLinks)> {
// Start transfers for tags and albums. // Start transfers for tags and albums.
// Also request the artist itself. // Also request the artist itself.
const tagsPromise: Promise<api.TagWithId[]> = const tagsPromise: Promise<(Tag & Name & Id & TagRefs)[]> =
knex.select('tagId') knex.select('tagId')
.from('artists_tags') .from('artists_tags')
.where({ 'artistId': id }) .where({ 'artistId': id })
@ -20,9 +20,13 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId']) knex.select(['id', 'name', 'parentId'])
.from('tags') .from('tags')
.whereIn('id', ids) .whereIn('id', ids)
.then((tags: (Id & Name & TagRefs)[]) =>
tags.map((tag : (Id & Name & TagRefs)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
); );
const albumsPromise: Promise<api.AlbumWithId[]> = const albumsPromise: Promise<(Album & Name & Id & StoreLinks)[]> =
knex.select('albumId') knex.select('albumId')
.from('artists_albums') .from('artists_albums')
.where({ 'artistId': id }) .where({ 'artistId': id })
@ -31,9 +35,13 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks']) knex.select(['id', 'name', 'storeLinks'])
.from('albums') .from('albums')
.whereIn('id', ids) .whereIn('id', ids)
.then((albums: (Id & Name & StoreLinks)[]) =>
albums.map((tag : (Id & Name & StoreLinks)) =>
{ return {...tag, mbApi_typename: "album"}}
))
); );
const tracksPromise: Promise<api.TrackWithId[]> = const tracksPromise: Promise<(Track & Id & Name & StoreLinks)[]> =
knex.select('trackId') knex.select('trackId')
.from('tracks_artists') .from('tracks_artists')
.where({ 'artistId': id }) .where({ 'artistId': id })
@ -42,26 +50,30 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks']) knex.select(['id', 'name', 'storeLinks'])
.from('tracks') .from('tracks')
.whereIn('id', ids) .whereIn('id', ids)
.then((tracks: (Id & Name & StoreLinks)[]) =>
tracks.map((tag : (Id & Name & StoreLinks)) =>
{ return {...tag, mbApi_typename: "track"}}
))
); );
const artistPromise: Promise<api.Artist | undefined> = const artistPromise: Promise<(Artist & Name & StoreLinks) | undefined> =
knex.select('name', 'storeLinks') knex.select('name', 'storeLinks')
.from('artists') .from('artists')
.where({ 'user': userId }) .where({ 'user': userId })
.where({ id: id }) .where({ id: id })
.then((artists: any) => artists[0]); .then((artists: any) => { return { ...artists[0], mbApi_typename: 'artist' } });
// Wait for the requests to finish. // Wait for the requests to finish.
const [artist, tags, albums, tracks] = const [artist, tags, albums, tracks] =
await Promise.all([artistPromise, tagsPromise, albumsPromise, tracksPromise]); await Promise.all([artistPromise, tagsPromise, albumsPromise, tracksPromise]);
if (artist) { if (artist && artist['name']) {
return { return {
mbApi_typename: 'artist', mbApi_typename: 'artist',
name: artist['name'], name: artist['name'],
albums: albums as api.AlbumWithId[], albums: albums,
tags: tags as api.TagWithId[], tags: tags,
tracks: tracks as api.TrackWithId[], tracks: tracks,
storeLinks: asJson(artist['storeLinks'] || []), storeLinks: asJson(artist['storeLinks'] || []),
}; };
} }
@ -70,7 +82,7 @@ export async function getArtist(id: number, userId: number, knex: Knex):
} }
// Returns the id of the created artist. // Returns the id of the created artist.
export async function createArtist(userId: number, artist: ArtistWithRefs, knex: Knex): Promise<number> { export async function createArtist(userId: number, artist: (Artist & ArtistRefs & Name), knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => { return await knex.transaction(async (trx) => {
// Start retrieving albums. // Start retrieving albums.
const albumIdsPromise: Promise<number[]> = const albumIdsPromise: Promise<number[]> =
@ -157,7 +169,7 @@ export async function createArtist(userId: number, artist: ArtistWithRefs, knex:
}) })
} }
export async function modifyArtist(userId: number, artistId: number, artist: ArtistBaseWithRefs, knex: Knex): Promise<void> { export async function modifyArtist(userId: number, artistId: number, artist: Artist, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => { await knex.transaction(async (trx) => {
// Start retrieving the artist itself. // Start retrieving the artist itself.
const artistIdPromise: Promise<number | undefined | null> = const artistIdPromise: Promise<number | undefined | null> =

@ -1,5 +1,5 @@
import Knex from "knex"; import Knex from "knex";
import { TrackWithRefsWithId, AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, AlbumBaseWithRefs, DBImportResponse, IDMappings } from "../../client/src/api/api"; import { Track, TrackRefs, Id, Name, StoreLinks, Album, AlbumRefs, Artist, ArtistRefs, Tag, TagRefs, isTrackRefs, isAlbumRefs, DBImportResponse, IDMappings } from "../../client/src/api/api";
import * as api from '../../client/src/api/api'; import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson"; import asJson from "../lib/asJson";
import { createArtist } from "./Artist"; import { createArtist } from "./Artist";
@ -12,7 +12,7 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
// First, retrieve all the objects without taking linking tables into account. // First, retrieve all the objects without taking linking tables into account.
// Fetch the links separately. // Fetch the links separately.
let tracksPromise: Promise<api.TrackWithRefsWithId[]> = let tracksPromise: Promise<(Track & Id & Name & StoreLinks & TrackRefs)[]> =
knex.select('id', 'name', 'storeLinks', 'album') knex.select('id', 'name', 'storeLinks', 'album')
.from('tracks') .from('tracks')
.where({ 'user': userId }) .where({ 'user': userId })
@ -28,7 +28,7 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
} }
})); }));
let albumsPromise: Promise<api.AlbumWithRefsWithId[]> = let albumsPromise: Promise<(Album & Id & Name & StoreLinks & AlbumRefs)[]> =
knex.select('name', 'storeLinks', 'id') knex.select('name', 'storeLinks', 'id')
.from('albums') .from('albums')
.where({ 'user': userId }) .where({ 'user': userId })
@ -44,7 +44,7 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
} }
})); }));
let artistsPromise: Promise<api.ArtistWithRefsWithId[]> = let artistsPromise: Promise<(Artist & Id & Name & ArtistRefs & StoreLinks)[]> =
knex.select('name', 'storeLinks', 'id') knex.select('name', 'storeLinks', 'id')
.from('artists') .from('artists')
.where({ 'user': userId }) .where({ 'user': userId })
@ -60,7 +60,7 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
} }
})); }));
let tagsPromise: Promise<api.TagWithRefsWithId[]> = let tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
knex.select('name', 'parentId', 'id') knex.select('name', 'parentId', 'id')
.from('tags') .from('tags')
.where({ 'user': userId }) .where({ 'user': userId })
@ -123,28 +123,28 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
// Now store the links inside the resource objects. // Now store the links inside the resource objects.
tracksArtists.forEach((v: [number, number]) => { tracksArtists.forEach((v: [number, number]) => {
let [trackId, artistId] = v; let [trackId, artistId] = v;
tracks.find((t: TrackWithRefsWithId) => t.id === trackId)?.artistIds.push(artistId); tracks.find((t: (Track & Id & TrackRefs)) => t.id === trackId)?.artistIds.push(artistId);
artists.find((a: ArtistWithRefsWithId) => a.id === artistId)?.trackIds.push(trackId); artists.find((a: (Artist & Id & ArtistRefs)) => a.id === artistId)?.trackIds.push(trackId);
}) })
tracks.forEach((t: api.TrackWithRefsWithId) => { tracks.forEach((t: (Track & Id & TrackRefs)) => {
albums.find((a: AlbumWithRefsWithId) => t.albumId && a.id === t.albumId)?.trackIds.push(t.id); albums.find((a: (Album & Id & AlbumRefs)) => t.albumId && a.id === t.albumId)?.trackIds.push(t.id);
}) })
tracksTags.forEach((v: [number, number]) => { tracksTags.forEach((v: [number, number]) => {
let [trackId, tagId] = v; let [trackId, tagId] = v;
tracks.find((t: TrackWithRefsWithId) => t.id === trackId)?.tagIds.push(tagId); tracks.find((t: (Track & Id & TrackRefs)) => t.id === trackId)?.tagIds.push(tagId);
}) })
artistsTags.forEach((v: [number, number]) => { artistsTags.forEach((v: [number, number]) => {
let [artistId, tagId] = v; let [artistId, tagId] = v;
artists.find((t: ArtistWithRefsWithId) => t.id === artistId)?.tagIds.push(tagId); artists.find((t: (Artist & Id & ArtistRefs)) => t.id === artistId)?.tagIds.push(tagId);
}) })
albumsTags.forEach((v: [number, number]) => { albumsTags.forEach((v: [number, number]) => {
let [albumId, tagId] = v; let [albumId, tagId] = v;
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.tagIds.push(tagId); albums.find((t: (Album & Id & AlbumRefs)) => t.id === albumId)?.tagIds.push(tagId);
}) })
artistsAlbums.forEach((v: [number, number]) => { artistsAlbums.forEach((v: [number, number]) => {
let [albumId, artistId] = v; let [albumId, artistId] = v;
artists.find((t: ArtistWithRefsWithId) => t.id === artistId)?.albumIds.push(albumId); artists.find((t: (Artist & Id & ArtistRefs)) => t.id === artistId)?.albumIds.push(albumId);
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.artistIds.push(artistId); albums.find((t: (Album & Id & AlbumRefs)) => t.id === albumId)?.artistIds.push(artistId);
}) })
return { return {

@ -2,9 +2,10 @@ import * as api from '../../client/src/api/api';
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from '../endpoints/types'; import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from '../endpoints/types';
import Knex from 'knex'; import Knex from 'knex';
import asJson from '../lib/asJson'; import asJson from '../lib/asJson';
import { Tag, TagDetails, Id, Name, Artist, Track, TrackDetails, Album, StoreLinks } from '../../client/src/api/api';
export function toApiTag(dbObj: any): api.TagWithDetailsWithId { export function toApiTag(dbObj: any): api.QueryResponseTagDetails {
return <api.TagWithDetailsWithId>{ return {
mbApi_typename: "tag", mbApi_typename: "tag",
id: dbObj['tags.id'], id: dbObj['tags.id'],
name: dbObj['tags.name'], name: dbObj['tags.name'],
@ -13,8 +14,8 @@ export function toApiTag(dbObj: any): api.TagWithDetailsWithId {
}; };
} }
export function toApiArtist(dbObj: any): api.ArtistWithId { export function toApiArtist(dbObj: any): api.QueryResponseArtistDetails {
return <api.ArtistWithId>{ return {
mbApi_typename: "artist", mbApi_typename: "artist",
id: dbObj['artists.id'], id: dbObj['artists.id'],
name: dbObj['artists.name'], name: dbObj['artists.name'],
@ -22,8 +23,8 @@ export function toApiArtist(dbObj: any): api.ArtistWithId {
}; };
} }
export function toApiTrack(dbObj: any, artists: any[], tags: any[], albums: any[]): api.TrackWithDetailsWithId { export function toApiTrack(dbObj: any, artists: any[], tags: any[], albums: any[]): api.QueryResponseTrackDetails {
return <api.TrackWithDetailsWithId>{ return {
mbApi_typename: "track", mbApi_typename: "track",
id: dbObj['tracks.id'], id: dbObj['tracks.id'],
name: dbObj['tracks.name'], name: dbObj['tracks.name'],
@ -38,8 +39,8 @@ export function toApiTrack(dbObj: any, artists: any[], tags: any[], albums: any[
} }
} }
export function toApiAlbum(dbObj: any): api.AlbumWithId { export function toApiAlbum(dbObj: any): api.QueryResponseAlbumDetails {
return <api.AlbumWithId>{ return {
mbApi_typename: "album", mbApi_typename: "album",
id: dbObj['albums.id'], id: dbObj['albums.id'],
name: dbObj['albums.name'], name: dbObj['albums.name'],
@ -351,7 +352,8 @@ async function getFullTag(knex: Knex, userId: number, tag: any): Promise<any> {
return await resolveTag(tag); return await resolveTag(tag);
} }
export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex): Promise<api.QueryResponse> { export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex):
Promise<api.QueryResponse> {
const trackLimit = q.offsetsLimits.trackLimit; const trackLimit = q.offsetsLimits.trackLimit;
const trackOffset = q.offsetsLimits.trackOffset; const trackOffset = q.offsetsLimits.trackOffset;
const tagLimit = q.offsetsLimits.tagLimit; const tagLimit = q.offsetsLimits.tagLimit;

@ -1,7 +1,7 @@
import Knex from "knex"; import Knex from "knex";
import { isConstructorDeclaration } from "typescript"; import { isConstructorDeclaration } from "typescript";
import * as api from '../../client/src/api/api'; import * as api from '../../client/src/api/api';
import { TagBaseWithRefs, TagWithDetails, TagWithId, TagWithRefs, TagWithRefsWithId } from "../../client/src/api/api"; import { Tag, TagRefs, TagDetails, Id, Name } from "../../client/src/api/api";
import { DBError, DBErrorKind } from "../endpoints/types"; import { DBError, DBErrorKind } from "../endpoints/types";
import { makeNotFoundError } from "./common"; import { makeNotFoundError } from "./common";
@ -35,7 +35,7 @@ export async function getTagChildrenRecursive(id: number,
} }
// Returns the id of the created tag. // Returns the id of the created tag.
export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): Promise<number> { export async function createTag(userId: number, tag: (Tag & Name & TagRefs), knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => { return await knex.transaction(async (trx) => {
// If applicable, retrieve the parent tag. // If applicable, retrieve the parent tag.
const maybeMatches: any[] | null = const maybeMatches: any[] | null =
@ -123,27 +123,34 @@ export async function deleteTag(userId: number, tagId: number, knex: Knex) {
}) })
} }
export async function getTag(userId: number, tagId: number, knex: Knex): Promise<TagWithDetails> { export async function getTag(userId: number, tagId: number, knex: Knex): Promise<(Tag & TagDetails & Name)> {
const tagPromise: Promise<TagWithRefsWithId | undefined> = const tagPromise: Promise<(Tag & Id & Name & TagRefs) | null> =
knex.select(['id', 'name', 'parentId']) knex.select(['id', 'name', 'parentId'])
.from('tags') .from('tags')
.where({ 'user': userId }) .where({ 'user': userId })
.where({ 'id': tagId }) .where({ 'id': tagId })
.then((r: TagWithRefsWithId[] | undefined) => r ? r[0] : undefined); .then((r: (Id & Name & TagRefs)[] | undefined) => r ? r[0] : null)
.then((r: (Id & Name & TagRefs) | null) => {
if (r) {
return { ...r, mbApi_typename: 'tag'};
}
return null;
})
const parentPromise: Promise<TagWithId | null> = const parentPromise: Promise<(Tag & Id & Name & TagDetails) | null> =
tagPromise tagPromise
.then((r: TagWithRefsWithId | undefined) => .then((r: (Tag & Id & Name & TagRefs) | null) =>
(r && r.parentId) ? ( (r && r.parentId) ? (
getTag(userId, r.parentId, knex) getTag(userId, r.parentId, knex)
.then((rr: TagWithDetails | null) => rr ? { ...rr, id: r.parentId || 0 } : null) .then((rr: (Tag & Name & TagDetails) | null) =>
rr ? { ...rr, id: r.parentId || 0 } : null)
) : null ) : null
) )
const [maybeTag, maybeParent] = await Promise.all([tagPromise, parentPromise]); const [maybeTag, maybeParent] = await Promise.all([tagPromise, parentPromise]);
if (maybeTag) { if (maybeTag) {
let result: TagWithDetails = { let result: (Tag & Name & TagDetails) = {
mbApi_typename: "tag", mbApi_typename: "tag",
name: maybeTag.name, name: maybeTag.name,
parent: maybeParent, parent: maybeParent,
@ -154,7 +161,7 @@ export async function getTag(userId: number, tagId: number, knex: Knex): Promise
} }
} }
export async function modifyTag(userId: number, tagId: number, tag: TagBaseWithRefs, knex: Knex): Promise<void> { export async function modifyTag(userId: number, tagId: number, tag: Tag, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => { await knex.transaction(async (trx) => {
// Start retrieving the parent tag. // Start retrieving the parent tag.
const parentTagIdPromise: Promise<number | undefined | null> = tag.parentId ? const parentTagIdPromise: Promise<number | undefined | null> = tag.parentId ?

@ -1,5 +1,5 @@
import Knex from "knex"; import Knex from "knex";
import { Track, TrackBaseWithRefs, TrackWithDetails, TrackWithRefs } from "../../client/src/api/api"; import { Track, TrackRefs, TrackDetails, Id, Name, StoreLinks, Tag, Album, Artist, TagRefs } from "../../client/src/api/api";
import * as api from '../../client/src/api/api'; import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson"; import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types"; import { DBError, DBErrorKind } from "../endpoints/types";
@ -8,10 +8,10 @@ var _ = require('lodash')
// Returns an track with details, or null if not found. // Returns an track with details, or null if not found.
export async function getTrack(id: number, userId: number, knex: Knex): export async function getTrack(id: number, userId: number, knex: Knex):
Promise<Track> { Promise<Track & Name & StoreLinks & TrackDetails> {
// Start transfers for tracks, tags and artists. // Start transfers for tracks, tags and artists.
// Also request the track itself. // Also request the track itself.
const tagsPromise: Promise<api.TagWithId[]> = const tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
knex.select('tagId') knex.select('tagId')
.from('tracks_tags') .from('tracks_tags')
.where({ 'trackId': id }) .where({ 'trackId': id })
@ -20,9 +20,13 @@ export async function getTrack(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId']) knex.select(['id', 'name', 'parentId'])
.from('tags') .from('tags')
.whereIn('id', ids) .whereIn('id', ids)
.then((tags: (Id & Name & TagRefs)[]) =>
tags.map((tag : (Id & Name & TagRefs)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
); );
const artistsPromise: Promise<api.ArtistWithId[]> = const artistsPromise: Promise<(Artist & Id & Name & StoreLinks)[]> =
knex.select('artistId') knex.select('artistId')
.from('tracks_artists') .from('tracks_artists')
.where({ 'trackId': id }) .where({ 'trackId': id })
@ -31,24 +35,35 @@ export async function getTrack(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks']) knex.select(['id', 'name', 'storeLinks'])
.from('artists') .from('artists')
.whereIn('id', ids) .whereIn('id', ids)
.then((artists: (Id & Name & StoreLinks)[]) =>
artists.map((artist : (Id & Name & StoreLinks)) =>
{ return {...artist, mbApi_typename: "artist"}}
))
); );
const trackPromise: Promise<api.Track | undefined> = const trackPromise: Promise<(Track & StoreLinks & Name) | undefined> =
knex.select('name', 'storeLinks') knex.select('name', 'storeLinks', 'album')
.from('tracks') .from('tracks')
.where({ 'user': userId }) .where({ 'user': userId })
.where({ id: id }) .where({ id: id })
.then((tracks: any) => tracks[0]); .then((tracks: any) => { return {
name: tracks[0].name,
storeLinks: tracks[0].storeLinks,
albumId: tracks[0].album,
mbApi_typename: 'track'
}});
const albumPromise: Promise<api.AlbumWithId | null> = const albumPromise: Promise<(Album & Name & Id & StoreLinks) | null> =
trackPromise trackPromise
.then((t: api.Track | undefined) => .then((t: api.Track | undefined) =>
t ? knex.select('id', 'name', 'storeLinks') t ? knex.select('id', 'name', 'storeLinks')
.from('albums') .from('albums')
.where({ 'user': userId }) .where({ 'user': userId })
.where({ id: t.albumId }) .where({ id: t.albumId })
.then((albums: any) => albums.length > 0 ? albums[0] : null) .then((albums: any) => albums.length > 0 ?
{...albums[0], mpApi_typename: 'album' }
: null)
: (() => null)() : (() => null)()
) )
@ -60,9 +75,9 @@ export async function getTrack(id: number, userId: number, knex: Knex):
return { return {
mbApi_typename: 'track', mbApi_typename: 'track',
name: track['name'], name: track['name'],
artists: artists as api.ArtistWithId[], artists: artists || [],
tags: tags as api.TagWithId[], tags: tags || [],
album: album as api.AlbumWithId | null, album: album || null,
storeLinks: asJson(track['storeLinks'] || []), storeLinks: asJson(track['storeLinks'] || []),
}; };
} else { } else {
@ -71,7 +86,7 @@ export async function getTrack(id: number, userId: number, knex: Knex):
} }
// Returns the id of the created track. // Returns the id of the created track.
export async function createTrack(userId: number, track: TrackWithRefs, knex: Knex): Promise<number> { export async function createTrack(userId: number, track: (Track & Name & TrackRefs), knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => { return await knex.transaction(async (trx) => {
// Start retrieving artists. // Start retrieving artists.
@ -152,7 +167,7 @@ export async function createTrack(userId: number, track: TrackWithRefs, knex: Kn
}) })
} }
export async function modifyTrack(userId: number, trackId: number, track: TrackBaseWithRefs, knex: Knex): Promise<void> { export async function modifyTrack(userId: number, trackId: number, track: Track, knex: Knex): Promise<void> {
await knex.transaction(async (trx) => { await knex.transaction(async (trx) => {
// Start retrieving the track itself. // Start retrieving the track itself.
const trackIdPromise: Promise<number | undefined> = const trackIdPromise: Promise<number | undefined> =

@ -2,7 +2,6 @@ import * as api from '../../client/src/api/api';
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types'; import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types';
import Knex from 'knex'; import Knex from 'knex';
import asJson from '../lib/asJson'; import asJson from '../lib/asJson';
import { AlbumWithDetails } from '../../client/src/api/api';
import { createAlbum, deleteAlbum, getAlbum, modifyAlbum } from '../db/Album'; import { createAlbum, deleteAlbum, getAlbum, modifyAlbum } from '../db/Album';
import { GetArtist } from './Artist'; import { GetArtist } from './Artist';

Loading…
Cancel
Save