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
// 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.
// Each object has an ID and references others by ID.
// Any object referenced by ID also has a reverse reference.
// In other words, if A references B, B must also reference A.
export interface DBDataFormat {
tracks: TrackWithRefsWithId[],
albums: AlbumWithRefsWithId[],
artists: ArtistWithRefsWithId[],
tags: TagWithRefsWithId[],
tracks: (Track & Id & TrackRefs)[],
albums: (Album & Id & AlbumRefs)[],
artists: (Artist & Id & ArtistRefs)[],
tags: (Tag & Id & TagRefs)[],
}
// 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 &&
'tags' in v &&
v.tracks.reduce((prev: boolean, cur: any) => {
return prev && isTrackWithRefs(cur);
return prev && isTrackRefs(cur);
}, true) &&
v.albums.reduce((prev: boolean, cur: any) => {
return prev && isAlbumWithRefs(cur);
return prev && isAlbumRefs(cur);
}, true) &&
v.artists.reduce((prev: boolean, cur: any) => {
return prev && isArtistWithRefs(cur);
return prev && isArtistRefs(cur);
}, true) &&
v.tags.reduce((prev: boolean, cur: any) => {
return prev && isTagWithRefs(cur);
return prev && isTagRefs(cur);
}, true);
}

@ -1,6 +1,6 @@
// 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';
@ -77,11 +77,15 @@ export interface QueryRequest {
}
// 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 {
tracks: TrackWithId[] | number[] | number, // Details | IDs | count, depending on QueryResponseType
artists: ArtistWithId[] | number[] | number,
tags: TagWithId[] | number[] | number,
albums: AlbumWithId[] | number[] | number,
tracks: QueryResponseTrackDetails[] | number[] | number, // Details | IDs | count, depending on QueryResponseType
artists: QueryResponseArtistDetails[] | number[] | number,
tags: QueryResponseTagDetails[] | number[] | number,
albums: QueryResponseAlbumDetails[] | number[] | number,
}
// Note: use -1 as an infinity limit.

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

