Skip to content
Open
26 changes: 23 additions & 3 deletions src/embed/hostEventClient/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,29 @@ export interface Applicability {
}

export interface FilterUpdate {
column: string;
oper: string;
values: string[];
/**
* Name of the column to filter on.
* @deprecated Use `columnName`, which matches {@link RuntimeFilter} and the
* payload emitted by `EmbedEvent.FilterChanged`. Still accepted, and still
* what is sent on the wire.
*/
column?: string;
/**
* Name of the column to filter on. Preferred over `column`.
*/
columnName?: string;
/**
* Filter operator, for example EQ, IN, CONTAINS.
* @deprecated Use `operator`, which matches {@link RuntimeFilter} and the
* payload emitted by `EmbedEvent.FilterChanged`. Still accepted, and still
* what is sent on the wire.
*/
oper?: string;
/**
* Filter operator, for example EQ, IN, CONTAINS. Preferred over `oper`.
*/
operator?: string;
Comment on lines +36 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc comments here are published to developers.thoughtspot.com, and TypeDoc attaches a leading comment only to the declaration immediately below it — so columnName and operator (the new alias properties) will render with no description at all, while column/oper get one. Suggest giving the aliases their own (even one-line) doc comments.

Suggested change
/**
* Name of the column to filter on. `columnName` is an accepted alias, so a
* payload produced by `convertFilterChangedToUpdateFiltersPayload` can be
* passed straight through.
*/
column?: string;
columnName?: string;
/**
* Operator to apply. `operator` is an accepted alias.
*/
oper?: string;
operator?: string;
/**
* Name of the column to filter on. `columnName` is an accepted alias, so a
* payload produced by `convertFilterChangedToUpdateFiltersPayload` can be
* passed straight through.
*/
column?: string;
/**
* Alias for `column`.
*/
columnName?: string;
/**
* Operator to apply. `operator` is an accepted alias.
*/
oper?: string;
/**
* Alias for `oper`.
*/
operator?: string;

values: (string | number | boolean | bigint)[];
type?: string;
applicability?: Applicability;
}
Expand Down
8 changes: 7 additions & 1 deletion src/embed/hostEventClient/host-event-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { processTrigger as processTriggerService } from '../../utils/processTrig
import { getEmbedConfig } from '../embedConfig';
import {
isValidUpdateFiltersPayload,
resolveUpdateFiltersAliases,
isValidUpdateParametersPayload,
isValidDrillDownPayload,
throwUpdateFiltersValidationError,
Expand Down Expand Up @@ -244,7 +245,12 @@ export class HostEventClient {
throwUpdateFiltersValidationError();
}

return this.handleHostEventWithParam(UIPassthroughEvent.UpdateFilters, payload, context as ContextType);
// The payload is forwarded to the embedded app as-is, so swap the
// columnName/operator aliases for column/oper first - otherwise a payload
// using the alias passes validation and is then ignored downstream.
const resolvedPayload = resolveUpdateFiltersAliases(payload);

return this.handleHostEventWithParam(UIPassthroughEvent.UpdateFilters, resolvedPayload, context as ContextType);
}

protected handleUpdateParametersEvent(
Expand Down
83 changes: 82 additions & 1 deletion src/embed/hostEventClient/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
isValidUpdateFiltersPayload,
resolveUpdateFiltersAliases,
isValidUpdateParametersPayload,
isValidDrillDownPayload,
createValidationError,
Expand Down Expand Up @@ -426,4 +427,84 @@ describe('hostEventClient utils', () => {
.toThrow(ERROR_MESSAGE.DRILLDOWN_INVALID_PAYLOAD);
});
});
});
describe('resolveUpdateFiltersAliases', () => {
// The payload is forwarded to the embedded app verbatim, and only
// column/oper are known to be understood there. Without this step a
// payload using the columnName/operator aliases passes validation and
// is then silently ignored downstream.
it('rewrites columnName/operator to column/oper on a single filter', () => {
const out = resolveUpdateFiltersAliases({
filter: { columnName: 'region', operator: 'EQ', values: ['west'] },
} as any);

expect(out.filter).toEqual({ column: 'region', oper: 'EQ', values: ['west'] });
expect(out.filter).not.toHaveProperty('columnName');
expect(out.filter).not.toHaveProperty('operator');
});

it('rewrites every entry in a filters array', () => {
const out = resolveUpdateFiltersAliases({
filters: [
{ columnName: 'region', operator: 'EQ', values: ['west'] },
{ columnName: 'item type', operator: 'IN', values: ['bags'] },
],
} as any);

expect(out.filters).toEqual([
{ column: 'region', oper: 'EQ', values: ['west'] },
{ column: 'item type', oper: 'IN', values: ['bags'] },
]);
});

it('leaves a payload that already uses column/oper untouched', () => {
const payload = { filter: { column: 'region', oper: 'EQ', values: ['west'] } } as any;

expect(resolveUpdateFiltersAliases(payload)).toEqual(payload);
});

it('prefers column/oper when both spellings are present', () => {
const out = resolveUpdateFiltersAliases({
filter: {
column: 'from-column', columnName: 'from-columnName',
oper: 'EQ', operator: 'IN',
values: ['x'],
},
} as any);

expect(out.filter).toEqual({ column: 'from-column', oper: 'EQ', values: ['x'] });
});

it('preserves the other filter fields, including applicability', () => {
const out = resolveUpdateFiltersAliases({
filter: {
columnName: 'date',
operator: 'EQ',
values: [1700000000],
type: 'EXACT_DATE',
applicability: { level: 'TAB', targetId: 'tab-guid' },
},
} as any);

expect(out.filter).toEqual({
column: 'date',
oper: 'EQ',
values: [1700000000],
type: 'EXACT_DATE',
applicability: { level: 'TAB', targetId: 'tab-guid' },
});
});

it('produces a payload that still passes validation', () => {
const out = resolveUpdateFiltersAliases({
filters: [{ columnName: 'region', operator: 'EQ', values: ['west'] }],
} as any);

expect(isValidUpdateFiltersPayload(out as any)).toBe(true);
});

it('is a no-op on a non-object payload', () => {
expect(resolveUpdateFiltersAliases(undefined as any)).toBeUndefined();
expect(resolveUpdateFiltersAliases(null as any)).toBeNull();
});
});
});
42 changes: 42 additions & 0 deletions src/embed/hostEventClient/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ export function isValidUpdateFiltersPayload(
return !!(hasValidFilter || hasValidFilters);
}

