Refactored queries to make searching other object types possible and to

include leaf operands that translate to different server-side operands
depending on the type of object requested.
master
Sander Vocke 4 years ago
parent cda62f0a80
commit 9df02ccb48
  1. 2
      client/src/components/querybuilder/QBNodeElem.tsx
  2. 2
      client/src/components/querybuilder/QueryBuilder.tsx
  3. 9
      client/src/components/windows/manage_links/BatchLinkDialog.tsx
  4. 23
      client/src/components/windows/manage_links/LinksStatusWidget.tsx
  5. 183
      client/src/components/windows/query/QueryWindow.tsx
  6. 40
      client/src/lib/backend/queries.tsx
  7. 110
      client/src/lib/query/Query.tsx

@ -22,7 +22,7 @@ export function QBNodeElem(props: NodeProps) {
} else { } else {
ops.splice(idx, 1); ops.splice(idx, 1);
} }
let newNode = simplify({ operands: ops, nodeOp: e.nodeOp }); let newNode = simplify({ operands: ops, nodeOp: e.nodeOp }, null);
props.onReplace(newNode); props.onReplace(newNode);
} }

@ -27,7 +27,7 @@ export interface IProps {
} }
export default function QueryBuilder(props: IProps) { export default function QueryBuilder(props: IProps) {
const simpleQuery = simplify(props.query); const simpleQuery = simplify(props.query, null);
const showQuery = props.editing ? const showQuery = props.editing ?
addPlaceholders(simpleQuery, null) : simpleQuery; addPlaceholders(simpleQuery, null) : simpleQuery;

@ -49,11 +49,6 @@ async function makeTasks(
linkAlbums: boolean, linkAlbums: boolean,
addTaskCb: (t: Task) => void, addTaskCb: (t: Task) => void,
) { ) {
let whichProp: any = {
[ResourceType.Track]: QueryLeafBy.TrackStoreLinks,
[ResourceType.Artist]: QueryLeafBy.ArtistStoreLinks,
[ResourceType.Album]: QueryLeafBy.AlbumStoreLinks,
}
let whichElem: any = { let whichElem: any = {
[ResourceType.Track]: 'tracks', [ResourceType.Track]: 'tracks',
[ResourceType.Artist]: 'artists', [ResourceType.Artist]: 'artists',
@ -66,9 +61,9 @@ async function makeTasks(
let store = maybeStore as IntegrationWith; let store = maybeStore as IntegrationWith;
let doForType = async (type: ResourceType) => { let doForType = async (type: ResourceType) => {
let ids: number[] = ((await queryItems( let ids: number[] = ((await queryItems(
[type], type,
queryNot({ queryNot({
a: whichProp[type], a: QueryLeafBy.StoreLinks,
leafOp: QueryLeafOp.Like, leafOp: QueryLeafOp.Like,
b: `%${IntegrationUrls[store]}%`, b: `%${IntegrationUrls[store]}%`,
}), }),

@ -2,7 +2,7 @@ import { Box, LinearProgress, Typography } from '@material-ui/core';
import React, { useCallback, useEffect, useReducer, useState } from 'react'; import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { $enum } from 'ts-enum-util'; import { $enum } from 'ts-enum-util';
import { IntegrationWith, ResourceType, QueryElemProperty, QueryResponseType, IntegrationUrls } from '../../../api/api'; import { IntegrationWith, ResourceType, QueryElemProperty, QueryResponseType, IntegrationUrls } from '../../../api/api';
import { queryItems } from '../../../lib/backend/queries'; import { queryAlbums, queryArtists, queryItems, queryTracks } from '../../../lib/backend/queries';
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import StoreLinkIcon from '../../common/StoreLinkIcon'; import StoreLinkIcon from '../../common/StoreLinkIcon';
@ -21,20 +21,15 @@ export default function LinksStatusWidget(props: {
let [linkedCounts, setLinkedCounts] = useState<Record<string, Counts>>({}); let [linkedCounts, setLinkedCounts] = useState<Record<string, Counts>>({});
let queryStoreCount = async (store: IntegrationWith, type: ResourceType) => { let queryStoreCount = async (store: IntegrationWith, type: ResourceType) => {
let whichProp: any = {
[ResourceType.Track]: QueryLeafBy.TrackStoreLinks,
[ResourceType.Artist]: QueryLeafBy.ArtistStoreLinks,
[ResourceType.Album]: QueryLeafBy.AlbumStoreLinks,
}
let whichElem: any = { let whichElem: any = {
[ResourceType.Track]: 'tracks', [ResourceType.Track]: 'tracks',
[ResourceType.Artist]: 'artists', [ResourceType.Artist]: 'artists',
[ResourceType.Album]: 'albums', [ResourceType.Album]: 'albums',
} }
let r: any = await queryItems( let r: any = await queryItems(
[type], type,
{ {
a: whichProp[type], a: QueryLeafBy.StoreLinks,
leafOp: QueryLeafOp.Like, leafOp: QueryLeafOp.Like,
b: `%${IntegrationUrls[store]}%`, b: `%${IntegrationUrls[store]}%`,
}, },
@ -49,13 +44,11 @@ export default function LinksStatusWidget(props: {
// Start retrieving total counts // Start retrieving total counts
useEffect(() => { useEffect(() => {
(async () => { (async () => {
let counts: any = await queryItems( let counts: Counts = {
[ResourceType.Track, ResourceType.Artist, ResourceType.Album], albums: await queryAlbums(undefined, undefined, undefined, QueryResponseType.Count) as number,
undefined, tracks: await queryTracks(undefined, undefined, undefined, QueryResponseType.Count) as number,
undefined, artists: await queryArtists(undefined, undefined, undefined, QueryResponseType.Count) as number,
undefined, }
QueryResponseType.Count
);
console.log("Got total counts: ", counts) console.log("Got total counts: ", counts)
setTotalCounts(counts); setTotalCounts(counts);
} }

@ -1,29 +1,50 @@
import React, { useEffect, useReducer, useCallback } from 'react'; import React, { useEffect, useReducer, useCallback } from 'react';
import { Box, LinearProgress } from '@material-ui/core'; import { Box, LinearProgress } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder'; import QueryBuilder from '../../querybuilder/QueryBuilder';
import TrackTable from '../../tables/ResultsTable'; import TrackTable from '../../tables/ResultsTable';
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries'; import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails} from '../../../api/api'; import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails } from '../../../api/api';
import { ServerStreamResponseOptions } from 'http2'; import { ServerStreamResponseOptions } from 'http2';
import { TrackChangesSharp } from '@material-ui/icons';
import { v4 as genUuid } from 'uuid';
var _ = require('lodash'); var _ = require('lodash');
export interface ResultsForQuery { export enum QueryItemType {
for: QueryElem, Artists,
results: any[], Tracks,
Albums,
Tags,
}; };
export interface ResultsForQuery {
kind: QueryItemType,
results: (
QueryResponseAlbumDetails[] |
QueryResponseArtistDetails[] |
QueryResponseTagDetails[] |
QueryResponseTrackDetails[]
),
}
export interface QueryWindowState extends WindowState { export interface QueryWindowState extends WindowState {
editingQuery: boolean, editingQuery: boolean, // Is the editor in "edit mode"
query: QueryElem | null, query: QueryElem | null, // The actual on-screen query
resultsForQuery: ResultsForQuery | null,
includeTypes: QueryItemType[], // which item types do we actually request results for?
// Whenever queries change, new requests are fired to the server.
// Each request gets a unique id hash.
// In this results record, we store the query IDs which
// we want to show results for.
resultsForQueries: Record<string, ResultsForQuery | null>;
} }
export enum QueryWindowStateActions { export enum QueryWindowStateActions {
SetQuery = "setQuery", FiredNewQueries = "firedNewQueries",
SetEditingQuery = "setEditingQuery", SetEditingQuery = "setEditingQuery",
SetResultsForQuery = "setResultsForQuery", ReceivedResult = "receivedResult",
} }
async function getArtistNames(filter: string) { async function getArtistNames(filter: string) {
@ -74,23 +95,55 @@ async function getTagItems(): Promise<any> {
return tags; return tags;
} }
export interface FireNewQueriesData {
query: QueryElem | null,
includeTypes: QueryItemType[],
resultIds: string[],
}
export interface ReceivedResultData {
result: ResultsForQuery,
id: string,
}
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.ReceivedResult:
return { ...state, query: action.value } var arr = action.value as ReceivedResultData;
if (Object.keys(state.resultsForQueries).includes(arr.id)) {
//console.log("Storing result:", arr);
var _n = _.cloneDeep(state);
_n.resultsForQueries[arr.id] = arr.result;
return _n;
}
//console.log("Discarding result:", arr);
return state;
case QueryWindowStateActions.FiredNewQueries:
var newState: QueryWindowState = _.cloneDeep(state);
let _action = action.value as FireNewQueriesData;
// Invalidate results
newState.resultsForQueries = {};
// Add a null result for each of the new IDs.
// Results will be added in as they come.
_action.resultIds && _action.resultIds.forEach((r: string) => {
newState.resultsForQueries[r] = null;
})
newState.query = _action.query;
newState.includeTypes = _action.includeTypes;
return newState;
case QueryWindowStateActions.SetEditingQuery: case QueryWindowStateActions.SetEditingQuery:
return { ...state, editingQuery: action.value } return { ...state, editingQuery: action.value }
case QueryWindowStateActions.SetResultsForQuery:
return { ...state, resultsForQuery: action.value }
default: default:
throw new Error("Unimplemented QueryWindow state update.") throw new Error("Unimplemented QueryWindow state update.")
} }
} }
export default function QueryWindow(props: {}) { export default function QueryWindow(props: {}) {
const [state, dispatch] = useReducer(QueryWindowReducer, { const [state, dispatch] = useReducer(QueryWindowReducer, {
editingQuery: false, editingQuery: false,
query: null, query: null,
resultsForQuery: null, resultsForQueries: {},
includeTypes: [QueryItemType.Tracks, QueryItemType.Artists, QueryItemType.Albums, QueryItemType.Tags],
}); });
return <QueryWindowControlled state={state} dispatch={dispatch} /> return <QueryWindowControlled state={state} dispatch={dispatch} />
@ -100,45 +153,70 @@ export function QueryWindowControlled(props: {
state: QueryWindowState, state: QueryWindowState,
dispatch: (action: any) => void, dispatch: (action: any) => void,
}) { }) {
let { query, editingQuery: editing, resultsForQuery: resultsFor } = props.state; let { query, editingQuery, resultsForQueries, includeTypes } = props.state;
let { dispatch } = props; let { dispatch } = props;
let setQuery = (q: QueryElem | null) => { // Call this function to fire new queries and prepare to receive their results.
props.dispatch({ type: QueryWindowStateActions.SetQuery, value: q }); // This will also set the query into the window state.
} const doQueries = async (_query: QueryElem | null, itemTypes: QueryItemType[]) => {
let setEditingQuery = (e: boolean) => { var promises: Promise<any>[] = [];
props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e }); var ids: string[] = itemTypes.map((i: any) => genUuid());
} var query_fns = {
let setResultsForQuery = useCallback((r: ResultsForQuery | null) => { [QueryItemType.Albums]: queryAlbums,
dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); [QueryItemType.Artists]: queryArtists,
}, [dispatch]); [QueryItemType.Tracks]: queryTracks,
[QueryItemType.Tags]: queryTags,
};
const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); let stateUpdateData: FireNewQueriesData = {
const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; query: _query,
includeTypes: itemTypes,
resultIds: ids
};
const doQuery = useCallback(async (_query: QueryElem) => { // First dispatch to the state that we are firing new queries.
const tracks: QueryResponseTrackDetails[] = await queryTracks( // This will update the query on the window page and invalidate
// any previous results on-screen.
dispatch({
type: QueryWindowStateActions.FiredNewQueries,
value: stateUpdateData
})
if (_query) {
itemTypes.forEach((itemType: QueryItemType, idx: number) => {
(promises as any[]).push(
(async () => {
let results = (await query_fns[itemType](
_query, _query,
0, 0, // TODO: pagination
100, //TODO: pagination 100,
QueryResponseType.Details QueryResponseType.Details
) as QueryResponseTrackDetails[]; )) as (
QueryResponseAlbumDetails[] |
QueryResponseArtistDetails[] |
QueryResponseTagDetails[] |
QueryResponseTrackDetails[]);
let r: ReceivedResultData = {
id: ids[idx],
result: {
kind: itemType,
results: results
}
};
if (_.isEqual(query, _query)) { dispatch({ type: QueryWindowStateActions.ReceivedResult, value: r })
setResultsForQuery({ })()
for: _query, );
results: tracks,
}) })
} }
}, [query, setResultsForQuery]);
useEffect(() => { await Promise.all(promises);
if (query) { };
doQuery(query);
} else { let setEditingQuery = (e: boolean) => {
setResultsForQuery(null); props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e });
} }
}, [query, doQuery, setResultsForQuery]);
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box <Box
@ -147,8 +225,10 @@ export function QueryWindowControlled(props: {
> >
<QueryBuilder <QueryBuilder
query={query} query={query}
onChangeQuery={setQuery} onChangeQuery={(q: QueryElem | null) => {
editing={editing} doQueries(q, includeTypes)
}}
editing={editingQuery}
onChangeEditing={setEditingQuery} onChangeEditing={setEditingQuery}
requestFunctions={{ requestFunctions={{
getArtists: getArtistNames, getArtists: getArtistNames,
@ -162,10 +242,15 @@ export function QueryWindowControlled(props: {
m={1} m={1}
width="80%" width="80%"
> >
<TrackTable {Object.values(resultsForQueries).map((r: ResultsForQuery | null) => <>
tracks={showResults} {r !== null && r.kind == QueryItemType.Tracks && <TrackTable
/> tracks={r.results as QueryResponseTrackDetails[]}
{loading && <LinearProgress />} />}
{r !== null && r.kind == QueryItemType.Albums && <>Found {r.results.length} albums.</>}
{r !== null && r.kind == QueryItemType.Artists && <>Found {r.results.length} artists.</>}
{r !== null && r.kind == QueryItemType.Tags && <>Found {r.results.length} tags.</>}
{r === null && <LinearProgress />}
</>)}
</Box> </Box>
</Box> </Box>
} }

@ -1,26 +1,34 @@
import * as serverApi from '../../api/api'; import * as serverApi from '../../api/api';
import { QueryElem, toApiQuery } from '../query/Query'; import { QueryElem, QueryFor, simplify, toApiQuery } from '../query/Query';
import backendRequest from './request'; import backendRequest from './request';
export async function queryItems( export async function queryItems(
types: serverApi.ResourceType[], type: serverApi.ResourceType,
query: QueryElem | undefined, query: QueryElem | undefined,
offset: number | undefined, offset: number | undefined,
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.QueryResponse> { ): Promise<serverApi.QueryResponse> {
console.log("Types:", types); const queryForMapping : any = {
[serverApi.ResourceType.Album]: QueryFor.Albums,
[serverApi.ResourceType.Artist]: QueryFor.Artists,
[serverApi.ResourceType.Tag]: QueryFor.Tags,
[serverApi.ResourceType.Track]: QueryFor.Tracks,
};
const simplified = simplify(query || null, queryForMapping[type]);
var q: serverApi.QueryRequest = { var q: serverApi.QueryRequest = {
query: query ? toApiQuery(query) : {}, query: simplified ? toApiQuery(simplified, queryForMapping[type]) : {},
offsetsLimits: { offsetsLimits: {
artistOffset: (types.includes(serverApi.ResourceType.Artist)) ? (offset || 0) : undefined, artistOffset: (type == serverApi.ResourceType.Artist) ? (offset || 0) : undefined,
artistLimit: (types.includes(serverApi.ResourceType.Artist)) ? (limit || -1) : undefined, artistLimit: (type == serverApi.ResourceType.Artist) ? (limit || -1) : undefined,
albumOffset: (types.includes(serverApi.ResourceType.Album)) ? (offset || 0) : undefined, albumOffset: (type == serverApi.ResourceType.Album) ? (offset || 0) : undefined,
albumLimit: (types.includes(serverApi.ResourceType.Album)) ? (limit || -1) : undefined, albumLimit: (type == serverApi.ResourceType.Album) ? (limit || -1) : undefined,
trackOffset: (types.includes(serverApi.ResourceType.Track)) ? (offset || 0) : undefined, trackOffset: (type == serverApi.ResourceType.Track) ? (offset || 0) : undefined,
trackLimit: (types.includes(serverApi.ResourceType.Track)) ? (limit || -1) : undefined, trackLimit: (type == serverApi.ResourceType.Track) ? (limit || -1) : undefined,
tagOffset: (types.includes(serverApi.ResourceType.Tag)) ? (offset || 0) : undefined, tagOffset: (type == serverApi.ResourceType.Tag) ? (offset || 0) : undefined,
tagLimit: (types.includes(serverApi.ResourceType.Tag)) ? (limit || -1) : undefined, tagLimit: (type == serverApi.ResourceType.Tag) ? (limit || -1) : undefined,
}, },
ordering: { ordering: {
orderBy: { orderBy: {
@ -50,7 +58,7 @@ export async function queryArtists(
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.QueryResponseArtistDetails[] | number[] | number> { ): Promise<serverApi.QueryResponseArtistDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Artist], query, offset, limit, responseType); let r = await queryItems(serverApi.ResourceType.Artist, query, offset, limit, responseType);
return r.artists; return r.artists;
} }
@ -60,7 +68,7 @@ export async function queryAlbums(
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.QueryResponseAlbumDetails[] | number[] | number> { ): Promise<serverApi.QueryResponseAlbumDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Album], query, offset, limit, responseType); let r = await queryItems(serverApi.ResourceType.Album, query, offset, limit, responseType);
return r.albums; return r.albums;
} }
@ -70,7 +78,7 @@ export async function queryTracks(
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.QueryResponseTrackDetails[] | number[] | number> { ): Promise<serverApi.QueryResponseTrackDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Track], query, offset, limit, responseType); let r = await queryItems(serverApi.ResourceType.Track, query, offset, limit, responseType);
return r.tracks; return r.tracks;
} }
@ -80,6 +88,6 @@ export async function queryTags(
limit: number | undefined, limit: number | undefined,
responseType: serverApi.QueryResponseType, responseType: serverApi.QueryResponseType,
): Promise<serverApi.QueryResponseTagDetails[] | number[] | number> { ): Promise<serverApi.QueryResponseTagDetails[] | number[] | number> {
let r = await queryItems([serverApi.ResourceType.Tag], query, offset, limit, responseType); let r = await queryItems(serverApi.ResourceType.Tag, query, offset, limit, responseType);
return r.tags; return r.tags;
} }

@ -1,5 +1,12 @@
import * as serverApi from '../../api/api'; import * as serverApi from '../../api/api';
export enum QueryFor {
Artists = 0,
Albums,
Tags,
Tracks,
}
export enum QueryLeafBy { export enum QueryLeafBy {
ArtistName = 0, ArtistName = 0,
ArtistId, ArtistId,
@ -9,9 +16,7 @@ export enum QueryLeafBy {
TagId, TagId,
TrackName, TrackName,
TrackId, TrackId,
TrackStoreLinks, StoreLinks,
ArtistStoreLinks,
AlbumStoreLinks,
} }
export enum QueryLeafOp { export enum QueryLeafOp {
@ -26,7 +31,7 @@ export interface TagQueryInfo {
} }
export function isTagQueryInfo(e: any): e is TagQueryInfo { export function isTagQueryInfo(e: any): e is TagQueryInfo {
return (typeof e === 'object') && 'matchIds' in e && 'fullName' in e; return (typeof e === 'object') && 'matchIds' in e && 'fullName' in e;
} }
export type QueryLeafOperand = string | number | TagQueryInfo; export type QueryLeafOperand = string | number | TagQueryInfo;
@ -77,6 +82,43 @@ export function queryNot(arg: QueryElem) {
export type QueryElem = QueryLeafElem | QueryNodeElem; export type QueryElem = QueryLeafElem | QueryNodeElem;
function mapToServerProperty(l: QueryLeafBy, queryFor: QueryFor | null) :
serverApi.QueryElemProperty | null {
return {
[QueryLeafBy.TrackName]: serverApi.QueryElemProperty.trackName,
[QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName,
[QueryLeafBy.AlbumName]: serverApi.QueryElemProperty.albumName,
[QueryLeafBy.AlbumId]: serverApi.QueryElemProperty.albumId,
[QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId,
[QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId,
[QueryLeafBy.TrackId]: serverApi.QueryElemProperty.trackId,
[QueryLeafBy.StoreLinks]:
(queryFor == QueryFor.Albums) ? serverApi.QueryElemProperty.albumStoreLinks :
(queryFor == QueryFor.Artists) ? serverApi.QueryElemProperty.artistStoreLinks :
(queryFor == QueryFor.Tracks) ? serverApi.QueryElemProperty.trackStoreLinks :
null,
[QueryLeafBy.TagInfo]: null,
}[l];
}
function mapToServerLeafOp(l: QueryLeafOp, queryFor: QueryFor | null) :
serverApi.QueryLeafOp | null {
return {
[QueryLeafOp.Equals]: serverApi.QueryLeafOp.Eq,
[QueryLeafOp.Like]: serverApi.QueryLeafOp.Like,
[QueryLeafOp.Placeholder]: null,
}[l];
}
function mapToServerNodeOp(l: QueryNodeOp, queryFor: QueryFor | null) :
serverApi.QueryNodeOp | null {
return {
[QueryNodeOp.And]: serverApi.QueryNodeOp.And,
[QueryNodeOp.Or]: serverApi.QueryNodeOp.Or,
[QueryNodeOp.Not]: serverApi.QueryNodeOp.Not,
}[l];
}
// Take a query and add placeholders. The placeholders are empty // Take a query and add placeholders. The placeholders are empty
// leaves. They should be placed so that all possible node combinations // leaves. They should be placed so that all possible node combinations
// from the existing nodes could have an added combinational leaf. // from the existing nodes could have an added combinational leaf.
@ -162,11 +204,11 @@ export function removePlaceholders(q: QueryElem | null): QueryElem | null {
return q; return q;
} }
export function simplify(q: QueryElem | null): QueryElem | null { export function simplify(q: QueryElem | null, queryFor: QueryFor | null): QueryElem | null {
if (q && isNodeElem(q)) { if (q && isNodeElem(q)) {
var newOperands: QueryElem[] = []; var newOperands: QueryElem[] = [];
q.operands.forEach((o: QueryElem) => { q.operands.forEach((o: QueryElem) => {
const s = simplify(o); const s = simplify(o, queryFor);
if (s !== null) { newOperands.push(s); } if (s !== null) { newOperands.push(s); }
}) })
if (newOperands.length === 0) { return null; } if (newOperands.length === 0) { return null; }
@ -174,33 +216,18 @@ export function simplify(q: QueryElem | null): QueryElem | null {
return { operands: newOperands, nodeOp: q.nodeOp }; return { operands: newOperands, nodeOp: q.nodeOp };
} }
return q; if (q && isLeafElem(q)) {
} if (mapToServerLeafOp(q.leafOp, queryFor) === null ||
mapToServerProperty(q.a, queryFor) === null) {
export function toApiQuery(q: QueryElem) : serverApi.Query { return null;
const propsMapping: any = {
[QueryLeafBy.TrackName]: serverApi.QueryElemProperty.trackName,
[QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName,
[QueryLeafBy.AlbumName]: serverApi.QueryElemProperty.albumName,
[QueryLeafBy.AlbumId]: serverApi.QueryElemProperty.albumId,
[QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId,
[QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId,
[QueryLeafBy.TrackId]: serverApi.QueryElemProperty.trackId,
[QueryLeafBy.TrackStoreLinks]: serverApi.QueryElemProperty.trackStoreLinks,
[QueryLeafBy.ArtistStoreLinks]: serverApi.QueryElemProperty.artistStoreLinks,
[QueryLeafBy.AlbumStoreLinks]: serverApi.QueryElemProperty.albumStoreLinks,
}
const leafOpsMapping: any = {
[QueryLeafOp.Equals]: serverApi.QueryLeafOp.Eq,
[QueryLeafOp.Like]: serverApi.QueryLeafOp.Like,
} }
const nodeOpsMapping: any = {
[QueryNodeOp.And]: serverApi.QueryNodeOp.And,
[QueryNodeOp.Or]: serverApi.QueryNodeOp.Or,
[QueryNodeOp.Not]: serverApi.QueryNodeOp.Not,
} }
if(isLeafElem(q) && isTagQueryInfo(q.b)) { return q;
}
export function toApiQuery(q: QueryElem, queryFor: QueryFor | null): serverApi.Query {
if (isLeafElem(q) && isTagQueryInfo(q.b)) {
// Special case for tag queries by ID // Special case for tag queries by ID
const r: serverApi.QueryElem = { const r: serverApi.QueryElem = {
prop: serverApi.QueryElemProperty.tagId, prop: serverApi.QueryElemProperty.tagId,
@ -208,18 +235,29 @@ export function toApiQuery(q: QueryElem) : serverApi.Query {
propOperand: q.b.matchIds, propOperand: q.b.matchIds,
} }
return r; return r;
} else if(isLeafElem(q)) { } else if (isLeafElem(q)) {
// If the property to operate on is non-existent
// (e.g. store links for a tag query), throw.
let a = mapToServerProperty(q.a, queryFor);
let op = mapToServerLeafOp(q.leafOp, queryFor);
if (a === null || op === null) {
throw 'Found a null leaf in query tree. Was it simplified first?';
}
// "Regular" queries // "Regular" queries
const r: serverApi.QueryElem = { const r: serverApi.QueryElem = {
prop: propsMapping[q.a], prop: a,
propOperator: leafOpsMapping[q.leafOp], propOperator: op,
propOperand: q.b, propOperand: q.b,
} }
return r; return r;
} else if(isNodeElem(q)) { } else if (isNodeElem(q)) {
let op = mapToServerNodeOp(q.nodeOp, queryFor);
if (op === null) {
throw 'Found a null node in query tree. Was it simplified first?'
}
const r = { const r = {
children: q.operands.map((op: any) => toApiQuery(op)), children: q.operands.map((op: any) => toApiQuery(op, queryFor)),
childrenOperator: nodeOpsMapping[q.nodeOp] childrenOperator: op
} }
return r; return r;
} }

Loading…
Cancel
Save