Starting to work on rendering tag changes.

pull/24/head
Sander Vocke 5 years ago
parent e81f61637f
commit 1c098ab224
  1. 78
      client/src/components/windows/album/AlbumWindow.tsx
  2. 78
      client/src/components/windows/artist/ArtistWindow.tsx
  3. 55
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  4. 39
      client/src/components/windows/manage_tags/TagChange.tsx
  5. 102
      client/src/components/windows/query/QueryWindow.tsx
  6. 41
      client/src/components/windows/song/SongWindow.tsx
  7. 99
      client/src/components/windows/tag/TagWindow.tsx
  8. 64
      client/src/lib/query/Backend.tsx
  9. 14
      client/src/lib/query/Query.tsx
  10. 31
      server/endpoints/QueryEndpointHandler.ts

@ -8,6 +8,8 @@ import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../../tables/ResultsTable'; import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveAlbumChanges } from '../../../lib/saveChanges'; import { saveAlbumChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, querySongs } from '../../../lib/query/Backend';
var _ = require('lodash'); var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails; export type AlbumMetadata = serverApi.AlbumDetails;
@ -50,38 +52,15 @@ export interface IProps {
} }
export async function getAlbumMetadata(id: number) { export async function getAlbumMetadata(id: number) {
const query = { return (await queryAlbums({
prop: serverApi.QueryElemProperty.albumId, query: {
propOperand: id, a: QueryLeafBy.AlbumId,
propOperator: serverApi.QueryFilterOp.Eq, b: id,
}; leafOp: QueryLeafOp.Equals,
var q: serverApi.QueryRequest = {
query: query,
offsetsLimits: {
albumOffset: 0,
albumLimit: 1,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
}, },
}; offset: 0,
limit: 1,
const requestOpts = { }))[0];
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
let album = json.albums[0];
return album;
})();
} }
export default function AlbumWindow(props: IProps) { export default function AlbumWindow(props: IProps) {
@ -103,36 +82,19 @@ export default function AlbumWindow(props: IProps) {
useEffect(() => { useEffect(() => {
if (props.state.songsOnAlbum) { return; } if (props.state.songsOnAlbum) { return; }
var q: serverApi.QueryRequest = {
query: {
prop: serverApi.QueryElemProperty.albumId,
propOperator: serverApi.QueryFilterOp.Eq,
propOperand: props.state.albumId,
},
offsetsLimits: {
songOffset: 0,
songLimit: 100,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
(async () => { (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const songs = await querySongs({
let json: any = await response.json(); query: {
a: QueryLeafBy.AlbumId,
b: props.state.albumId,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
limit: -1,
});
props.dispatch({ props.dispatch({
type: AlbumWindowStateActions.SetSongs, type: AlbumWindowStateActions.SetSongs,
value: json.songs, value: songs,
}); });
})(); })();
}, [props.state.songsOnAlbum]); }, [props.state.songsOnAlbum]);

@ -8,6 +8,8 @@ import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../../tables/ResultsTable'; import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveArtistChanges } from '../../../lib/saveChanges'; import { saveArtistChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, querySongs } from '../../../lib/query/Backend';
var _ = require('lodash'); var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails; export type ArtistMetadata = serverApi.ArtistDetails;
@ -50,38 +52,15 @@ export interface IProps {
} }
export async function getArtistMetadata(id: number) { export async function getArtistMetadata(id: number) {
const query = { return (await queryArtists({
prop: serverApi.QueryElemProperty.artistId, query: {
propOperand: id, a: QueryLeafBy.ArtistId,
propOperator: serverApi.QueryFilterOp.Eq, b: id,
}; leafOp: QueryLeafOp.Equals,
var q: serverApi.QueryRequest = {
query: query,
offsetsLimits: {
artistOffset: 0,
artistLimit: 1,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
}, },
}; offset: 0,
limit: 1,
const requestOpts = { }))[0];
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
let artist = json.artists[0];
return artist;
})();
} }
export default function ArtistWindow(props: IProps) { export default function ArtistWindow(props: IProps) {
@ -103,36 +82,19 @@ export default function ArtistWindow(props: IProps) {
useEffect(() => { useEffect(() => {
if (props.state.songsByArtist) { return; } if (props.state.songsByArtist) { return; }
var q: serverApi.QueryRequest = {
query: {
prop: serverApi.QueryElemProperty.artistId,
propOperator: serverApi.QueryFilterOp.Eq,
propOperand: props.state.artistId,
},
offsetsLimits: {
songOffset: 0,
songLimit: 100,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
(async () => { (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const songs = await querySongs({
let json: any = await response.json(); query: {
a: QueryLeafBy.ArtistId,
b: props.state.artistId,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
limit: -1,
});
props.dispatch({ props.dispatch({
type: ArtistWindowStateActions.SetSongs, type: ArtistWindowStateActions.SetSongs,
value: json.songs, value: songs,
}); });
})(); })();
}, [props.state.songsByArtist]); }, [props.state.songsByArtist]);

@ -6,24 +6,10 @@ import LoyaltyIcon from '@material-ui/icons/Loyalty';
import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ManageTagMenu from './ManageTagMenu'; import ManageTagMenu from './ManageTagMenu';
import ControlTagChanges, { TagChange, TagChangeType } from './TagChange';
import { queryTags } from '../../../lib/query/Backend';
var _ = require('lodash'); var _ = require('lodash');
export enum TagChangeType {
Delete = "Delete",
Create = "Create",
MoveTo = "MoveTo",
MergeTo = "MergeTo",
Rename = "Rename",
}
export interface TagChange {
type: TagChangeType,
id: number, // MuDBase ID. If not in database yet, negative IDs will be used until submitted.
parent?: number | null, // MuDBase ID. If not in database yet, negative IDs will be used until submitted.
// null refers to the tags root.
name?: string,
}
export interface ManageTagsWindowState extends WindowState { export interface ManageTagsWindowState extends WindowState {
fetchedTags: Record<number, any> | null, fetchedTags: Record<number, any> | null,
pendingChanges: TagChange[], pendingChanges: TagChange[],
@ -68,32 +54,14 @@ export function organiseTags(allTags: Record<number, any>, fromId: number | null
} }
export async function getAllTags() { export async function getAllTags() {
// Build a request to fetch all tags.
var q: serverApi.QueryRequest = {
query: {},
offsetsLimits: {
tagOffset: 0,
tagLimit: 1000,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => { return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
var retval: Record<number, any> = {}; var retval: Record<number, any> = {};
json.tags.forEach((tag: any) => { const tags = await queryTags({
query: undefined,
offset: 0,
limit: -1,
});
tags.forEach((tag: any) => {
retval[tag.tagId] = tag; retval[tag.tagId] = tag;
}); });
return retval; return retval;
@ -305,6 +273,13 @@ export default function ManageTagsWindow(props: IProps) {
> >
<Typography variant="h4">Manage Tags</Typography> <Typography variant="h4">Manage Tags</Typography>
</Box> </Box>
<Box
m={1}
mt={4}
width="80%"
>
<ControlTagChanges changes={props.state.pendingChanges}/>
</Box>
<Box <Box
m={1} m={1}
mt={4} mt={4}

@ -0,0 +1,39 @@
import React from 'react';
import { Typography, Chip } from '@material-ui/core';
export enum TagChangeType {
Delete = "Delete",
Create = "Create",
MoveTo = "MoveTo",
MergeTo = "MergeTo",
Rename = "Rename",
}
export interface TagChange {
type: TagChangeType,
id: number, // MuDBase ID. If not in database yet, negative IDs will be used until submitted.
parent?: number | null, // MuDBase ID. If not in database yet, negative IDs will be used until submitted.
// null refers to the tags root.
name?: string,
}
export function TagChangeDisplay(props: {
change: TagChange,
}) {
const tag = <Chip size="small" label={props.change.id} />;
switch (props.change.type) {
case TagChangeType.Delete:
return <Typography>Delete {tag}</Typography>
default:
throw new Error("Unhandled tag change type")
}
}
export default function ControlTagChanges(props: {
changes: TagChange[],
}) {
return <>
{props.changes.map((change: any) => <TagChangeDisplay change={change} />)}
</>
}

@ -1,11 +1,11 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; import { createMuiTheme, Box, LinearProgress } from '@material-ui/core';
import { QueryElem, toApiQuery } from '../../../lib/query/Query'; import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder'; import QueryBuilder from '../../querybuilder/QueryBuilder';
import * as serverApi from '../../../api'; import * as serverApi from '../../../api';
import SongTable from '../../tables/ResultsTable'; import SongTable from '../../tables/ResultsTable';
import { songGetters } from '../../../lib/songGetters'; import { songGetters } from '../../../lib/songGetters';
import { getArtists, getSongTitles, getAlbums, getTags } from '../../../lib/query/Getters'; import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/query/Backend';
import { grey } from '@material-ui/core/colors'; import { grey } from '@material-ui/core/colors';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
var _ = require('lodash'); var _ = require('lodash');
@ -36,6 +36,56 @@ export enum QueryWindowStateActions {
SetResultsForQuery = "setResultsForQuery", SetResultsForQuery = "setResultsForQuery",
} }
async function getArtistNames(filter: string) {
const artists = await queryArtists({
query: filter.length > 0 ? {
a: QueryLeafBy.ArtistName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
offset: 0,
limit: -1,
});
return [...(new Set([...(artists.map((a:any) => a.name))]))];
}
async function getAlbumNames(filter: string) {
const albums = await queryAlbums({
query: filter.length > 0 ? {
a: QueryLeafBy.AlbumName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
offset: 0,
limit: -1,
});
return [...(new Set([...(albums.map((a:any) => a.name))]))];
}
async function getSongTitles(filter: string) {
const songs = await querySongs({
query: filter.length > 0 ? {
a: QueryLeafBy.SongTitle,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
offset: 0,
limit: -1,
});
return [...(new Set([...(songs.map((s:any) => s.title))]))];
}
async function getTagItems() {
return await queryTags({
query: undefined,
offset: 0,
limit: -1,
});
}
export function QueryWindowReducer(state: QueryWindowState, action: any) { export function QueryWindowReducer(state: QueryWindowState, action: any) {
switch (action.type) { switch (action.type) {
case QueryWindowStateActions.SetQuery: case QueryWindowStateActions.SetQuery:
@ -73,36 +123,18 @@ export default function QueryWindow(props: IProps) {
const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : []; const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : [];
const doQuery = async (_query: QueryElem) => { const doQuery = async (_query: QueryElem) => {
var q: serverApi.QueryRequest = { const songs = await querySongs({
query: toApiQuery(_query), query: _query,
offsetsLimits: { offset: 0,
songOffset: 0, limit: 100, //TODO: pagination
songLimit: 100, });
},
ordering: { if (_.isEqual(query, _query)) {
orderBy: { setResultsForQuery({
type: serverApi.OrderByType.Name, for: _query,
}, results: songs,
ascending: true, })
}, }
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
if (_.isEqual(query, _query)) {
setResultsForQuery({
for: _query,
results: json.songs,
})
}
})();
} }
useEffect(() => { useEffect(() => {
@ -124,10 +156,10 @@ export default function QueryWindow(props: IProps) {
editing={editing} editing={editing}
onChangeEditing={setEditingQuery} onChangeEditing={setEditingQuery}
requestFunctions={{ requestFunctions={{
getArtists: getArtists, getArtists: getArtistNames,
getSongTitles: getSongTitles, getSongTitles: getSongTitles,
getAlbums: getAlbums, getAlbums: getAlbumNames,
getTags: getTags, getTags: getTagItems,
}} }}
/> />
</Box> </Box>

@ -11,6 +11,8 @@ import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import { saveSongChanges } from '../../../lib/saveChanges'; import { saveSongChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { querySongs } from '../../../lib/query/Backend';
export type SongMetadata = serverApi.SongDetails; export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest; export type SongMetadataChanges = serverApi.ModifySongRequest;
@ -47,38 +49,15 @@ export interface IProps {
} }
export async function getSongMetadata(id: number) { export async function getSongMetadata(id: number) {
const query = { return (await querySongs({
prop: serverApi.QueryElemProperty.songId, query: {
propOperand: id, a: QueryLeafBy.SongId,
propOperator: serverApi.QueryFilterOp.Eq, b: id,
}; leafOp: QueryLeafOp.Equals,
var q: serverApi.QueryRequest = {
query: query,
offsetsLimits: {
songOffset: 0,
songLimit: 1,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
}, },
}; offset: 0,
limit: 1,
const requestOpts = { }))[0];
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
let song = json.songs[0];
return song;
})();
} }
export default function SongWindow(props: IProps) { export default function SongWindow(props: IProps) {

@ -8,6 +8,8 @@ import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../../tables/ResultsTable'; import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveTagChanges } from '../../../lib/saveChanges'; import { saveTagChanges } from '../../../lib/saveChanges';
import { queryTags, querySongs } from '../../../lib/query/Backend';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
var _ = require('lodash'); var _ = require('lodash');
export interface FullTagMetadata extends serverApi.TagDetails { export interface FullTagMetadata extends serverApi.TagDetails {
@ -55,49 +57,27 @@ export interface IProps {
} }
export async function getTagMetadata(id: number) { export async function getTagMetadata(id: number) {
const query = { var tag = (await queryTags({
prop: serverApi.QueryElemProperty.tagId, query: {
propOperand: id, a: QueryLeafBy.TagId,
propOperator: serverApi.QueryFilterOp.Eq, b: id,
}; leafOp: QueryLeafOp.Equals,
var q: serverApi.QueryRequest = {
query: query,
offsetsLimits: {
tagOffset: 0,
tagLimit: 1,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
}, },
}; offset: 0,
limit: 1,
const requestOpts = { }))[0];
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // Recursively fetch parent tags to build the full metadata.
body: JSON.stringify(q), if (tag.parentId) {
}; const parent = await getTagMetadata(tag.parentId);
tag.fullName = [...parent.fullName, tag.name];
return (async () => { tag.fullId = [...parent.fullId, tag.tagId];
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) } else {
let json: any = await response.json(); tag.fullName = [tag.name];
let tag = json.tags[0]; tag.fullId = [tag.tagId];
}
// Recursively fetch parent tags to build the full metadata.
if (tag.parentId) {
const parent = await getTagMetadata(tag.parentId);
tag.fullName = [...parent.fullName, tag.name];
tag.fullId = [...parent.fullId, tag.tagId];
} else {
tag.fullName = [tag.name];
tag.fullId = [tag.tagId];
}
return tag; return tag;
})();
} }
export default function TagWindow(props: IProps) { export default function TagWindow(props: IProps) {
@ -119,36 +99,19 @@ export default function TagWindow(props: IProps) {
useEffect(() => { useEffect(() => {
if (props.state.songsWithTag) { return; } if (props.state.songsWithTag) { return; }
var q: serverApi.QueryRequest = {
query: {
prop: serverApi.QueryElemProperty.tagId,
propOperator: serverApi.QueryFilterOp.Eq,
propOperand: props.state.tagId,
},
offsetsLimits: {
songOffset: 0,
songLimit: 100,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
(async () => { (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const songs = await querySongs({
let json: any = await response.json(); query: {
a: QueryLeafBy.TagId,
b: props.state.tagId,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
limit: -1,
});
props.dispatch({ props.dispatch({
type: TagWindowStateActions.SetSongs, type: TagWindowStateActions.SetSongs,
value: json.songs, value: songs,
}); });
})(); })();
}, [props.state.songsWithTag]); }, [props.state.songsWithTag]);

@ -1,17 +1,18 @@
import * as serverApi from '../../api'; import * as serverApi from '../../api';
import { QueryElem, toApiQuery } from './Query';
export async function getArtists(filter: string) { export interface QueryArgs {
const query = filter.length > 0 ? { query?: QueryElem,
prop: serverApi.QueryElemProperty.artistName, offset: number,
propOperand: filter, limit: number,
propOperator: serverApi.QueryFilterOp.Like, }
} : {};
export async function queryArtists(args: QueryArgs) {
var q: serverApi.QueryRequest = { var q: serverApi.QueryRequest = {
query: query, query: args.query ? toApiQuery(args.query) : {},
offsetsLimits: { offsetsLimits: {
artistOffset: 0, artistOffset: args.offset,
artistLimit: 100, artistLimit: args.limit,
}, },
ordering: { ordering: {
orderBy: { orderBy: {
@ -30,23 +31,16 @@ export async function getArtists(filter: string) {
return (async () => { return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json(); let json: any = await response.json();
const names: string[] = json.artists.map((elem: any) => { return elem.name; }); return json.artists;
return [...new Set(names)];
})(); })();
} }
export async function getAlbums(filter: string) { export async function queryAlbums(args: QueryArgs) {
const query = filter.length > 0 ? {
prop: serverApi.QueryElemProperty.albumName,
propOperand: filter,
propOperator: serverApi.QueryFilterOp.Like,
} : {};
var q: serverApi.QueryRequest = { var q: serverApi.QueryRequest = {
query: query, query: args.query ? toApiQuery(args.query) : {},
offsetsLimits: { offsetsLimits: {
albumOffset: 0, albumOffset: args.offset,
albumLimit: 100, albumLimit: args.limit,
}, },
ordering: { ordering: {
orderBy: { orderBy: {
@ -65,23 +59,16 @@ export async function getAlbums(filter: string) {
return (async () => { return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json(); let json: any = await response.json();
const names: string[] = json.albums.map((elem: any) => { return elem.name; }); return json.albums;
return [...new Set(names)];
})(); })();
} }
export async function getSongTitles(filter: string) { export async function querySongs(args: QueryArgs) {
const query = filter.length > 0 ? {
prop: serverApi.QueryElemProperty.songTitle,
propOperand: filter,
propOperator: serverApi.QueryFilterOp.Like,
} : {};
var q: serverApi.QueryRequest = { var q: serverApi.QueryRequest = {
query: query, query: args.query ? toApiQuery(args.query) : {},
offsetsLimits: { offsetsLimits: {
songOffset: 0, songOffset: args.offset,
songLimit: 100, songLimit: args.limit,
}, },
ordering: { ordering: {
orderBy: { orderBy: {
@ -100,17 +87,16 @@ export async function getSongTitles(filter: string) {
return (async () => { return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json(); let json: any = await response.json();
const titles: string[] = json.songs.map((elem: any) => { return elem.title; }); return json.songs;
return [...new Set(titles)];
})(); })();
} }
export async function getTags() { export async function queryTags(args: QueryArgs) {
var q: serverApi.QueryRequest = { var q: serverApi.QueryRequest = {
query: {}, query: args.query ? toApiQuery(args.query) : {},
offsetsLimits: { offsetsLimits: {
tagOffset: 0, tagOffset: args.offset,
tagLimit: 100, tagLimit: args.limit,
}, },
ordering: { ordering: {
orderBy: { orderBy: {

@ -2,9 +2,13 @@ import * as serverApi from '../../api';
export enum QueryLeafBy { export enum QueryLeafBy {
ArtistName = 0, ArtistName = 0,
ArtistId,
AlbumName, AlbumName,
AlbumId,
TagInfo, TagInfo,
SongTitle TagId,
SongTitle,
SongId,
} }
export enum QueryLeafOp { export enum QueryLeafOp {
@ -14,11 +18,11 @@ export enum QueryLeafOp {
} }
export interface TagQueryInfo { export interface TagQueryInfo {
fullName: string[],
matchIds: number[], matchIds: number[],
fullName: string[],
} }
export function isTagQueryInfo(e: any): e is TagQueryInfo { export function isTagQueryInfo(e: any): e is TagQueryInfo {
return (typeof e === 'object') && 'fullName' in e && 'matchIds' in e; return (typeof e === 'object') && 'matchIds' in e && 'fullName' in e;
} }
export type QueryLeafOperand = string | number | TagQueryInfo; export type QueryLeafOperand = string | number | TagQueryInfo;
@ -166,6 +170,10 @@ export function toApiQuery(q: QueryElem) : serverApi.Query {
[QueryLeafBy.SongTitle]: serverApi.QueryElemProperty.songTitle, [QueryLeafBy.SongTitle]: serverApi.QueryElemProperty.songTitle,
[QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName, [QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName,
[QueryLeafBy.AlbumName]: serverApi.QueryElemProperty.albumName, [QueryLeafBy.AlbumName]: serverApi.QueryElemProperty.albumName,
[QueryLeafBy.AlbumId]: serverApi.QueryElemProperty.albumId,
[QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId,
[QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId,
[QueryLeafBy.SongId]: serverApi.QueryElemProperty.songId,
} }
const leafOpsMapping: any = { const leafOpsMapping: any = {
[QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq, [QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq,

@ -179,7 +179,7 @@ const objectColumns = {
}; };
function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering, function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering,
offset: number, limit: number) { offset: number, limit: number | null) {
const joinObjects = getRequiredDatabaseObjects(queryElem); const joinObjects = getRequiredDatabaseObjects(queryElem);
joinObjects.delete(queryFor); // We are already querying this object in the base query. joinObjects.delete(queryFor); // We are already querying this object in the base query.
@ -213,7 +213,12 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl
(ordering.ascending ? 'asc' : 'desc')); (ordering.ascending ? 'asc' : 'desc'));
// Apply limiting. // Apply limiting.
q = q.limit(limit).offset(offset); if(limit !== null) {
q = q.limit(limit)
}
// Apply offsetting.
q = q.offset(offset);
return q; return q;
} }
@ -271,43 +276,43 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
const albumLimit = reqObject.offsetsLimits.albumLimit; const albumLimit = reqObject.offsetsLimits.albumLimit;
const albumOffset = reqObject.offsetsLimits.albumOffset; const albumOffset = reqObject.offsetsLimits.albumOffset;
const artistsPromise: Promise<any> = (artistLimit && artistLimit > 0) ? const artistsPromise: Promise<any> = (artistLimit && artistLimit !== 0) ?
constructQuery(knex, constructQuery(knex,
ObjectType.Artist, ObjectType.Artist,
reqObject.query, reqObject.query,
reqObject.ordering, reqObject.ordering,
artistOffset || 0, artistOffset || 0,
artistLimit artistLimit >= 0 ? artistLimit : null,
) : ) :
(async () => [])(); (async () => [])();
const albumsPromise: Promise<any> = (albumLimit && albumLimit > 0) ? const albumsPromise: Promise<any> = (albumLimit && albumLimit !== 0) ?
constructQuery(knex, constructQuery(knex,
ObjectType.Album, ObjectType.Album,
reqObject.query, reqObject.query,
reqObject.ordering, reqObject.ordering,
artistOffset || 0, artistOffset || 0,
albumLimit albumLimit >= 0 ? albumLimit : null,
) : ) :
(async () => [])(); (async () => [])();
const songsPromise: Promise<any> = (songLimit && songLimit > 0) ? const songsPromise: Promise<any> = (songLimit && songLimit !== 0) ?
constructQuery(knex, constructQuery(knex,
ObjectType.Song, ObjectType.Song,
reqObject.query, reqObject.query,
reqObject.ordering, reqObject.ordering,
songOffset || 0, songOffset || 0,
songLimit songLimit >= 0 ? songLimit : null,
) : ) :
(async () => [])(); (async () => [])();
const tagsPromise: Promise<any> = (tagLimit && tagLimit > 0) ? const tagsPromise: Promise<any> = (tagLimit && tagLimit !== 0) ?
constructQuery(knex, constructQuery(knex,
ObjectType.Tag, ObjectType.Tag,
reqObject.query, reqObject.query,
reqObject.ordering, reqObject.ordering,
tagOffset || 0, tagOffset || 0,
tagLimit tagLimit >= 0 ? tagLimit : null,
) : ) :
(async () => [])(); (async () => [])();
@ -318,12 +323,12 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
const ids = songs.map((song: any) => song['songs.id']); const ids = songs.map((song: any) => song['songs.id']);
return ids; return ids;
})(); })();
const songsArtistsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit > 0) ? const songsArtistsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit !== 0) ?
(async () => { (async () => {
return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Artist, await songIdsPromise); return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Artist, await songIdsPromise);
})() : })() :
(async () => { return {}; })(); (async () => { return {}; })();
const songsTagsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit > 0) ? const songsTagsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit !== 0) ?
(async () => { (async () => {
const tagsPerSong: Record<number, any> = await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); const tagsPerSong: Record<number, any> = await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise);
var result: Record<number, any> = {}; var result: Record<number, any> = {};
@ -338,7 +343,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
return result; return result;
})() : })() :
(async () => { return {}; })(); (async () => { return {}; })();
const songsAlbumsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit > 0) ? const songsAlbumsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit !== 0) ?
(async () => { (async () => {
return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise); return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise);
})() : })() :

Loading…
Cancel
Save