Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 69 additions & 7 deletions packages/cubejs-api-gateway/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ const OrderBy = enumType({
]
});

const FloatWithTitle = objectType({
name: 'FloatWithTitle',
definition(t) {
t.float('value');
t.string('title');
t.string('shortTitle');
},
});

const StringWithTitle = objectType({
name: 'StringWithTitle',
definition(t) {
t.string('value');
t.string('title');
t.string('shortTitle');
},
});

export const TimeDimension = objectType({
name: 'TimeDimension',
definition(t) {
Expand Down Expand Up @@ -123,6 +141,8 @@ export const TimeDimension = objectType({
t.nonNull.field('year', {
type: 'DateTime',
});
t.string('title');
t.string('shortTitle');
},
});

Expand All @@ -131,11 +151,11 @@ function mapType(type: string, isInputType?: boolean) {
case 'time':
return isInputType ? 'DateTime' : 'TimeDimension';
case 'string':
return 'String';
return isInputType ? 'String' : 'StringWithTitle';
case 'number':
return 'Float';
return isInputType ? 'Float' : 'FloatWithTitle';
default:
return 'String';
return isInputType ? 'String' : 'StringWithTitle';
}
}

Expand Down Expand Up @@ -499,6 +519,8 @@ export function makeSchema(metaConfig: any): GraphQLSchema {
StringFilter,
DateTimeFilter,
OrderBy,
FloatWithTitle,
StringWithTitle,
TimeDimension
];

Expand Down Expand Up @@ -676,13 +698,53 @@ export function makeSchema(metaConfig: any): GraphQLSchema {

return results.data.map(entry => R.toPairs(entry)
.reduce((res, pair) => {
let path = pair[0].split('.');
const memberKey = pair[0];
let path = memberKey.split('.');
path[0] = unCapitalize(path[0]);
if (results.annotation.dimensions[pair[0]]?.type === 'time') {

// Get annotation for this member
const annotation =
results.annotation.measures[memberKey] ||
results.annotation.dimensions[memberKey] ||
results.annotation.timeDimensions[memberKey];

let value = pair[1];

// For time dimensions, handle specially
if (results.annotation.dimensions[memberKey]?.type === 'time') {
path = [...path, 'value'];
}
return (results.annotation.timeDimensions[pair[0]] && path.length !== 3)
? res : R.set(R.lensPath(path), pair[1], res);

// Skip time dimensions without proper path length
if (results.annotation.timeDimensions[memberKey] && path.length !== 3) {
return res;
}

// Wrap scalar values (measures and non-time dimensions) with title information
const isTimeDimension = results.annotation.dimensions[memberKey]?.type === 'time';
const isMeasure = !!results.annotation.measures[memberKey];
const isScalarDimension = results.annotation.dimensions[memberKey] && !isTimeDimension;

if (annotation && (isMeasure || isScalarDimension)) {
value = {
value: pair[1],
title: annotation.title,
shortTitle: annotation.shortTitle
};
}

// For time dimensions, add title to the result object
if (annotation && results.annotation.dimensions[memberKey]?.type === 'time') {
const baseRes = R.set(R.lensPath(path), pair[1], res);
const titlePath = [...path.slice(0, -1), 'title'];
const shortTitlePath = [...path.slice(0, -1), 'shortTitle'];
return R.pipe(
R.set(R.lensPath(titlePath), annotation.title),
R.set(R.lensPath(shortTitlePath), annotation.shortTitle)
)(baseRes);
}

return R.set(R.lensPath(path), value, res);
}, {}));
}
});
Expand Down
117 changes: 117 additions & 0 deletions packages/cubejs-api-gateway/test/graphql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,121 @@ describe('GraphQL Schema', () => {
});
});
});

describe('with titles', () => {
const app = express();

const metaConfigWithTitles = [
{
config: {
name: 'Orders',
measures: [
{
name: 'Orders.count',
title: 'Orders Count',
shortTitle: 'Count',
type: 'number',
isVisible: true,
},
],
dimensions: [
{
name: 'Orders.status',
title: 'Orders Status',
shortTitle: 'Status',
type: 'string',
isVisible: true,
},
{
name: 'Orders.createdAt',
title: 'Orders Created At',
shortTitle: 'Created At',
type: 'time',
isVisible: true,
},
],
},
},
];

app.use('/graphql', jsonParser, (req, res) => {
const schema = makeSchema(metaConfigWithTitles);

return graphqlHTTP({
schema,
context: {
req,
apiGateway: {
async load({ query, res: response }) {
response({
query,
annotation: {
measures: {
'Orders.count': {
title: 'Orders Count',
shortTitle: 'Count',
type: 'number',
},
},
dimensions: {
'Orders.status': {
title: 'Orders Status',
shortTitle: 'Status',
type: 'string',
},
'Orders.createdAt': {
title: 'Orders Created At',
shortTitle: 'Created At',
type: 'time',
},
},
timeDimensions: {},
},
data: [
{
'Orders.count': 150,
'Orders.status': 'completed',
},
],
});
},
},
},
})(req, res);
});

test('should return titles with scalar values', async () => {
const query = `
query CubeQuery {
cube {
orders {
count
status
}
}
}
`;

const response = await request(app)
.post('/graphql')
.set('Content-Type', 'application/json')
.send(gqlQuery(query));

console.log('Response:', JSON.stringify(response.body, null, 2));

expect(response.body.errors).toBeUndefined();
expect(response.body.data).toBeDefined();
expect(response.body.data.cube).toBeDefined();
expect(response.body.data.cube[0].orders.count).toEqual({
value: 150,
title: 'Orders Count',
shortTitle: 'Count',
});
expect(response.body.data.cube[0].orders.status).toEqual({
value: 'completed',
title: 'Orders Status',
shortTitle: 'Status',
});
});
});
});
Loading