diff --git a/superset/assets/jest.config.js b/superset/assets/jest.config.js index 64dcb6f3b33..858f6f92954 100644 --- a/superset/assets/jest.config.js +++ b/superset/assets/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - testRegex: '\\/spec\\/.*_spec\\.(j|t)sx?$', + testRegex: '\\/spec\\/.*(_spec|\\.test)\\.(j|t)sx?$', moduleNameMapper: { '\\.(css|less)$': '/spec/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg)$': '/spec/__mocks__/fileMock.js', diff --git a/superset/assets/spec/javascripts/superset-ui/WordCloudBuildQuery.test.ts b/superset/assets/spec/javascripts/superset-ui/WordCloudBuildQuery.test.ts new file mode 100644 index 00000000000..a6edfbc31d7 --- /dev/null +++ b/superset/assets/spec/javascripts/superset-ui/WordCloudBuildQuery.test.ts @@ -0,0 +1,15 @@ +import buildQuery from 'src/visualizations/wordcloud/buildQuery'; + +describe('WordCloud buildQuery', () => { + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + series: 'foo', + }; + + it('should build groupby with series in form data', () => { + const queryContext = buildQuery(formData); + const [ query ] = queryContext.queries; + expect(query.groupby).toEqual(['foo']); + }); +}); diff --git a/superset/assets/spec/javascripts/superset-ui/buildQueryContext.test.ts b/superset/assets/spec/javascripts/superset-ui/buildQueryContext.test.ts new file mode 100644 index 00000000000..b6fd995f4d0 --- /dev/null +++ b/superset/assets/spec/javascripts/superset-ui/buildQueryContext.test.ts @@ -0,0 +1,22 @@ +import build from 'src/query/buildQueryContext'; +import * as queryObjectBuilder from 'src/query/buildQueryObject'; + +describe('queryContextBuilder', () => { + it('should build datasource for table sources', () => { + const queryContext = build({ datasource: '5__table', granularity_sqla: 'ds'}); + expect(queryContext.datasource.id).toBe(5); + expect(queryContext.datasource.type).toBe('table'); + }); + + it('should build datasource for druid sources', () => { + const queryContext = build({ datasource: '5__druid', granularity: 'ds'}); + expect(queryContext.datasource.id).toBe(5); + expect(queryContext.datasource.type).toBe('druid'); + }); + + it('should call queryObjectBuilder to build queries', () => { + const buildQueryObjectSpy = jest.spyOn(queryObjectBuilder, 'default'); + build({ datasource: '5__table', granularity_sqla: 'ds'}); + expect(buildQueryObjectSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/superset/assets/spec/javascripts/superset-ui/buildQueryObject.test.ts b/superset/assets/spec/javascripts/superset-ui/buildQueryObject.test.ts new file mode 100644 index 00000000000..ec111dfdada --- /dev/null +++ b/superset/assets/spec/javascripts/superset-ui/buildQueryObject.test.ts @@ -0,0 +1,13 @@ +import build from 'src/query/buildQueryObject'; + +describe('queryObjectBuilder', () => { + it('should build granularity for sql alchemy datasources', () => { + const query = build({datasource: '5__table', granularity_sqla: 'ds'}); + expect(query.granularity).toEqual('ds'); + }); + + it('should build granularity for sql alchemy datasources', () => { + const query = build({datasource: '5__druid', granularity: 'ds'}); + expect(query.granularity).toEqual('ds'); + }); +}); diff --git a/superset/assets/src/query/DatasourceKey.ts b/superset/assets/src/query/DatasourceKey.ts new file mode 100644 index 00000000000..a13c1216639 --- /dev/null +++ b/superset/assets/src/query/DatasourceKey.ts @@ -0,0 +1,29 @@ +enum DatasourceType { + Table = 'table', + Druid = 'druid', +} + +export default interface DatasourceKey { + id: number; + type: DatasourceType; +} + +// Declaration merging with the interface above. No need to redeclare id and type. +export default class DatasourceKey { + constructor(key: string) { + const [ idStr, typeStr ] = key.split('__'); + this.id = parseInt(idStr, 10); + this.type = typeStr === 'table' ? DatasourceType.Table : DatasourceType.Druid; + } + + public toString() { + return `${this.id}__${this.type}`; + } + + public toObject() { + return { + id: this.id, + type: this.type, + }; + } +} diff --git a/superset/assets/src/query/FormData.ts b/superset/assets/src/query/FormData.ts new file mode 100644 index 00000000000..daa7abf1613 --- /dev/null +++ b/superset/assets/src/query/FormData.ts @@ -0,0 +1,21 @@ +// Type signature and utility functions for formData shared by all viz types +// It will be gradually filled out as we build out the query object +interface BaseFormData { + datasource: string; +} + +// FormData is either sqla-based or druid-based +interface SqlaFormData extends BaseFormData { + granularity_sqla: string; +} + +interface DruidFormData extends BaseFormData { + granularity: string; +} + +type FormData = SqlaFormData | DruidFormData; +export default FormData; + +export function getGranularity(formData: FormData): string { + return 'granularity_sqla' in formData ? formData.granularity_sqla : formData.granularity; +} diff --git a/superset/assets/src/query/buildQueryContext.ts b/superset/assets/src/query/buildQueryContext.ts new file mode 100644 index 00000000000..8ce0464c63a --- /dev/null +++ b/superset/assets/src/query/buildQueryContext.ts @@ -0,0 +1,15 @@ +import buildQueryObject, { QueryObject } from './buildQueryObject'; +import DatasourceKey from './DatasourceKey'; +import FormData from './FormData'; + +const WRAP_IN_ARRAY = (baseQueryObject: QueryObject) => [baseQueryObject]; + +// Note: let TypeScript infer the return type +export default function buildQueryContext( + formData: FormData, + buildQuery: (baseQueryObject: QueryObject) => QueryObject[] = WRAP_IN_ARRAY) { + return { + datasource: new DatasourceKey(formData.datasource).toObject(), + queries: buildQuery(buildQueryObject(formData)), + }; +} diff --git a/superset/assets/src/query/buildQueryObject.ts b/superset/assets/src/query/buildQueryObject.ts new file mode 100644 index 00000000000..578d9ae5316 --- /dev/null +++ b/superset/assets/src/query/buildQueryObject.ts @@ -0,0 +1,18 @@ +import FormData, { getGranularity } from './FormData'; + +// TODO: fill out the rest of the query object +export interface QueryObject { + granularity: string; + groupby?: string[]; +} + +// Build the common segments of all query objects (e.g. the granularity field derived from +// either sql alchemy or druid). The segments specific to each viz type is constructed in the +// buildQuery method for each viz type (see `wordcloud/buildQuery.ts` for an example). +// Note the type of the formData argument passed in here is the type of the formData for a +// specific viz, which is a subtype of the generic formData shared among all viz types. +export default function buildQueryObject(formData: T): QueryObject { + return { + granularity: getGranularity(formData), + }; +} diff --git a/superset/assets/src/query/index.ts b/superset/assets/src/query/index.ts new file mode 100644 index 00000000000..cda71267a5f --- /dev/null +++ b/superset/assets/src/query/index.ts @@ -0,0 +1,3 @@ +// Public API of the query module +export { default } from './buildQueryContext'; +export { default as FormData } from './FormData'; diff --git a/superset/assets/src/visualizations/wordcloud/FormData.ts b/superset/assets/src/visualizations/wordcloud/FormData.ts new file mode 100644 index 00000000000..55f81313af0 --- /dev/null +++ b/superset/assets/src/visualizations/wordcloud/FormData.ts @@ -0,0 +1,11 @@ +import { FormData as GenericFormData } from 'src/query'; + +// FormData specific to the wordcloud viz +interface WordCloudFormData { + series: string; +} + +// FormData for wordcloud contains both common properties of all form data +// and properties specific to wordcloud vizzes +type FormData = GenericFormData & WordCloudFormData; +export default FormData; diff --git a/superset/assets/src/visualizations/wordcloud/WordCloudChartPlugin.js b/superset/assets/src/visualizations/wordcloud/WordCloudChartPlugin.js index b82882080f1..19b43c59663 100644 --- a/superset/assets/src/visualizations/wordcloud/WordCloudChartPlugin.js +++ b/superset/assets/src/visualizations/wordcloud/WordCloudChartPlugin.js @@ -1,5 +1,6 @@ import { t } from '@superset-ui/translation'; import { ChartMetadata, ChartPlugin } from '@superset-ui/chart'; +import buildQuery from './buildQuery'; import transformProps from './transformProps'; import thumbnail from './images/thumbnail.png'; @@ -14,6 +15,7 @@ export default class WordCloudChartPlugin extends ChartPlugin { constructor() { super({ metadata, + buildQuery, transformProps, loadChart: () => import('./ReactWordCloud.js'), }); diff --git a/superset/assets/src/visualizations/wordcloud/buildQuery.ts b/superset/assets/src/visualizations/wordcloud/buildQuery.ts new file mode 100644 index 00000000000..2aa0f2cd75b --- /dev/null +++ b/superset/assets/src/visualizations/wordcloud/buildQuery.ts @@ -0,0 +1,10 @@ +import buildQueryContext from 'src/query'; +import FormData from './FormData'; + +export default function buildQuery(formData: FormData) { + // Set the single QueryObject's groupby field with series in formData + return buildQueryContext(formData, (baseQueryObject) => [{ + ...baseQueryObject, + groupby: [formData.series], + }]); +}