From b27176ae66df400e0be3a018e93e71d893abca39 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 20 Oct 2021 10:17:55 +0200 Subject: [PATCH] Finished a refactoring of the resource type system. Runs again. Patch requests fail though. --- client/src/api/endpoints/data.ts | 18 +- client/src/api/endpoints/query.ts | 14 +- client/src/api/endpoints/resources.ts | 79 +++--- client/src/api/types/resources.ts | 233 ++++++++---------- client/src/components/tables/ResultsTable.tsx | 54 ++-- client/src/components/windows/Windows.tsx | 4 - .../components/windows/album/AlbumWindow.tsx | 11 +- .../windows/artist/ArtistWindow.tsx | 10 +- .../components/windows/query/QueryWindow.tsx | 9 +- .../src/components/windows/tag/TagWindow.tsx | 10 +- .../components/windows/track/TrackWindow.tsx | 6 +- client/src/lib/backend/albums.tsx | 3 + client/src/lib/backend/artists.tsx | 3 + client/src/lib/backend/queries.tsx | 8 +- client/src/lib/backend/tracks.tsx | 3 + client/src/lib/trackGetters.tsx | 28 --- server/db/Album.ts | 36 ++- server/db/Artist.ts | 38 ++- server/db/Data.ts | 28 +-- server/db/Query.ts | 20 +- server/db/Tag.ts | 27 +- server/db/Track.ts | 43 ++-- server/endpoints/Album.ts | 1 - 23 files changed, 342 insertions(+), 344 deletions(-) delete mode 100644 client/src/lib/trackGetters.tsx diff --git a/client/src/api/endpoints/data.ts b/client/src/api/endpoints/data.ts index e94f97b..87ffa5f 100644 --- a/client/src/api/endpoints/data.ts +++ b/client/src/api/endpoints/data.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); } diff --git a/client/src/api/endpoints/query.ts b/client/src/api/endpoints/query.ts index dbdf8da..f7b2efa 100644 --- a/client/src/api/endpoints/query.ts +++ b/client/src/api/endpoints/query.ts @@ -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. diff --git a/client/src/api/endpoints/resources.ts b/client/src/api/endpoints/resources.ts index 585bd15..79259e3 100644 --- a/client/src/api/endpoints/resources.ts +++ b/client/src/api/endpoints/resources.ts @@ -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"; diff --git a/client/src/api/types/resources.ts b/client/src/api/types/resources.ts index ff5d023..e4c11fe 100644 --- a/client/src/api/types/resources.ts +++ b/client/src/api/types/resources.ts @@ -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. diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 4f605d3..319aa37 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -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: { - {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; }) diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index 27d2e8b..e2384ba 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -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, } }, diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx index 7c4535c..27e90a8 100644 --- a/client/src/components/windows/album/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -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 { 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: { {props.state.tracksOnAlbum && } {!props.state.tracksOnAlbum && } diff --git a/client/src/components/windows/artist/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx index 0b49e06..f5b23f5 100644 --- a/client/src/components/windows/artist/ArtistWindow.tsx +++ b/client/src/components/windows/artist/ArtistWindow.tsx @@ -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 { 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: { {props.state.tracksByArtist && } {!props.state.tracksByArtist && } diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx index 7f3db2e..c140cdb 100644 --- a/client/src/components/windows/query/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -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: { > {loading && } diff --git a/client/src/components/windows/tag/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx index 5f9a12f..213a3ac 100644 --- a/client/src/components/windows/tag/TagWindow.tsx +++ b/client/src/components/windows/tag/TagWindow.tsx @@ -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 { 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: { {props.state.tracksWithTag && } {!props.state.tracksWithTag && } diff --git a/client/src/components/windows/track/TrackWindow.tsx b/client/src/components/windows/track/TrackWindow.tsx index 14a76f8..dc3abe6 100644 --- a/client/src/components/windows/track/TrackWindow.tsx +++ b/client/src/components/windows/track/TrackWindow.tsx @@ -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 = {metadata?.name || "(Unknown title)"} - const artists = metadata?.artists && metadata?.artists.map((artist: ArtistMetadata) => { + const artists = metadata?.artists && metadata?.artists.map((artist: (serverApi.Artist & serverApi.Name)) => { return {artist.name} diff --git a/client/src/lib/backend/albums.tsx b/client/src/lib/backend/albums.tsx index 90d01db..b5f9f0b 100644 --- a/client/src/lib/backend/albums.tsx +++ b/client/src/lib/backend/albums.tsx @@ -3,6 +3,9 @@ import { GetAlbumResponse } from '../../api/api'; import backendRequest from './request'; export async function getAlbum(id: number): Promise { + 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)); diff --git a/client/src/lib/backend/artists.tsx b/client/src/lib/backend/artists.tsx index b98fd9e..4112b2f 100644 --- a/client/src/lib/backend/artists.tsx +++ b/client/src/lib/backend/artists.tsx @@ -3,6 +3,9 @@ import { GetArtistResponse } from '../../api/api'; import backendRequest from './request'; export async function getArtist(id: number): Promise { + 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)); diff --git a/client/src/lib/backend/queries.tsx b/client/src/lib/backend/queries.tsx index b94e4e6..b107103 100644 --- a/client/src/lib/backend/queries.tsx +++ b/client/src/lib/backend/queries.tsx @@ -51,7 +51,7 @@ export async function queryArtists( offset: number | undefined, limit: number | undefined, responseType: serverApi.QueryResponseType, -): Promise { +): Promise { 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 { +): Promise { 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 { +): Promise { 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 { +): Promise { let r = await queryItems([serverApi.ResourceType.Tag], query, offset, limit, responseType); return r.tags; } \ No newline at end of file diff --git a/client/src/lib/backend/tracks.tsx b/client/src/lib/backend/tracks.tsx index 6c60d7d..51a9a80 100644 --- a/client/src/lib/backend/tracks.tsx +++ b/client/src/lib/backend/tracks.tsx @@ -2,6 +2,9 @@ import * as serverApi from '../../api/api'; import backendRequest from './request'; export async function getTrack(id: number): Promise { + 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)); diff --git a/client/src/lib/trackGetters.tsx b/client/src/lib/trackGetters.tsx deleted file mode 100644 index 1c4a709..0000000 --- a/client/src/lib/trackGetters.tsx +++ /dev/null @@ -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)); - }, -} \ No newline at end of file diff --git a/server/db/Album.ts b/server/db/Album.ts index 008e691..ba21e14 100644 --- a/server/db/Album.ts +++ b/server/db/Album.ts @@ -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 { + Promise<(Album & AlbumDetails & StoreLinks & Name)> { // Start transfers for tracks, tags and artists. // Also request the album itself. - const tagsPromise: Promise = + 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 = + 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 = + 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 = + 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 { +export async function createAlbum(userId: number, album: (Album & Name & AlbumRefs), knex: Knex): Promise { return await knex.transaction(async (trx) => { // Start retrieving artists. const artistIdsPromise: Promise = @@ -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 { +export async function modifyAlbum(userId: number, albumId: number, album: Album, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the album itself. const albumIdPromise: Promise = diff --git a/server/db/Artist.ts b/server/db/Artist.ts index 41a0e1c..58b67e8 100644 --- a/server/db/Artist.ts +++ b/server/db/Artist.ts @@ -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 { + Promise<(Artist & ArtistDetails & Name & StoreLinks)> { // Start transfers for tags and albums. // Also request the artist itself. - const tagsPromise: Promise = + 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 = + 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 = + 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 = + 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 { +export async function createArtist(userId: number, artist: (Artist & ArtistRefs & Name), knex: Knex): Promise { return await knex.transaction(async (trx) => { // Start retrieving albums. const albumIdsPromise: Promise = @@ -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 { +export async function modifyArtist(userId: number, artistId: number, artist: Artist, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the artist itself. const artistIdPromise: Promise = diff --git a/server/db/Data.ts b/server/db/Data.ts index f4291f9..ce342a5 100644 --- a/server/db/Data.ts +++ b/server/db/Data.ts @@ -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 = + 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 = + 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 = + 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 = + 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 { 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 { diff --git a/server/db/Query.ts b/server/db/Query.ts index 3062a9e..11c1b96 100644 --- a/server/db/Query.ts +++ b/server/db/Query.ts @@ -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 { +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 { +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 { +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 { +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 { return await resolveTag(tag); } -export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex): Promise { +export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex): + Promise { const trackLimit = q.offsetsLimits.trackLimit; const trackOffset = q.offsetsLimits.trackOffset; const tagLimit = q.offsetsLimits.tagLimit; diff --git a/server/db/Tag.ts b/server/db/Tag.ts index dcc091d..f513288 100644 --- a/server/db/Tag.ts +++ b/server/db/Tag.ts @@ -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 { +export async function createTag(userId: number, tag: (Tag & Name & TagRefs), knex: Knex): Promise { 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 { - const tagPromise: Promise = +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 = + 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 { +export async function modifyTag(userId: number, tagId: number, tag: Tag, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the parent tag. const parentTagIdPromise: Promise = tag.parentId ? diff --git a/server/db/Track.ts b/server/db/Track.ts index 9761e28..e1307e8 100644 --- a/server/db/Track.ts +++ b/server/db/Track.ts @@ -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 { + Promise { // Start transfers for tracks, tags and artists. // Also request the track itself. - const tagsPromise: Promise = + 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 = + 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 = - 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 = + 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 { +export async function createTrack(userId: number, track: (Track & Name & TrackRefs), knex: Knex): Promise { 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 { +export async function modifyTrack(userId: number, trackId: number, track: Track, knex: Knex): Promise { await knex.transaction(async (trx) => { // Start retrieving the track itself. const trackIdPromise: Promise = diff --git a/server/endpoints/Album.ts b/server/endpoints/Album.ts index 3d23539..aa3b5ec 100644 --- a/server/endpoints/Album.ts +++ b/server/endpoints/Album.ts @@ -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';