Skip to content

Commit

Permalink
feat(schema-compiler): expose custom granularities details via meta A…
Browse files Browse the repository at this point in the history
…PI and query annotation (#8740)

* extend response for /meta with custom granularity details

* update OpenAPI spec with custom granularities details

* update annotation for query with custom granularity (if it was used) with granularity details

* add tests for /meta response

* add tests for custom granularity in query annotation

* Update types and structure in prepareAnnotation

* adopt tests with changes
  • Loading branch information
KSDaemon authored Sep 26, 2024
1 parent a292c3f commit c58e97a
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 15 deletions.
7 changes: 7 additions & 0 deletions packages/cubejs-api-gateway/openspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,18 @@ components:
required:
- name
- title
- interval
properties:
name:
type: "string"
title:
type: "string"
interval:
type: "string"
offset:
type: "string"
origin:
type: "string"
V1CubeMetaDimension:
type: "object"
required:
Expand Down
72 changes: 58 additions & 14 deletions packages/cubejs-api-gateway/src/helpers/prepareAnnotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@
*/

import R from 'ramda';
import { isPredefinedGranularity } from '@cubejs-backend/shared';
import { MetaConfig, MetaConfigMap, toConfigMap } from './toConfigMap';
import { MemberType } from '../types/strings';
import { MemberType as MemberTypeEnum } from '../types/enums';
import { MemberExpression } from '../types/query';

type GranularityMeta = {
name: string;
title: string;
interval: string;
offset?: string;
origin?: string;
};

/**
* Annotation item for cube's member.
*/
Expand All @@ -23,6 +32,11 @@ type ConfigItem = {
meta: any;
drillMembers?: any[];
drillMembersGrouped?: any;
granularities?: GranularityMeta[];
};

type AnnotatedConfigItem = Omit<ConfigItem, 'granularities'> & {
granularity?: GranularityMeta;
};

/**
Expand Down Expand Up @@ -50,7 +64,10 @@ const annotation = (
...(memberType === MemberTypeEnum.MEASURES ? {
drillMembers: config.drillMembers,
drillMembersGrouped: config.drillMembersGrouped
} : {})
} : {}),
...(memberType === MemberTypeEnum.DIMENSIONS && config.granularities ? {
granularities: config.granularities || [],
} : {}),
}];
};

Expand Down Expand Up @@ -81,25 +98,52 @@ function prepareAnnotation(metaConfig: MetaConfig[], query: any) {
(query.timeDimensions || [])
.filter(td => !!td.granularity)
.map(
td => [
annotation(
td => {
const an = annotation(
configMap,
MemberTypeEnum.DIMENSIONS,
)(
`${td.dimension}.${td.granularity}`
)
].concat(
);

let dimAnnotation: [string, AnnotatedConfigItem] | undefined;

if (an) {
let granularityMeta: GranularityMeta | undefined;
if (isPredefinedGranularity(td.granularity)) {
granularityMeta = {
name: td.granularity,
title: td.granularity,
interval: `1 ${td.granularity}`,
};
} else if (an[1].granularities) {
// No need to send all the granularities defined, only those make sense for this query
granularityMeta = an[1].granularities.find(g => g.name === td.granularity);
}

const { granularities: _, ...rest } = an[1];
dimAnnotation = [an[0], { ...rest, granularity: granularityMeta }];
}

// TODO: deprecated: backward compatibility for
// referencing time dimensions without granularity
dimensions.indexOf(td.dimension) === -1
? [
annotation(
configMap,
MemberTypeEnum.DIMENSIONS
)(td.dimension)
]
: []
).filter(a => !!a)
if (dimensions.indexOf(td.dimension) !== -1) {
return [dimAnnotation].filter(a => !!a);
}

const dimWithoutGranularity = annotation(
configMap,
MemberTypeEnum.DIMENSIONS
)(td.dimension);

if (dimWithoutGranularity && dimWithoutGranularity[1].granularities) {
// no need to populate granularities here
dimWithoutGranularity[1].granularities = undefined;
}

return [dimAnnotation].concat([dimWithoutGranularity])
.filter(a => !!a);
}
)
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('prepareAnnotation helpers', () => {
type: undefined,
}
});

// query timeDimensions
expect(
prepareAnnotation([{
Expand Down Expand Up @@ -197,6 +197,11 @@ describe('prepareAnnotation helpers', () => {
shortTitle: undefined,
title: undefined,
type: undefined,
granularity: {
name: 'day',
title: 'day',
interval: '1 day',
}
},
});

Expand Down
22 changes: 22 additions & 0 deletions packages/cubejs-api-gateway/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,28 @@ describe('API Gateway', () => {
expect(res.body && res.body.data).toStrictEqual([{ 'Foo.bar': 42 }]);
});

test('custom granularities in annotation', async () => {
const { app } = await createApiGateway();

const res = await request(app)
.get(
'/cubejs-api/v1/load?query={"measures":["Foo.bar"],"timeDimensions":[{"dimension":"Foo.timeGranularities","granularity":"half_year_by_1st_april"}]}'
)
.set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M')
.expect(200);
console.log(res.body);
expect(res.body && res.body.data).toStrictEqual([{ 'Foo.bar': 42 }]);
expect(res.body.annotation.timeDimensions['Foo.timeGranularities.half_year_by_1st_april'])
.toStrictEqual({
granularity: {
name: 'half_year_by_1st_april',
title: 'Half Year By1 St April',
interval: '6 months',
offset: '3 months',
}
});
});

test('dry-run', async () => {
const { app } = await createApiGateway();

Expand Down
12 changes: 12 additions & 0 deletions packages/cubejs-api-gateway/test/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ export const compilerApi = jest.fn().mockImplementation(async () => ({
name: 'Foo.time',
isVisible: true,
},
{
name: 'Foo.timeGranularities',
isVisible: true,
granularities: [
{
name: 'half_year_by_1st_april',
title: 'Half Year By1 St April',
interval: '6 months',
offset: '3 months'
}
]
},
],
segments: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export class CubeToMetaTransformer {
? R.compose(R.map((g) => ({
name: g[0],
title: this.title(cubeTitle, g, true),
interval: g[1].interval,
offset: g[1].offset,
origin: g[1].origin,
})), R.toPairs)(nameToDimension[1].granularities)
: undefined,
})),
Expand Down
9 changes: 9 additions & 0 deletions packages/cubejs-schema-compiler/test/unit/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,20 @@ describe('Schema Testing', () => {
let gr = dg.granularities.find(g => g.name === 'half_year');
expect(gr).toBeDefined();
expect(gr.title).toBe('6 month intervals');
expect(gr.interval).toBe('6 months');

gr = dg.granularities.find(g => g.name === 'half_year_by_1st_april');
expect(gr).toBeDefined();
expect(gr.title).toBe('Half year from Apr to Oct');
expect(gr.interval).toBe('6 months');
expect(gr.offset).toBe('3 months');

// // Granularity defined without title -> titlize()
gr = dg.granularities.find(g => g.name === 'half_year_by_1st_june');
expect(gr).toBeDefined();
expect(gr.title).toBe('Half Year By1 St June');
expect(gr.interval).toBe('6 months');
expect(gr.origin).toBe('2020-06-01 10:00:00');
});

it('join types', async () => {
Expand Down

0 comments on commit c58e97a

Please sign in to comment.