/**
* Rewrites one filter's `columnName`/`operator` to `column`/`oper`.
*
* Callers may use either spelling: `columnName`/`operator` match
* {@link RuntimeFilter} and the `EmbedEvent.FilterChanged` payload, so a
* converted payload can be replayed as-is. But the payload is forwarded to the
* embedded app unchanged, and only `column`/`oper` are known to be understood
* there - so the alias must be swapped here, or it would pass validation and
* then be silently ignored.
* @param filter One entry from the UpdateFilters payload.
*/
function resolveFilterAliases<T extends { column?: string; columnName?: string; oper?: string; operator?: string }>(
filter: T,
): T {
if (!isPlainObject(filter)) return filter;

const column = filter.column ?? filter.columnName;
const oper = filter.oper ?? filter.operator;
const { columnName, operator, ...rest } = filter;

return {
...rest,
...(isUndefined(column) ? {} : { column }),
...(isUndefined(oper) ? {} : { oper }),
} as T;
}

/**
* Rewrites every filter in an UpdateFilters payload to use `column`/`oper`,
* whichever spelling the caller supplied. Call after validation.
* @param payload The UpdateFilters host event payload.
*/
export function resolveUpdateFiltersAliases<T extends { filter?: any; filters?: any[] }>(payload: T): T {
if (!isPlainObject(payload)) return payload;

return {
...payload,
...(payload.filter ? { filter: resolveFilterAliases(payload.filter) } : {}),
...(Array.isArray(payload.filters) ? { filters: payload.filters.map(resolveFilterAliases) } : {}),
};
}

export function isValidUpdateParametersPayload(payload: unknown): boolean {
// Only validates the applicability of each parameter (null treated as absent); the rest is forwarded as-is for backward compatibility.
if (!Array.isArray(payload)) return true;
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ import {
LiveboardFilter,
LiveboardParameter,
} from './embed/hostEventClient/contracts';
import {
convertFilterChangedToUpdateFiltersPayload,
FilterChangedPayload,
UpdateFiltersFilterParam,
UpdateFiltersPayload,
} from './utils/filterConverter';

export {
init,
Expand Down Expand Up @@ -228,6 +234,10 @@ export {
DataLabelFilterOperator,
TableTheme,
TableContentDensity,
convertFilterChangedToUpdateFiltersPayload,
FilterChangedPayload,
UpdateFiltersFilterParam,
UpdateFiltersPayload,
};

export { resetCachedAuthToken } from './authToken';
Expand Down
4 changes: 4 additions & 0 deletions src/react/all-types-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,8 @@ export {
StarterPromptCategory,
StarterPreviewDataCategory,
StarterPromptQuestion,
convertFilterChangedToUpdateFiltersPayload,
FilterChangedPayload,
UpdateFiltersFilterParam,
UpdateFiltersPayload,
} from '../index';
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5875,6 +5875,10 @@ export enum HostEvent {
* }
* });
* ```
* `columnName` and `operator` are also accepted as aliases for `column`
Comment on lines 5875 to +5878

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag-ordering (style guide rule 1): this new paragraph about the columnName/operator aliases and convertFilterChangedToUpdateFiltersPayload is inserted after all the @example blocks but before @version. Per the canonical order, free-text description belongs with the short description at the top of the comment, not sandwiched between @example and @version. Consider moving it up to the main description instead.

* and `oper` respectively. To reapply the filter state captured from
* {@link EmbedEvent.FilterChanged}, use `convertFilterChangedToUpdateFiltersPayload`
* to convert its payload into the shape expected here.
* @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
*/
UpdateFilters = 'updateFilters',
Expand Down
Loading
Loading