Skip to content

Commit

Permalink
feature/boolean filter query endpoint (#1024)
Browse files Browse the repository at this point in the history
* nest query endpoints and prepare one for boolean

* boolean filter query endpoint and usage

* fix tests

* extract api call creation

* reuse type

* fix spread of arguments
  • Loading branch information
adrianmroz-allegro authored Feb 8, 2023
1 parent f2d49c0 commit 4d0bd34
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { OauthMessageView } from "../../oauth/oauth-message-view";
import { Ajax } from "../../utils/ajax/ajax";
import { reportError } from "../../utils/error-reporter/error-reporter";
import { replaceHash } from "../../utils/url/url";
import { ApiContext } from "../../views/cube-view/api-context";
import { ApiContext, CreateApiContext } from "../../views/cube-view/api-context";
import { CubeView } from "../../views/cube-view/cube-view";
import { SettingsContext, SettingsContextValue } from "../../views/cube-view/settings-context";
import { GeneralError } from "../../views/error-view/general-error";
Expand Down Expand Up @@ -242,12 +242,12 @@ export class TurniloApplication extends React.Component<TurniloApplicationProps,
return <React.StrictMode>
<main className="turnilo-application">
<SettingsContext.Provider value={this.getSettingsContext()}>
<ApiContext.Provider appSettings={this.props.appSettings}>
<CreateApiContext appSettings={this.props.appSettings}>
{this.renderView()}
{this.renderAboutModal()}
<Notifications/>
<Questions/>
</ApiContext.Provider>
</CreateApiContext>
</SettingsContext.Provider>
</main>
</React.StrictMode>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
*/

import { Set } from "immutable";
import { $, Dataset } from "plywood";
import { Dataset } from "plywood";
import React from "react";
import { Dimension } from "../../../../common/models/dimension/dimension";
import { Essence } from "../../../../common/models/essence/essence";
import { BooleanFilterClause, FilterClause } from "../../../../common/models/filter-clause/filter-clause";
import { Stage } from "../../../../common/models/stage/stage";
import { Timekeeper } from "../../../../common/models/timekeeper/timekeeper";
import { Unary } from "../../../../common/utils/functional/functional";
import { Fn } from "../../../../common/utils/general/general";
import { STRINGS } from "../../../config/constants";
import { ApiContext, ApiContextValue } from "../../../views/cube-view/api-context";
import { BubbleMenu } from "../../bubble-menu/bubble-menu";
import { Button } from "../../button/button";
import { Checkbox } from "../../checkbox/checkbox";
Expand All @@ -36,7 +36,6 @@ interface BooleanFilterMenuProps {
dimension: Dimension;
saveClause: Unary<FilterClause, void>;
essence: Essence;
timekeeper: Timekeeper;
onClose: Fn;
containerStage?: Stage;
openOn: Element;
Expand All @@ -52,6 +51,9 @@ interface BooleanFilterMenuState {
}

export class BooleanFilterMenu extends React.Component<BooleanFilterMenuProps, BooleanFilterMenuState> {
static contextType = ApiContext;

context: ApiContextValue;

state = this.initialValues();

Expand All @@ -72,16 +74,11 @@ export class BooleanFilterMenu extends React.Component<BooleanFilterMenuProps, B
}

fetchData() {
const { essence, timekeeper, dimension } = this.props;
const { dataCube } = essence;
const filterExpression = essence.getEffectiveFilter(timekeeper, { unfilterDimension: dimension }).toExpression(dataCube);

const query = $("main")
.filter(filterExpression)
.split(dimension.expression, dimension.name);
const { booleanFilterQuery } = this.context;
const { essence, dimension } = this.props;

this.setState({ loading: true });
dataCube.executor(query, { timezone: essence.timezone })
booleanFilterQuery(essence, dimension)
.then(
(dataset: Dataset) => {
this.setState({
Expand Down
77 changes: 55 additions & 22 deletions src/client/views/cube-view/api-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,91 @@
import { Dataset, DatasetJS } from "plywood";
import React, { useContext } from "react";
import { ClientAppSettings } from "../../../common/models/app-settings/app-settings";
import { Dimension } from "../../../common/models/dimension/dimension";
import { Essence } from "../../../common/models/essence/essence";
import { Unary } from "../../../common/utils/functional/functional";
import { Binary, Unary } from "../../../common/utils/functional/functional";
import { DEFAULT_VIEW_DEFINITION_VERSION, definitionConverters } from "../../../common/view-definitions";
import { Ajax } from "../../utils/ajax/ajax";

export type QueryCall = Unary<Essence, Promise<Dataset>>;
export type VisualizationQuery = Unary<Essence, Promise<Dataset>>;

interface ApiContextValue {
query: QueryCall;
export type BooleanFilterQuery = Binary<Essence, Dimension, Promise<Dataset>>;

export interface ApiContextValue {
visualizationQuery: VisualizationQuery;
booleanFilterQuery: BooleanFilterQuery;
}

const InternalApiContext = React.createContext<ApiContextValue>({
get query(): QueryCall {
export const ApiContext = React.createContext<ApiContextValue>({
get booleanFilterQuery(): BooleanFilterQuery {
throw new Error("Attempted to consume ApiContext when there was no Provider");
},
get visualizationQuery(): VisualizationQuery {
throw new Error("Attempted to consume ApiContext when there was no Provider");
}
});

export function useApiContext(): ApiContextValue {
return useContext(InternalApiContext);
return useContext(ApiContext);
}

function createQueryApi({ clientTimeout: timeout, oauth }: ClientAppSettings): QueryCall {
interface QueryResponse {
result: DatasetJS;
}

type QueryEndpoints = "visualization" | "boolean-filter";

type ExtraParams = Record<string, unknown>;

type SerializeExtraBase = (...args: any[]) => ExtraParams;
type QueryFunction<T extends SerializeExtraBase> = (essence: Essence, ...args: Parameters<T>) => Promise<Dataset>;

function createApiCall<T extends SerializeExtraBase>(settings: ClientAppSettings, query: QueryEndpoints, serializeExtraParams: T): QueryFunction<T> {
const { oauth, clientTimeout: timeout } = settings;
const viewDefinitionVersion = DEFAULT_VIEW_DEFINITION_VERSION;
const converter = definitionConverters[viewDefinitionVersion];
return (essence: Essence) => {
return (essence: Essence, ...args: Parameters<T>) => {
const extra = serializeExtraParams(...args);
const { dataCube: { name } } = essence;
const viewDefinition = converter.toViewDefinition(essence);
return Ajax.query<{ result: DatasetJS }>({
return Ajax.query<QueryResponse>({
method: "POST",
url: "query",
url: `query/${query}`,
timeout,
data: {
viewDefinitionVersion,
dataCube: name,
viewDefinition
viewDefinition,
...extra
}
}, oauth).then(res => Dataset.fromJS(res.result));
}, oauth).then(constructDataset);
};
}

const constructDataset = (res: QueryResponse) => Dataset.fromJS(res.result);

function createVizQueryApi(settings: ClientAppSettings): VisualizationQuery {
return createApiCall(settings, "visualization", () => ({}));
}

function createBooleanFilterQuery(settings: ClientAppSettings): BooleanFilterQuery {
return createApiCall(settings, "boolean-filter", (dimension: Dimension) => ({ dimension: dimension.name }));
}

function createApi(settings: ClientAppSettings): ApiContextValue {
return {
booleanFilterQuery: createBooleanFilterQuery(settings),
visualizationQuery: createVizQueryApi(settings)
};
}

interface ApiContextProviderProps {
appSettings: ClientAppSettings;
}

const Provider: React.FunctionComponent<ApiContextProviderProps> = ({ children, appSettings }) => {
const value = { query: createQueryApi(appSettings) };
return <InternalApiContext.Provider value={value}>
export const CreateApiContext: React.FunctionComponent<ApiContextProviderProps> = ({ children, appSettings }) => {
const value = createApi(appSettings);
return <ApiContext.Provider value={value}>
{children}
</InternalApiContext.Provider>;
};

export const ApiContext = {
Provider,
Consumer: InternalApiContext.Consumer
</ApiContext.Provider>;
};
4 changes: 2 additions & 2 deletions src/client/views/cube-view/center-panel/center-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ function ChartWrapper(props: ChartWrapperProps) {
return <HighlightController essence={essence} clicker={clicker}>
{highlightProps =>
<ApiContext.Consumer>
{({ query }) =>
{({ visualizationQuery }) =>
<DataProvider
refreshRequestTimestamp={lastRefreshRequestTimestamp}
query={query}
query={visualizationQuery}
essence={essence}
timekeeper={timekeeper}
stage={stage}>
Expand Down
4 changes: 2 additions & 2 deletions src/client/visualizations/data-provider/data-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ import { debounceWithPromise, Unary } from "../../../common/utils/functional/fun
import { Loader } from "../../components/loader/loader";
import { QueryError } from "../../components/query-error/query-error";
import { reportError } from "../../utils/error-reporter/error-reporter";
import { QueryCall } from "../../views/cube-view/api-context";
import { VisualizationQuery } from "../../views/cube-view/api-context";
import { DownloadableDataset, DownloadableDatasetContext } from "../../views/cube-view/downloadable-dataset-context";

interface DataProviderProps {
refreshRequestTimestamp: number;
essence: Essence;
timekeeper: Timekeeper;
stage: Stage;
query: QueryCall;
query: VisualizationQuery;
children: Unary<Dataset, React.ReactNode>;
}

Expand Down
109 changes: 55 additions & 54 deletions src/server/routes/query/query.mocha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
*/

import bodyParser from "body-parser";
import { expect } from "chai";
import express from "express";
import supertest, { Response } from "supertest";
import supertest from "supertest";
import { NOOP_LOGGER } from "../../../common/logger/logger";
import { appSettings } from "../../../common/models/app-settings/app-settings.fixtures";
import { wikiSourcesWithExecutor } from "../../../common/models/sources/sources.fixtures";
Expand All @@ -40,61 +39,63 @@ app.use(bodyParser.json());
app.use("/", queryRouter(settingsManagerFixture));

describe("query router", () => {
it("should require dataCube", (testComplete: any) => {
supertest(app)
.post("/")
.set("Content-Type", "application/json")
.send({})
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "must have a dataCube" })
.end(testComplete);
});
describe("visualization endpoint", () => {
it("should require dataCube", (testComplete: any) => {
supertest(app)
.post("/visualization")
.set("Content-Type", "application/json")
.send({})
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "must have a dataCube" })
.end(testComplete);
});

it("should validate viewDefinition", (testComplete: any) => {
supertest(app)
.post("/")
.set("Content-Type", "application/json")
.send({ dataCube: "wiki" })
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "viewDefinition must be an object" })
.end(testComplete);
});
it("should validate viewDefinition", (testComplete: any) => {
supertest(app)
.post("/visualization")
.set("Content-Type", "application/json")
.send({ dataCube: "wiki" })
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "viewDefinition must be an object" })
.end(testComplete);
});

it("should require viewDefinitionVersion", (testComplete: any) => {
supertest(app)
.post("/")
.set("Content-Type", "application/json")
.send({ dataCube: "wiki", viewDefinition: {} })
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "must have a viewDefinitionVersion" })
.end(testComplete);
});
it("should require viewDefinitionVersion", (testComplete: any) => {
supertest(app)
.post("/visualization")
.set("Content-Type", "application/json")
.send({ dataCube: "wiki", viewDefinition: {} })
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "must have a viewDefinitionVersion" })
.end(testComplete);
});

it("should validate viewDefinitionVersion", (testComplete: any) => {
supertest(app)
.post("/")
.set("Content-Type", "application/json")
.send({ dataCube: "wiki", viewDefinition: {}, viewDefinitionVersion: "foobar" })
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "unsupported viewDefinitionVersion value" })
.end(testComplete);
});
it("should validate viewDefinitionVersion", (testComplete: any) => {
supertest(app)
.post("/visualization")
.set("Content-Type", "application/json")
.send({ dataCube: "wiki", viewDefinition: {}, viewDefinitionVersion: "foobar" })
.expect("Content-Type", "application/json; charset=utf-8")
.expect(400)
.expect({ error: "unsupported viewDefinitionVersion value" })
.end(testComplete);
});

it("should return 200 for valid parameters", (testComplete: any) => {
supertest(app)
.post("/")
.set("Content-Type", "application/json")
.send({
dataCube: "wiki",
viewDefinitionVersion: "4",
viewDefinition: total
})
.expect("Content-Type", "application/json; charset=utf-8")
.expect(200)
.end(testComplete);
it("should return 200 for valid parameters", (testComplete: any) => {
supertest(app)
.post("/visualization")
.set("Content-Type", "application/json")
.send({
dataCube: "wiki",
viewDefinitionVersion: "4",
viewDefinition: total
})
.expect("Content-Type", "application/json; charset=utf-8")
.expect(200)
.end(testComplete);
});
});
});
Loading

0 comments on commit 4d0bd34

Please sign in to comment.