From 224a503ddb9e8f2c6dedf71b4aa130c3aa315b3e Mon Sep 17 00:00:00 2001 From: akshaykumar2505 Date: Mon, 17 Aug 2026 18:46:26 +0530 Subject: [PATCH] fix(Playground): handle undefined operators and improve filter processing - Updated PlaygroundFilterGroup to safely map operators, preventing errors when operators are undefined. - Enhanced useDataSourcesMeta to include validation for member names and added a new utility function to filter member names. - Improved filter processing in parseRestBodyToPlaygroundState to flatten filters and normalize time dimensions, ensuring accurate state representation. Co-authored-by: Cursor --- .../PlaygroundFilterGroup/index.tsx | 2 +- src/hooks/useDataSourcesMeta.ts | 49 +++++-- .../parseRestBodyToPlaygroundState.test.ts | 125 ++++++++++++++++++ .../helpers/parseRestBodyToPlaygroundState.ts | 110 ++++++++++++++- 4 files changed, 267 insertions(+), 19 deletions(-) diff --git a/src/components/PlaygroundFilterGroup/index.tsx b/src/components/PlaygroundFilterGroup/index.tsx index b988688b..fa7b076c 100644 --- a/src/components/PlaygroundFilterGroup/index.tsx +++ b/src/components/PlaygroundFilterGroup/index.tsx @@ -46,7 +46,7 @@ const FilterGroup: FC = ({ size="large" suffixIcon={} > - {m.operators!.map((operator) => ( + {(m.operators || []).map((operator) => ( {t(operator.title)} diff --git a/src/hooks/useDataSourcesMeta.ts b/src/hooks/useDataSourcesMeta.ts index 2005a277..f6285710 100644 --- a/src/hooks/useDataSourcesMeta.ts +++ b/src/hooks/useDataSourcesMeta.ts @@ -85,6 +85,13 @@ class Meta { } resolveMember(memberName: string, memberType: string | string[]) { + if (!memberName || typeof memberName !== "string") { + return { + title: String(memberName ?? ""), + error: `Path not found '${memberName}'`, + }; + } + const [cube] = memberName.split("."); if (!this.cubesMap[cube]) { @@ -119,6 +126,13 @@ class Meta { } } +const filterMemberName = (filter: any): string | undefined => { + if (typeof filter?.dimension === "string") return filter.dimension; + if (typeof filter?.member === "string") return filter.member; + if (typeof filter?.dimension?.name === "string") return filter.dimension.name; + return undefined; +}; + const enrichPlaygroundMembers = (cubesMetaCls: Meta, playgroundState: any) => { const resolveWithIndex = (key: string) => getOr([], key, playgroundState).map((value: string, index: number) => ({ @@ -135,20 +149,24 @@ const enrichPlaygroundMembers = (cubesMetaCls: Meta, playgroundState: any) => { }) ); - const filters = getOr([], "filters", playgroundState).map( - (m: any, index: number) => ({ - ...m, - dimension: cubesMetaCls.resolveMember(m.dimension, [ - "dimensions", - "measures", - ]), - operators: cubesMetaCls.filterOperatorsForMember(m.dimension, [ - "dimensions", - "measures", - ]), - index, + const filters = getOr([], "filters", playgroundState) + .map((m: any, index: number) => { + const memberName = filterMemberName(m); + if (!memberName) return null; + return { + ...m, + dimension: cubesMetaCls.resolveMember(memberName, [ + "dimensions", + "measures", + ]), + operators: cubesMetaCls.filterOperatorsForMember(memberName, [ + "dimensions", + "measures", + ]), + index, + }; }) - ); + .filter(Boolean); const enrichedMembers = { measures: resolveWithIndex("measures"), @@ -175,7 +193,10 @@ const updatePlaygroundState = (playgroundState: any, cubesMeta: Meta) => { const filteredArray = playgroundState[curKey].filter((m: any) => { let resolved: any = false; if (curKey === "filters") { - resolved = cubesMeta.resolveMember(m.dimension, [ + if (Array.isArray(m?.or) || Array.isArray(m?.and)) { + return true; + } + resolved = cubesMeta.resolveMember(filterMemberName(m), [ "dimensions", "measures", ]); diff --git a/src/utils/helpers/parseRestBodyToPlaygroundState.test.ts b/src/utils/helpers/parseRestBodyToPlaygroundState.test.ts index 15e11147..d8217477 100644 --- a/src/utils/helpers/parseRestBodyToPlaygroundState.test.ts +++ b/src/utils/helpers/parseRestBodyToPlaygroundState.test.ts @@ -53,4 +53,129 @@ describe("parseRestBodyToPlaygroundState", () => { test("throws on invalid json", () => { expect(() => parseRestBodyToPlaygroundState("{")).toThrow("Invalid JSON"); }); + + test("maps member filters and dateRange-only timeDimensions", () => { + const state = parseRestBodyToPlaygroundState( + JSON.stringify({ + query: { + measures: [ + "semantic_events.count", + "semantic_events.count_share_pct", + ], + dimensions: ["semantic_events.dimensions_website"], + segments: ["semantic_events.document_analysed"], + filters: [ + { + member: "semantic_events.dimensions_website", + operator: "set", + }, + ], + timeDimensions: [ + { + dimension: "semantic_events.timestamp", + dateRange: [ + "2026-07-01T00:00:00.000", + "2026-07-01T23:59:59.999", + ], + }, + ], + order: { "semantic_events.count": "desc" }, + timezone: "UTC", + limit: 100, + offset: 0, + }, + }) + ); + + expect(state.measures).toEqual([ + "semantic_events.count", + "semantic_events.count_share_pct", + ]); + expect(state.dimensions).toEqual(["semantic_events.dimensions_website"]); + expect(state.segments).toEqual(["semantic_events.document_analysed"]); + expect(state.filters).toEqual([ + { + dimension: "semantic_events.dimensions_website", + operator: "set", + values: [], + }, + { + dimension: "semantic_events.timestamp", + operator: "inDateRange", + values: ["2026-07-01T00:00:00.000", "2026-07-01T23:59:59.999"], + }, + ]); + expect(state.timeDimensions).toEqual([]); + expect(state.order).toEqual([{ id: "semantic_events.count", desc: true }]); + expect(state.limit).toBe(100); + expect(state.offset).toBe(0); + }); + + test("keeps granularity and converts dateRange to a filter", () => { + const state = parseRestBodyToPlaygroundState( + JSON.stringify({ + measures: ["Orders.count"], + timeDimensions: [ + { + dimension: "Orders.createdAt", + granularity: "day", + dateRange: ["2026-07-01", "2026-07-31"], + }, + ], + }) + ); + + expect(state.timeDimensions).toEqual([ + { dimension: "Orders.createdAt", granularity: "day" }, + ]); + expect(state.filters).toEqual([ + { + dimension: "Orders.createdAt", + operator: "inDateRange", + values: ["2026-07-01", "2026-07-31"], + }, + ]); + }); + + test("flattens and-groups and keeps or-groups", () => { + const state = parseRestBodyToPlaygroundState( + JSON.stringify({ + filters: [ + { + and: [ + { member: "Orders.status", operator: "equals", values: ["open"] }, + { dimension: "Orders.city", operator: "set" }, + ], + }, + { + or: [ + { member: "Orders.country", operator: "equals", values: ["IS"] }, + ], + }, + ], + }) + ); + + expect(state.filters).toEqual([ + { + dimension: "Orders.status", + operator: "equals", + values: ["open"], + }, + { + dimension: "Orders.city", + operator: "set", + values: [], + }, + { + or: [ + { + dimension: "Orders.country", + operator: "equals", + values: ["IS"], + }, + ], + }, + ]); + }); }); diff --git a/src/utils/helpers/parseRestBodyToPlaygroundState.ts b/src/utils/helpers/parseRestBodyToPlaygroundState.ts index 26dff054..b411488e 100644 --- a/src/utils/helpers/parseRestBodyToPlaygroundState.ts +++ b/src/utils/helpers/parseRestBodyToPlaygroundState.ts @@ -15,6 +15,8 @@ const QUERY_KEYS = [ "page", ] as const; +const UNARY_FILTER_OPERATORS = new Set(["set", "notSet"]); + const isPlainObject = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value); @@ -64,6 +66,102 @@ const extractQuery = (parsed: unknown): Record => { ); }; +const filterMemberName = (filter: Record): string | undefined => { + if (typeof filter.dimension === "string") return filter.dimension; + if (typeof filter.member === "string") return filter.member; + return undefined; +}; + +const normalizeFilterValues = ( + operator: string, + values: unknown +): unknown[] => { + if (Array.isArray(values)) return values; + if (UNARY_FILTER_OPERATORS.has(operator)) return []; + if (values == null) return []; + return [values]; +}; + +const flattenFilters = (filters: unknown[]): Record[] => { + const out: Record[] = []; + + for (const filter of filters) { + if (!isPlainObject(filter)) continue; + + if (Array.isArray(filter.and)) { + out.push(...flattenFilters(filter.and)); + continue; + } + + // Keep OR groups intact so Cube.js still ORs them at runtime. + if (Array.isArray(filter.or)) { + const children = flattenFilters(filter.or); + if (children.length > 0) { + out.push({ or: children }); + } + continue; + } + + const member = filterMemberName(filter); + if (!member || typeof filter.operator !== "string") continue; + + out.push({ + dimension: member, + operator: filter.operator, + values: normalizeFilterValues(filter.operator, filter.values), + }); + } + + return out; +}; + +const normalizeTimeDimensions = ( + timeDimensions: unknown[], + filters: Record[] +): { dimension: string; granularity?: string; dateRange?: unknown }[] => { + const next: { dimension: string; granularity?: string; dateRange?: unknown }[] = + []; + + for (const td of timeDimensions) { + if (!isPlainObject(td) || typeof td.dimension !== "string") continue; + + const granularity = + typeof td.granularity === "string" ? td.granularity : undefined; + const dateRange = td.dateRange; + + if (Array.isArray(dateRange) && dateRange.length > 0) { + filters.push({ + dimension: td.dimension, + operator: "inDateRange", + values: dateRange.map((value) => String(value)), + }); + } + + if (granularity) { + const item: { + dimension: string; + granularity: string; + dateRange?: unknown; + } = { + dimension: td.dimension, + granularity, + }; + if (typeof dateRange === "string") { + item.dateRange = dateRange; + } + next.push(item); + continue; + } + + // Named ranges ("Last 7 days") only exist on timeDimensions in Cube.js. + if (typeof dateRange === "string") { + next.push({ dimension: td.dimension, dateRange }); + } + } + + return next; +}; + /** * Parse a Cube.js REST /v1/load body (or a bare query) into Explore playground state. */ @@ -79,14 +177,18 @@ export const parseRestBodyToPlaygroundState = ( } const query = extractQuery(parsed); + const filters = flattenFilters(Array.isArray(query.filters) ? query.filters : []); + const timeDimensions = normalizeTimeDimensions( + Array.isArray(query.timeDimensions) ? query.timeDimensions : [], + filters + ); + const next: PlaygroundState = { ...initialState, measures: Array.isArray(query.measures) ? query.measures : [], dimensions: Array.isArray(query.dimensions) ? query.dimensions : [], - filters: Array.isArray(query.filters) ? (query.filters as any) : [], - timeDimensions: Array.isArray(query.timeDimensions) - ? (query.timeDimensions as PlaygroundState["timeDimensions"]) - : [], + filters: filters as PlaygroundState["filters"], + timeDimensions: timeDimensions as PlaygroundState["timeDimensions"], segments: Array.isArray(query.segments) ? (query.segments as any) : [], order: normalizeOrder(query.order), timezone: