Skip to content

Commit c58e97a

Browse files
authored
feat(schema-compiler): expose custom granularities details via meta API 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
1 parent a292c3f commit c58e97a

File tree

7 files changed

+117
-15
lines changed

7 files changed

+117
-15
lines changed

packages/cubejs-api-gateway/openspec.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,18 @@ components:
103103
required:
104104
- name
105105
- title
106+
- interval
106107
properties:
107108
name:
108109
type: "string"
109110
title:
110111
type: "string"
112+
interval:
113+
type: "string"
114+
offset:
115+
type: "string"
116+
origin:
117+
type: "string"
111118
V1CubeMetaDimension:
112119
type: "object"
113120
required:

packages/cubejs-api-gateway/src/helpers/prepareAnnotation.ts

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
*/
77

88
import R from 'ramda';
9+
import { isPredefinedGranularity } from '@cubejs-backend/shared';
910
import { MetaConfig, MetaConfigMap, toConfigMap } from './toConfigMap';
1011
import { MemberType } from '../types/strings';
1112
import { MemberType as MemberTypeEnum } from '../types/enums';
1213
import { MemberExpression } from '../types/query';
1314

15+
type GranularityMeta = {
16+
name: string;
17+
title: string;
18+
interval: string;
19+
offset?: string;
20+
origin?: string;
21+
};
22+
1423
/**
1524
* Annotation item for cube's member.
1625
*/
@@ -23,6 +32,11 @@ type ConfigItem = {
2332
meta: any;
2433
drillMembers?: any[];
2534
drillMembersGrouped?: any;
35+
granularities?: GranularityMeta[];
36+
};
37+
38+
type AnnotatedConfigItem = Omit<ConfigItem, 'granularities'> & {
39+
granularity?: GranularityMeta;
2640
};
2741

2842
/**
@@ -50,7 +64,10 @@ const annotation = (
5064
...(memberType === MemberTypeEnum.MEASURES ? {
5165
drillMembers: config.drillMembers,
5266
drillMembersGrouped: config.drillMembersGrouped
53-
} : {})
67+
} : {}),
68+
...(memberType === MemberTypeEnum.DIMENSIONS && config.granularities ? {
69+
granularities: config.granularities || [],
70+
} : {}),
5471
}];
5572
};
5673

@@ -81,25 +98,52 @@ function prepareAnnotation(metaConfig: MetaConfig[], query: any) {
8198
(query.timeDimensions || [])
8299
.filter(td => !!td.granularity)
83100
.map(
84-
td => [
85-
annotation(
101+
td => {
102+
const an = annotation(
86103
configMap,
87104
MemberTypeEnum.DIMENSIONS,
88105
)(
89106
`${td.dimension}.${td.granularity}`
90-
)
91-
].concat(
107+
);
108+
109+
let dimAnnotation: [string, AnnotatedConfigItem] | undefined;
110+
111+
if (an) {
112+
let granularityMeta: GranularityMeta | undefined;
113+
if (isPredefinedGranularity(td.granularity)) {
114+
granularityMeta = {
115+
name: td.granularity,
116+
title: td.granularity,
117+
interval: `1 ${td.granularity}`,
118+
};
119+
} else if (an[1].granularities) {
120+
// No need to send all the granularities defined, only those make sense for this query
121+
granularityMeta = an[1].granularities.find(g => g.name === td.granularity);
122+
}
123+
124+
const { granularities: _, ...rest } = an[1];
125+
dimAnnotation = [an[0], { ...rest, granularity: granularityMeta }];
126+
}
127+
92128
// TODO: deprecated: backward compatibility for
93129
// referencing time dimensions without granularity
94-
dimensions.indexOf(td.dimension) === -1
95-
? [
96-
annotation(
97-
configMap,
98-
MemberTypeEnum.DIMENSIONS
99-
)(td.dimension)
100-
]
101-
: []
102-
).filter(a => !!a)
130+
if (dimensions.indexOf(td.dimension) !== -1) {
131+
return [dimAnnotation].filter(a => !!a);
132+
}
133+
134+
const dimWithoutGranularity = annotation(
135+
configMap,
136+
MemberTypeEnum.DIMENSIONS
137+
)(td.dimension);
138+
139+
if (dimWithoutGranularity && dimWithoutGranularity[1].granularities) {
140+
// no need to populate granularities here
141+
dimWithoutGranularity[1].granularities = undefined;
142+
}
143+
144+
return [dimAnnotation].concat([dimWithoutGranularity])
145+
.filter(a => !!a);
146+
}
103147
)
104148
)
105149
),

packages/cubejs-api-gateway/test/helpers/prepareAnnotation.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ describe('prepareAnnotation helpers', () => {
164164
type: undefined,
165165
}
166166
});
167-
167+
168168
// query timeDimensions
169169
expect(
170170
prepareAnnotation([{
@@ -197,6 +197,11 @@ describe('prepareAnnotation helpers', () => {
197197
shortTitle: undefined,
198198
title: undefined,
199199
type: undefined,
200+
granularity: {
201+
name: 'day',
202+
title: 'day',
203+
interval: '1 day',
204+
}
200205
},
201206
});
202207

packages/cubejs-api-gateway/test/index.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,28 @@ describe('API Gateway', () => {
316316
expect(res.body && res.body.data).toStrictEqual([{ 'Foo.bar': 42 }]);
317317
});
318318

319+
test('custom granularities in annotation', async () => {
320+
const { app } = await createApiGateway();
321+
322+
const res = await request(app)
323+
.get(
324+
'/cubejs-api/v1/load?query={"measures":["Foo.bar"],"timeDimensions":[{"dimension":"Foo.timeGranularities","granularity":"half_year_by_1st_april"}]}'
325+
)
326+
.set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M')
327+
.expect(200);
328+
console.log(res.body);
329+
expect(res.body && res.body.data).toStrictEqual([{ 'Foo.bar': 42 }]);
330+
expect(res.body.annotation.timeDimensions['Foo.timeGranularities.half_year_by_1st_april'])
331+
.toStrictEqual({
332+
granularity: {
333+
name: 'half_year_by_1st_april',
334+
title: 'Half Year By1 St April',
335+
interval: '6 months',
336+
offset: '3 months',
337+
}
338+
});
339+
});
340+
319341
test('dry-run', async () => {
320342
const { app } = await createApiGateway();
321343

packages/cubejs-api-gateway/test/mocks.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ export const compilerApi = jest.fn().mockImplementation(async () => ({
9898
name: 'Foo.time',
9999
isVisible: true,
100100
},
101+
{
102+
name: 'Foo.timeGranularities',
103+
isVisible: true,
104+
granularities: [
105+
{
106+
name: 'half_year_by_1st_april',
107+
title: 'Half Year By1 St April',
108+
interval: '6 months',
109+
offset: '3 months'
110+
}
111+
]
112+
},
101113
],
102114
segments: [
103115
{

packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export class CubeToMetaTransformer {
8585
? R.compose(R.map((g) => ({
8686
name: g[0],
8787
title: this.title(cubeTitle, g, true),
88+
interval: g[1].interval,
89+
offset: g[1].offset,
90+
origin: g[1].origin,
8891
})), R.toPairs)(nameToDimension[1].granularities)
8992
: undefined,
9093
})),

packages/cubejs-schema-compiler/test/unit/schema.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,20 @@ describe('Schema Testing', () => {
314314
let gr = dg.granularities.find(g => g.name === 'half_year');
315315
expect(gr).toBeDefined();
316316
expect(gr.title).toBe('6 month intervals');
317+
expect(gr.interval).toBe('6 months');
318+
319+
gr = dg.granularities.find(g => g.name === 'half_year_by_1st_april');
320+
expect(gr).toBeDefined();
321+
expect(gr.title).toBe('Half year from Apr to Oct');
322+
expect(gr.interval).toBe('6 months');
323+
expect(gr.offset).toBe('3 months');
317324

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

324333
it('join types', async () => {

0 commit comments

Comments
 (0)