@ -6,195 +6,168 @@ export enum ResourceType {
Tag = "tag"
}
export interface TrackBase {
mbApi_typename: "track",
export interface Id {
id: number,
}
name?: string,
storeLinks?: string[],
albumId?: number | null,
export function isId(q : any) : q is Id {
return 'id' in q;
}
export interface TrackBaseWithRefs extends TrackBase {
artistIds?: number[],
albumId?: number | null,
tagIds?: number[],
export interface Name {
name: string,
}
export interface TrackBaseWithDetails extends TrackBase {
artists: ArtistWithId[],
album: AlbumWithId | null,
tags: TagWithId[],
export function isName(q : any) : q is Name {
return 'name' in q;
}
export interface TrackWithDetails extends TrackBaseWithDetails {
name: string,
export interface StoreLinks {
storeLinks: string[],
}
export interface TrackWithRefs extends TrackBaseWithRefs {
name: string,
artistIds: number[],
export interface TrackRefs {
albumId: number | null,
artistIds: number[],
tagIds: number[],
}
export interface Track extends TrackBase {
name: string,
album: AlbumWithId | null,
artists: ArtistWithId[],
tags: TagWithId[],
}
export interface TrackWithRefsWithId extends TrackWithRefs {
id: number,
}
export interface TrackWithDetailsWithId extends TrackWithDetails {
id: number,
export interface TrackDetails {
artists: (Artist & Id)[],
album: (Album & Id) | null,
tags: (Tag & Id)[],
}
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";
}
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",
name?: string,
id?: number,
storeLinks?: string[],
}
export interface ArtistBaseWithRefs extends ArtistBase {
albumIds?: number[],
tagIds?: number[],
trackIds?: number[],
albums?: (Album & Id)[],
tags?: (Tag & Id)[],
tracks?: (Track & Id)[],
}
export interface ArtistBaseWithDetails extends ArtistBase {
albums: AlbumWithId[],
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 {
export function isArtist(q: any): q is Artist {
return q.mbApi_typename && q.mbApi_typename === "artist";
}
export function isArtistBaseWithRefs(q: any): q is ArtistBaseWithRefs {
return isArtistBase(q);
}
export function isArtistWithRefs(q: any): q is ArtistWithRefs {
return isArtistBaseWithRefs(q) && "name" in q;
export function isArtistRefs(q: any): q is ArtistRefs {
return isTag(q) && 'albumIds' in q && 'trackIds' in q && 'tagIds' 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",
name?: string,
id?: number,
storeLinks?: string[],
}
export interface AlbumBaseWithRefs extends AlbumBase {
artistIds?: number[],
trackIds?: number[],
tagIds?: number[],
artists?: (Artist & Id)[],
tracks?: (Track & Id)[],
tags?: (Tag & Id)[],
}
export interface AlbumBaseWithDetails extends AlbumBase {
artists: ArtistWithId[],
tracks: TrackWithId[],
tags: TagWithId[],
}
export interface AlbumWithDetails extends AlbumBaseWithDetails {
name: string,
}
export interface AlbumWithRefs extends AlbumBaseWithRefs {
name: string,
export interface AlbumRefs {
artistIds: number[],
trackIds: number[],
tagIds: number[],
}
export interface Album extends AlbumBase {
name: string,
artists: ArtistWithId[],
}
export interface AlbumWithRefsWithId extends AlbumWithRefs {
id: number,
}
export interface AlbumWithDetailsWithId extends AlbumWithDetails {
id: number,
}
export interface AlbumWithId extends Album {
id: number,
export interface AlbumDetails {
artists: (Artist & Id)[],
tracks: (Track & Id)[],
tags: (Tag & Id)[],
}
export function isAlbumBase(q: any): q is AlbumBase {
export function isAlbum(q: any): q is Album {
return q.mbApi_typename && q.mbApi_typename === "album";
}
export function isAlbumBaseWithRefs(q: any): q is AlbumBaseWithRefs {
return isAlbumBase(q);
}
export function isAlbumWithRefs(q: any): q is AlbumWithRefs {
return isAlbumBaseWithRefs(q) && "name" in q;
export function isAlbumRefs(q: any): q is AlbumRefs {
return isTag(q) && 'artistIds' in q && 'trackIds' in q && 'tagIds' 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",
name?: string,
}
export interface TagBaseWithRefs extends TagBase {
id?: number,
parentId?: number | null,
parent?: (Tag & Id) | null,
}
export interface TagBaseWithDetails extends TagBase {
parent?: TagWithId | null,
}
export interface TagWithDetails extends TagBaseWithDetails {
name: string,
}
export interface TagWithRefs extends TagBaseWithRefs {
name: string,
export interface TagRefs {
parentId: number | null,
}
export interface Tag extends TagBaseWithDetails {
name: string,
}
export interface TagWithRefsWithId extends TagWithRefs {
id: number,
}
export interface TagWithDetailsWithId extends TagWithDetails {
id: number,
}
export interface TagWithId extends Tag {
id: number,
export interface TagDetails {
parent: (Tag & Id) | null,
}
export function isTagBase(q: any): q is TagBase {
export function isTag(q: any): q is Tag {
return q.mbApi_typename && q.mbApi_typename === "tag";
}
export function isTagBaseWithRefs(q: any): q is TagBaseWithRefs {
return isTagBase(q);
}
export function isTagWithRefs(q: any): q is TagWithRefs {
return isTagBaseWithRefs(q) && "name" in q;
export function isTagRefs(q: any): q is TagRefs {
return isTag(q) && 'parentId' in q;
}
export function isTagDetails(q: any): q is TagDetails {
return isTag(q) && 'parent' in q;
}
// There are several implemented integration solutions,
// 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 stringifyList from '../../lib/stringifyList';
import { useHistory } from 'react-router';
import { Artist, QueryResponseTrackDetails, Tag, Name } from '../../api/api';
export interface TrackGetters {
getName: (track: any) => string,
getId: (track: any) => number,
getArtistNames: (track: any) => string[],
getArtistIds: (track: any) => number[],
getAlbumName: (track: any) => string | undefined,
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.
function getTagNames (track: QueryResponseTrackDetails) : string[][] {
// 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: 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: {
tracks: any[],
trackGetters: TrackGetters,
tracks: QueryResponseTrackDetails[]
}) {
const history = useHistory();
@ -44,16 +55,19 @@ export default function TrackTable(props: {
</TableRow>
</TableHead>
<TableBody>
{props.tracks.map((track: any) => {
const name = props.trackGetters.getName(track);
{props.tracks.map((track: QueryResponseTrackDetails) => {
const name = track.name;
// 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 mainArtistId = props.trackGetters.getArtistIds(track)[0];
const album = props.trackGetters.getAlbumName(track);
const albumId = props.trackGetters.getAlbumId(track);
const trackId = props.trackGetters.getId(track);
const tagIds = props.trackGetters.getTagIds(track);
const mainArtistId =
(track.artists.length > 0 && track.artists[0].id) || undefined;
const album = track.album?.name || undefined;
const albumId = track.album?.id || undefined;
const trackId = track.id;
const tagIds = getTagIds(track);
const onClickArtist = () => {
history.push('/artist/' + mainArtistId);
@ -71,7 +85,7 @@ export default function TrackTable(props: {
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) => {
return (idx === 0) ? e : " / " + e;
})

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

@ -6,16 +6,16 @@ import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable, { TrackGetters } from '../../tables/ResultsTable';
import TrackTable from '../../tables/ResultsTable';
import { modifyAlbum } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, queryTracks } from '../../../lib/backend/queries';
import { trackGetters } from '../../../lib/trackGetters';
import { useParams } from 'react-router';
import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request';
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 interface AlbumWindowState extends WindowState {
@ -23,7 +23,6 @@ export interface AlbumWindowState extends WindowState {
metadata: AlbumMetadata | null,
pendingChanges: AlbumMetadataChanges | null,
tracksOnAlbum: any[] | null,
trackGetters: TrackGetters,
}
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(
{
a: QueryLeafBy.AlbumId,
@ -65,7 +64,6 @@ export default function AlbumWindow(props: {}) {
id: parseInt(id),
metadata: null,
pendingChanges: null,
trackGetters: trackGetters,
tracksOnAlbum: null,
});
@ -198,7 +196,6 @@ export function AlbumWindowControlled(props: {
</Box>
{props.state.tracksOnAlbum && <TrackTable
tracks={props.state.tracksOnAlbum}
trackGetters={props.state.trackGetters}
/>}
{!props.state.tracksOnAlbum && <CircularProgress />}
</Box>

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

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

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

@ -16,7 +16,7 @@ import EditIcon from '@material-ui/icons/Edit';
import { modifyTrack } from '../../../lib/saveChanges';
import { getTrack } from '../../../lib/backend/tracks';
export type TrackMetadata = serverApi.TrackWithId;
export type TrackMetadata = serverApi.QueryResponseTrackDetails;
export interface TrackWindowState extends WindowState {
id: number,
@ -60,7 +60,7 @@ export function TrackWindowControlled(props: {
useEffect(() => {
if (metadata === null) {
getTrack(trackId)
.then((m: serverApi.Track) => {
.then((m: serverApi.GetTrackResponse) => {
dispatch({
type: TrackWindowStateActions.SetMetadata,
value: m
@ -71,7 +71,7 @@ export function TrackWindowControlled(props: {
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>
{artist.name}
</Typography>

@ -3,6 +3,9 @@ import { GetAlbumResponse } from '../../api/api';
import backendRequest from './request';
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}`))
if (!response.ok) {
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';
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}`))
if (!response.ok) {
throw new Error("Response to artist request not OK: " + JSON.stringify(response));

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

@ -2,6 +2,9 @@ import * as serverApi from '../../api/api';
import backendRequest from './request';
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}`))
if (!response.ok) {
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 { 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 asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
@ -9,12 +9,12 @@ var _ = require('lodash');
// Returns an album with details, or null if not found.
export async function getAlbum(id: number, userId: number, knex: Knex):
Promise<AlbumWithDetails> {
Promise<(Album & AlbumDetails & StoreLinks & Name)> {
// Start transfers for tracks, tags and artists.
// Also request the album itself.
const tagsPromise: Promise<api.TagWithId[]> =
const tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
knex.select('tagId')
.from('albums_tags')
.where({ 'albumId': id })
@ -23,15 +23,21 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId'])
.from('tags')
.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'])
.from('tracks')
.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')
.from('artists_albums')
.where({ 'albumId': id })
@ -40,14 +46,18 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks'])
.from('artists')
.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')
.from('albums')
.where({ 'user': userId })
.where({ id: id })
.then((albums: any) => albums[0]);
.then((albums: any) => { return { ...albums[0], mbApi_typename: 'album' }});
// Wait for the requests to finish.
const [album, tags, tracks, artists] =
@ -57,9 +67,9 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
return {
mbApi_typename: 'album',
name: album['name'],
artists: artists as api.ArtistWithId[],
tags: tags as api.TagWithId[],
tracks: tracks as api.TrackWithId[],
artists: artists || [],
tags: tags || [],
tracks: tracks || [],
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.
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) => {
// Start retrieving artists.
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) => {
// Start retrieving the album itself.
const albumIdPromise: Promise<number | undefined> =

@ -1,5 +1,5 @@
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 asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
@ -8,10 +8,10 @@ var _ = require('lodash')
// Returns an artist with details, or null if not found.
export async function getArtist(id: number, userId: number, knex: Knex):
Promise<ArtistWithDetails> {
Promise<(Artist & ArtistDetails & Name & StoreLinks)> {
// Start transfers for tags and albums.
// Also request the artist itself.
const tagsPromise: Promise<api.TagWithId[]> =
const tagsPromise: Promise<(Tag & Name & Id & TagRefs)[]> =
knex.select('tagId')
.from('artists_tags')
.where({ 'artistId': id })
@ -20,9 +20,13 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId'])
.from('tags')
.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')
.from('artists_albums')
.where({ 'artistId': id })
@ -31,9 +35,13 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks'])
.from('albums')
.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')
.from('tracks_artists')
.where({ 'artistId': id })
@ -42,26 +50,30 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks'])
.from('tracks')
.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')
.from('artists')
.where({ 'user': userId })
.where({ id: id })
.then((artists: any) => artists[0]);
.then((artists: any) => { return { ...artists[0], mbApi_typename: 'artist' } });
// Wait for the requests to finish.
const [artist, tags, albums, tracks] =
await Promise.all([artistPromise, tagsPromise, albumsPromise, tracksPromise]);
if (artist) {
if (artist && artist['name']) {
return {
mbApi_typename: 'artist',
name: artist['name'],
albums: albums as api.AlbumWithId[],
tags: tags as api.TagWithId[],
tracks: tracks as api.TrackWithId[],
albums: albums,
tags: tags,
tracks: tracks,
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.
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) => {
// Start retrieving albums.
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) => {
// Start retrieving the artist itself.
const artistIdPromise: Promise<number | undefined | null> =

@ -1,5 +1,5 @@
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 asJson from "../lib/asJson";
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.
// Fetch the links separately.
let tracksPromise: Promise<api.TrackWithRefsWithId[]> =
let tracksPromise: Promise<(Track & Id & Name & StoreLinks & TrackRefs)[]> =
knex.select('id', 'name', 'storeLinks', 'album')
.from('tracks')
.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')
.from('albums')
.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')
.from('artists')
.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')
.from('tags')
.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.
tracksArtists.forEach((v: [number, number]) => {
let [trackId, artistId] = v;
tracks.find((t: TrackWithRefsWithId) => t.id === trackId)?.artistIds.push(artistId);
artists.find((a: ArtistWithRefsWithId) => a.id === artistId)?.trackIds.push(trackId);
tracks.find((t: (Track & Id & TrackRefs)) => t.id === trackId)?.artistIds.push(artistId);
artists.find((a: (Artist & Id & ArtistRefs)) => a.id === artistId)?.trackIds.push(trackId);
})
tracks.forEach((t: api.TrackWithRefsWithId) => {
albums.find((a: AlbumWithRefsWithId) => t.albumId && a.id === t.albumId)?.trackIds.push(t.id);
tracks.forEach((t: (Track & Id & TrackRefs)) => {
albums.find((a: (Album & Id & AlbumRefs)) => t.albumId && a.id === t.albumId)?.trackIds.push(t.id);
})
tracksTags.forEach((v: [number, number]) => {
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]) => {
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]) => {
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]) => {
let [albumId, artistId] = v;
artists.find((t: ArtistWithRefsWithId) => t.id === artistId)?.albumIds.push(albumId);
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.artistIds.push(artistId);
artists.find((t: (Artist & Id & ArtistRefs)) => t.id === artistId)?.albumIds.push(albumId);
albums.find((t: (Album & Id & AlbumRefs)) => t.id === albumId)?.artistIds.push(artistId);
})
return {

@ -2,9 +2,10 @@ import * as api from '../../client/src/api/api';
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from '../endpoints/types';
import Knex from 'knex';
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 {
return <api.TagWithDetailsWithId>{
export function toApiTag(dbObj: any): api.QueryResponseTagDetails {
return {
mbApi_typename: "tag",
id: dbObj['tags.id'],
name: dbObj['tags.name'],
@ -13,8 +14,8 @@ export function toApiTag(dbObj: any): api.TagWithDetailsWithId {
};
}
export function toApiArtist(dbObj: any): api.ArtistWithId {
return <api.ArtistWithId>{
export function toApiArtist(dbObj: any): api.QueryResponseArtistDetails {
return {
mbApi_typename: "artist",
id: dbObj['artists.id'],
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 {
return <api.TrackWithDetailsWithId>{
export function toApiTrack(dbObj: any, artists: any[], tags: any[], albums: any[]): api.QueryResponseTrackDetails {
return {
mbApi_typename: "track",
id: dbObj['tracks.id'],
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 {
return <api.AlbumWithId>{
export function toApiAlbum(dbObj: any): api.QueryResponseAlbumDetails {
return {
mbApi_typename: "album",
id: dbObj['albums.id'],
name: dbObj['albums.name'],
@ -351,7 +352,8 @@ async function getFullTag(knex: Knex, userId: number, tag: any): Promise<any> {
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 trackOffset = q.offsetsLimits.trackOffset;
const tagLimit = q.offsetsLimits.tagLimit;

@ -1,7 +1,7 @@
import Knex from "knex";
import { isConstructorDeclaration } from "typescript";
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 { makeNotFoundError } from "./common";
@ -35,7 +35,7 @@ export async function getTagChildrenRecursive(id: number,
}
// 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) => {
// If applicable, retrieve the parent tag.
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> {
const tagPromise: Promise<TagWithRefsWithId | undefined> =
export async function getTag(userId: number, tagId: number, knex: Knex): Promise<(Tag & TagDetails & Name)> {
const tagPromise: Promise<(Tag & Id & Name & TagRefs) | null> =
knex.select(['id', 'name', 'parentId'])
.from('tags')
.where({ 'user': userId })
.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
.then((r: TagWithRefsWithId | undefined) =>
.then((r: (Tag & Id & Name & TagRefs) | null) =>
(r && r.parentId) ? (
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
)
const [maybeTag, maybeParent] = await Promise.all([tagPromise, parentPromise]);
if (maybeTag) {
let result: TagWithDetails = {
let result: (Tag & Name & TagDetails) = {
mbApi_typename: "tag",
name: maybeTag.name,
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) => {
// Start retrieving the parent tag.
const parentTagIdPromise: Promise<number | undefined | null> = tag.parentId ?

@ -1,5 +1,5 @@
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 asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
@ -8,10 +8,10 @@ var _ = require('lodash')
// Returns an track with details, or null if not found.
export async function getTrack(id: number, userId: number, knex: Knex):
Promise<Track> {
Promise<Track & Name & StoreLinks & TrackDetails> {
// Start transfers for tracks, tags and artists.
// Also request the track itself.
const tagsPromise: Promise<api.TagWithId[]> =
const tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
knex.select('tagId')
.from('tracks_tags')
.where({ 'trackId': id })
@ -20,9 +20,13 @@ export async function getTrack(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId'])
.from('tags')
.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')
.from('tracks_artists')
.where({ 'trackId': id })
@ -31,24 +35,35 @@ export async function getTrack(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'storeLinks'])
.from('artists')
.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> =
knex.select('name', 'storeLinks')
const trackPromise: Promise<(Track & StoreLinks & Name) | undefined> =
knex.select('name', 'storeLinks', 'album')
.from('tracks')
.where({ 'user': userId })
.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
.then((t: api.Track | undefined) =>
t ? knex.select('id', 'name', 'storeLinks')
.from('albums')
.where({ 'user': userId })
.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)()
)
@ -60,9 +75,9 @@ export async function getTrack(id: number, userId: number, knex: Knex):
return {
mbApi_typename: 'track',
name: track['name'],
artists: artists as api.ArtistWithId[],
tags: tags as api.TagWithId[],
album: album as api.AlbumWithId | null,
artists: artists || [],
tags: tags || [],
album: album || null,
storeLinks: asJson(track['storeLinks'] || []),
};
} else {
@ -71,7 +86,7 @@ export async function getTrack(id: number, userId: number, knex: Knex):
}
// 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) => {
// 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) => {
// Start retrieving the track itself.
const trackIdPromise: Promise<number | undefined> =

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

Loading…
Cancel
Save