From 9df02ccb48b64b2482b50eb7734cdfd68142a4e4 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Thu, 21 Oct 2021 16:43:06 +0200 Subject: [PATCH] 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. --- .../components/querybuilder/QBNodeElem.tsx | 2 +- .../components/querybuilder/QueryBuilder.tsx | 2 +- .../windows/manage_links/BatchLinkDialog.tsx | 9 +- .../manage_links/LinksStatusWidget.tsx | 23 +-- .../components/windows/query/QueryWindow.tsx | 195 +++++++++++++----- client/src/lib/backend/queries.tsx | 40 ++-- client/src/lib/query/Query.tsx | 112 ++++++---- 7 files changed, 251 insertions(+), 132 deletions(-) diff --git a/client/src/components/querybuilder/QBNodeElem.tsx b/client/src/components/querybuilder/QBNodeElem.tsx index 4b8691a..c922799 100644 --- a/client/src/components/querybuilder/QBNodeElem.tsx +++ b/client/src/components/querybuilder/QBNodeElem.tsx @@ -22,7 +22,7 @@ export function QBNodeElem(props: NodeProps) { } else { ops.splice(idx, 1); } - let newNode = simplify({ operands: ops, nodeOp: e.nodeOp }); + let newNode = simplify({ operands: ops, nodeOp: e.nodeOp }, null); props.onReplace(newNode); } diff --git a/client/src/components/querybuilder/QueryBuilder.tsx b/client/src/components/querybuilder/QueryBuilder.tsx index 15bcf80..de49e44 100644 --- a/client/src/components/querybuilder/QueryBuilder.tsx +++ b/client/src/components/querybuilder/QueryBuilder.tsx @@ -27,7 +27,7 @@ export interface IProps { } export default function QueryBuilder(props: IProps) { - const simpleQuery = simplify(props.query); + const simpleQuery = simplify(props.query, null); const showQuery = props.editing ? addPlaceholders(simpleQuery, null) : simpleQuery; diff --git a/client/src/components/windows/manage_links/BatchLinkDialog.tsx b/client/src/components/windows/manage_links/BatchLinkDialog.tsx index c5360d5..ca45086 100644 --- a/client/src/components/windows/manage_links/BatchLinkDialog.tsx +++ b/client/src/components/windows/manage_links/BatchLinkDialog.tsx @@ -49,11 +49,6 @@ async function makeTasks( linkAlbums: boolean, addTaskCb: (t: Task) => void, ) { - let whichProp: any = { - [ResourceType.Track]: QueryLeafBy.TrackStoreLinks, - [ResourceType.Artist]: QueryLeafBy.ArtistStoreLinks, - [ResourceType.Album]: QueryLeafBy.AlbumStoreLinks, - } let whichElem: any = { [ResourceType.Track]: 'tracks', [ResourceType.Artist]: 'artists', @@ -66,9 +61,9 @@ async function makeTasks( let store = maybeStore as IntegrationWith; let doForType = async (type: ResourceType) => { let ids: number[] = ((await queryItems( - [type], + type, queryNot({ - a: whichProp[type], + a: QueryLeafBy.StoreLinks, leafOp: QueryLeafOp.Like, b: `%${IntegrationUrls[store]}%`, }), diff --git a/client/src/components/windows/manage_links/LinksStatusWidget.tsx b/client/src/components/windows/manage_links/LinksStatusWidget.tsx index 30cdd57..65088e0 100644 --- a/client/src/components/windows/manage_links/LinksStatusWidget.tsx +++ b/client/src/components/windows/manage_links/LinksStatusWidget.tsx @@ -2,7 +2,7 @@ import { Box, LinearProgress, Typography } from '@material-ui/core'; import React, { useCallback, useEffect, useReducer, useState } from 'react'; import { $enum } from 'ts-enum-util'; 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 StoreLinkIcon from '../../common/StoreLinkIcon'; @@ -21,20 +21,15 @@ export default function LinksStatusWidget(props: { let [linkedCounts, setLinkedCounts] = useState>({}); 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 = { [ResourceType.Track]: 'tracks', [ResourceType.Artist]: 'artists', [ResourceType.Album]: 'albums', } let r: any = await queryItems( - [type], + type, { - a: whichProp[type], + a: QueryLeafBy.StoreLinks, leafOp: QueryLeafOp.Like, b: `%${IntegrationUrls[store]}%`, }, @@ -49,13 +44,11 @@ export default function LinksStatusWidget(props: { // Start retrieving total counts useEffect(() => { (async () => { - let counts: any = await queryItems( - [ResourceType.Track, ResourceType.Artist, ResourceType.Album], - undefined, - undefined, - undefined, - QueryResponseType.Count - ); + let counts: Counts = { + albums: await queryAlbums(undefined, undefined, undefined, QueryResponseType.Count) as number, + tracks: await queryTracks(undefined, undefined, undefined, QueryResponseType.Count) as number, + artists: await queryArtists(undefined, undefined, undefined, QueryResponseType.Count) as number, + } console.log("Got total counts: ", counts) setTotalCounts(counts); } diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx index c140cdb..1323acc 100644 --- a/client/src/components/windows/query/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -1,29 +1,50 @@ import React, { useEffect, useReducer, useCallback } from 'react'; 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 TrackTable from '../../tables/ResultsTable'; import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries'; 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 { TrackChangesSharp } from '@material-ui/icons'; +import { v4 as genUuid } from 'uuid'; var _ = require('lodash'); -export interface ResultsForQuery { - for: QueryElem, - results: any[], +export enum QueryItemType { + Artists, + Tracks, + Albums, + Tags, }; +export interface ResultsForQuery { + kind: QueryItemType, + results: ( + QueryResponseAlbumDetails[] | + QueryResponseArtistDetails[] | + QueryResponseTagDetails[] | + QueryResponseTrackDetails[] + ), +} + export interface QueryWindowState extends WindowState { - editingQuery: boolean, - query: QueryElem | null, - resultsForQuery: ResultsForQuery | null, + editingQuery: boolean, // Is the editor in "edit mode" + query: QueryElem | null, // The actual on-screen query + + 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; } export enum QueryWindowStateActions { - SetQuery = "setQuery", + FiredNewQueries = "firedNewQueries", SetEditingQuery = "setEditingQuery", - SetResultsForQuery = "setResultsForQuery", + ReceivedResult = "receivedResult", } async function getArtistNames(filter: string) { @@ -74,23 +95,55 @@ async function getTagItems(): Promise { 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) { switch (action.type) { - case QueryWindowStateActions.SetQuery: - return { ...state, query: action.value } + case QueryWindowStateActions.ReceivedResult: + 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: return { ...state, editingQuery: action.value } - case QueryWindowStateActions.SetResultsForQuery: - return { ...state, resultsForQuery: action.value } default: throw new Error("Unimplemented QueryWindow state update.") } } + export default function QueryWindow(props: {}) { const [state, dispatch] = useReducer(QueryWindowReducer, { editingQuery: false, query: null, - resultsForQuery: null, + resultsForQueries: {}, + includeTypes: [QueryItemType.Tracks, QueryItemType.Artists, QueryItemType.Albums, QueryItemType.Tags], }); return @@ -100,45 +153,70 @@ export function QueryWindowControlled(props: { state: QueryWindowState, dispatch: (action: any) => void, }) { - let { query, editingQuery: editing, resultsForQuery: resultsFor } = props.state; + let { query, editingQuery, resultsForQueries, includeTypes } = props.state; let { dispatch } = props; - let setQuery = (q: QueryElem | null) => { - props.dispatch({ type: QueryWindowStateActions.SetQuery, value: q }); - } - let setEditingQuery = (e: boolean) => { - props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e }); - } - let setResultsForQuery = useCallback((r: ResultsForQuery | null) => { - dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); - }, [dispatch]); - - const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); - const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; - - const doQuery = useCallback(async (_query: QueryElem) => { - const tracks: QueryResponseTrackDetails[] = await queryTracks( - _query, - 0, - 100, //TODO: pagination - QueryResponseType.Details - ) as QueryResponseTrackDetails[]; - - if (_.isEqual(query, _query)) { - setResultsForQuery({ - for: _query, - results: tracks, + // Call this function to fire new queries and prepare to receive their results. + // This will also set the query into the window state. + const doQueries = async (_query: QueryElem | null, itemTypes: QueryItemType[]) => { + var promises: Promise[] = []; + var ids: string[] = itemTypes.map((i: any) => genUuid()); + var query_fns = { + [QueryItemType.Albums]: queryAlbums, + [QueryItemType.Artists]: queryArtists, + [QueryItemType.Tracks]: queryTracks, + [QueryItemType.Tags]: queryTags, + }; + + let stateUpdateData: FireNewQueriesData = { + query: _query, + includeTypes: itemTypes, + resultIds: ids + }; + + // First dispatch to the state that we are firing new queries. + // 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, + 0, // TODO: pagination + 100, + QueryResponseType.Details + )) as ( + QueryResponseAlbumDetails[] | + QueryResponseArtistDetails[] | + QueryResponseTagDetails[] | + QueryResponseTrackDetails[]); + + let r: ReceivedResultData = { + id: ids[idx], + result: { + kind: itemType, + results: results + } + }; + + dispatch({ type: QueryWindowStateActions.ReceivedResult, value: r }) + })() + ); }) } - }, [query, setResultsForQuery]); - useEffect(() => { - if (query) { - doQuery(query); - } else { - setResultsForQuery(null); - } - }, [query, doQuery, setResultsForQuery]); + await Promise.all(promises); + }; + + let setEditingQuery = (e: boolean) => { + props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e }); + } return { + doQueries(q, includeTypes) + }} + editing={editingQuery} onChangeEditing={setEditingQuery} requestFunctions={{ getArtists: getArtistNames, @@ -162,10 +242,15 @@ export function QueryWindowControlled(props: { m={1} width="80%" > - - {loading && } + {Object.values(resultsForQueries).map((r: ResultsForQuery | null) => <> + {r !== null && r.kind == QueryItemType.Tracks && } + {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 && } + )} } \ No newline at end of file diff --git a/client/src/lib/backend/queries.tsx b/client/src/lib/backend/queries.tsx index 4f408ce..3164169 100644 --- a/client/src/lib/backend/queries.tsx +++ b/client/src/lib/backend/queries.tsx @@ -1,26 +1,34 @@ import * as serverApi from '../../api/api'; -import { QueryElem, toApiQuery } from '../query/Query'; +import { QueryElem, QueryFor, simplify, toApiQuery } from '../query/Query'; import backendRequest from './request'; export async function queryItems( - types: serverApi.ResourceType[], + type: serverApi.ResourceType, query: QueryElem | undefined, offset: number | undefined, limit: number | undefined, responseType: serverApi.QueryResponseType, ): Promise { - 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 = { - query: query ? toApiQuery(query) : {}, + query: simplified ? toApiQuery(simplified, queryForMapping[type]) : {}, offsetsLimits: { - artistOffset: (types.includes(serverApi.ResourceType.Artist)) ? (offset || 0) : undefined, - artistLimit: (types.includes(serverApi.ResourceType.Artist)) ? (limit || -1) : undefined, - albumOffset: (types.includes(serverApi.ResourceType.Album)) ? (offset || 0) : undefined, - albumLimit: (types.includes(serverApi.ResourceType.Album)) ? (limit || -1) : undefined, - trackOffset: (types.includes(serverApi.ResourceType.Track)) ? (offset || 0) : undefined, - trackLimit: (types.includes(serverApi.ResourceType.Track)) ? (limit || -1) : undefined, - tagOffset: (types.includes(serverApi.ResourceType.Tag)) ? (offset || 0) : undefined, - tagLimit: (types.includes(serverApi.ResourceType.Tag)) ? (limit || -1) : undefined, + artistOffset: (type == serverApi.ResourceType.Artist) ? (offset || 0) : undefined, + artistLimit: (type == serverApi.ResourceType.Artist) ? (limit || -1) : undefined, + albumOffset: (type == serverApi.ResourceType.Album) ? (offset || 0) : undefined, + albumLimit: (type == serverApi.ResourceType.Album) ? (limit || -1) : undefined, + trackOffset: (type == serverApi.ResourceType.Track) ? (offset || 0) : undefined, + trackLimit: (type == serverApi.ResourceType.Track) ? (limit || -1) : undefined, + tagOffset: (type == serverApi.ResourceType.Tag) ? (offset || 0) : undefined, + tagLimit: (type == serverApi.ResourceType.Tag) ? (limit || -1) : undefined, }, ordering: { orderBy: { @@ -50,7 +58,7 @@ export async function queryArtists( limit: number | undefined, responseType: serverApi.QueryResponseType, ): Promise { - 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; } @@ -60,7 +68,7 @@ export async function queryAlbums( limit: number | undefined, responseType: serverApi.QueryResponseType, ): Promise { - 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; } @@ -70,7 +78,7 @@ export async function queryTracks( limit: number | undefined, responseType: serverApi.QueryResponseType, ): Promise { - 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; } @@ -80,6 +88,6 @@ export async function queryTags( limit: number | undefined, responseType: serverApi.QueryResponseType, ): Promise { - 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; } \ No newline at end of file diff --git a/client/src/lib/query/Query.tsx b/client/src/lib/query/Query.tsx index ea94a47..21f3a27 100644 --- a/client/src/lib/query/Query.tsx +++ b/client/src/lib/query/Query.tsx @@ -1,5 +1,12 @@ import * as serverApi from '../../api/api'; +export enum QueryFor { + Artists = 0, + Albums, + Tags, + Tracks, +} + export enum QueryLeafBy { ArtistName = 0, ArtistId, @@ -9,9 +16,7 @@ export enum QueryLeafBy { TagId, TrackName, TrackId, - TrackStoreLinks, - ArtistStoreLinks, - AlbumStoreLinks, + StoreLinks, } export enum QueryLeafOp { @@ -26,7 +31,7 @@ export interface TagQueryInfo { } export function isTagQueryInfo(e: any): e is TagQueryInfo { return (typeof e === 'object') && 'matchIds' in e && 'fullName' in e; - } +} export type QueryLeafOperand = string | number | TagQueryInfo; @@ -77,6 +82,43 @@ export function queryNot(arg: QueryElem) { 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 // leaves. They should be placed so that all possible node combinations // from the existing nodes could have an added combinational leaf. @@ -162,11 +204,11 @@ export function removePlaceholders(q: QueryElem | null): QueryElem | null { 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)) { var newOperands: QueryElem[] = []; q.operands.forEach((o: QueryElem) => { - const s = simplify(o); + const s = simplify(o, queryFor); if (s !== null) { newOperands.push(s); } }) 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 q; + if (q && isLeafElem(q)) { + if (mapToServerLeafOp(q.leafOp, queryFor) === null || + mapToServerProperty(q.a, queryFor) === null) { + return null; + } + } + + return q; } -export function toApiQuery(q: QueryElem) : serverApi.Query { - 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)) { +export function toApiQuery(q: QueryElem, queryFor: QueryFor | null): serverApi.Query { + if (isLeafElem(q) && isTagQueryInfo(q.b)) { // Special case for tag queries by ID const r: serverApi.QueryElem = { prop: serverApi.QueryElemProperty.tagId, @@ -208,18 +235,29 @@ export function toApiQuery(q: QueryElem) : serverApi.Query { propOperand: q.b.matchIds, } 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 const r: serverApi.QueryElem = { - prop: propsMapping[q.a], - propOperator: leafOpsMapping[q.leafOp], + prop: a, + propOperator: op, propOperand: q.b, } 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 = { - children: q.operands.map((op: any) => toApiQuery(op)), - childrenOperator: nodeOpsMapping[q.nodeOp] + children: q.operands.map((op: any) => toApiQuery(op, queryFor)), + childrenOperator: op } return r; }