Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-gateway): graphql json query conversion #8189

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cubejs-api-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"express-graphql": "^0.12.0",
"graphql": "^15.8.0",
"graphql-scalars": "^1.10.0",
"graphql-tag": "^2.12.6",
"inflection": "^1.12.0",
"joi": "^17.8.3",
"jsonwebtoken": "^8.3.0",
Expand Down
21 changes: 20 additions & 1 deletion packages/cubejs-api-gateway/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
import { cachedHandler } from './cached-handler';
import { createJWKsFetcher } from './jwk';
import { SQLServer } from './sql-server';
import { makeSchema } from './graphql';
import { getJsonQueryFromGraphQLQuery, makeSchema } from './graphql';
import { ConfigItem, prepareAnnotation } from './helpers/prepareAnnotation';
import transformData from './helpers/transformData';
import {
Expand Down Expand Up @@ -147,7 +147,7 @@

protected readonly playgroundAuthSecret?: string;

protected readonly event: (name: string, props?: object) => void;

Check warning on line 150 in packages/cubejs-api-gateway/src/gateway.ts

View workflow job for this annotation

GitHub Actions / lint

'name' is defined but never used. Allowed unused args must match /^_.*/u

Check warning on line 150 in packages/cubejs-api-gateway/src/gateway.ts

View workflow job for this annotation

GitHub Actions / lint

'props' is defined but never used. Allowed unused args must match /^_.*/u

public constructor(
protected readonly apiSecret: string,
Expand Down Expand Up @@ -180,7 +180,7 @@
this.contextRejectionMiddleware = options.contextRejectionMiddleware || (async (req, res, next) => next());
this.wsContextAcceptor = options.wsContextAcceptor || (() => ({ accepted: true }));
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.event = options.event || function () {};

Check warning on line 183 in packages/cubejs-api-gateway/src/gateway.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected unnamed function
}

public initApp(app: ExpressApplication) {
Expand All @@ -207,9 +207,28 @@
* graphql scope *
*************************************************************** */

app.post(`${this.basePath}/graphql-to-json`, userMiddlewares, async (req: any, res) => {
Dismissed Show dismissed Hide dismissed
const { query, variables } = req.body;

Check warning on line 211 in packages/cubejs-api-gateway/src/gateway.ts

View workflow job for this annotation

GitHub Actions / lint

'variables' is assigned a value but never used. Allowed unused vars must match /^_.*/u
const compilerApi = await this.getCompilerApi(req.context);

const metaConfig = await compilerApi.metaConfig({
requestId: req.context.requestId,
});

let schema = compilerApi.getGraphQLSchema();
if (!schema) {
schema = makeSchema(metaConfig);
compilerApi.setGraphQLSchema(schema);
}

const jsonQuery = getJsonQueryFromGraphQLQuery(query, metaConfig);

res.json({ jsonQuery });
});

app.use(
`${this.basePath}/graphql`,
userMiddlewares,

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
userAsyncHandler(async (req, res) => {
await this.assertApiScope(
'graphql',
Expand Down Expand Up @@ -1689,7 +1708,7 @@
const {
context,
res,
apiType = 'sql',

Check warning on line 1711 in packages/cubejs-api-gateway/src/gateway.ts

View workflow job for this annotation

GitHub Actions / lint

'apiType' is assigned a value but never used. Allowed unused vars must match /^_.*/u
} = request;
const requestStarted = new Date();

Expand Down
238 changes: 139 additions & 99 deletions packages/cubejs-api-gateway/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DateTimeResolver,
} from 'graphql-scalars';

import gql from 'graphql-tag';
import { QueryType, MemberType } from './types/enums';

const DateTimeScalar = asNexusMethod(DateTimeResolver, 'date');
Expand Down Expand Up @@ -355,6 +356,142 @@ function parseDates(result: any) {
});
}

export function getJsonQuery(metaConfig: any, args: Record<string, any>, infos: GraphQLResolveInfo) {
const { where, limit, offset, timezone, orderBy, renewQuery, ungrouped } = args;

const measures: string[] = [];
const dimensions: string[] = [];
const timeDimensions: any[] = [];
let filters: any[] = [];
const order: [string, 'asc' | 'desc'][] = [];

if (where) {
filters = whereArgToQueryFilters(where, undefined, metaConfig);
}

if (orderBy) {
Object.entries<any>(orderBy).forEach(([cubeName, members]) => {
Object.entries<any>(members).forEach(([member, value]) => {
order.push([`${capitalize(cubeName)}.${member}`, value]);
});
});
}

getFieldNodeChildren(infos.fieldNodes[0], infos).forEach(cubeNode => {
const cubeExists = metaConfig.find((cube) => cube.config.name === cubeNode.name.value);

const cubeName = cubeExists ? (cubeNode.name.value) : capitalize(cubeNode.name.value);
const orderByArg = getArgumentValue(cubeNode, 'orderBy', infos.variableValues);
// todo: throw if both RootOrderByInput and [Cube]OrderByInput provided
if (orderByArg) {
Object.keys(orderByArg).forEach(key => {
order.push([`${cubeName}.${key}`, orderByArg[key]]);
});
}

const whereArg = getArgumentValue(cubeNode, 'where', infos.variableValues);
if (whereArg) {
filters = whereArgToQueryFilters(whereArg, cubeName).concat(filters);
}

// Push down all inDateRange filters to time dimensions to leverage pre-aggregations
const dateRangeFilters = {};
filters = filters.filter((f) => {
if (f.operator === 'inDateRange' && !dateRangeFilters[f.member]) {
dateRangeFilters[f.member] = f.values;
return false;
}

return true;
});

getFieldNodeChildren(cubeNode, infos).forEach(memberNode => {
const memberName = memberNode.name.value;
const memberType = getMemberType(metaConfig, cubeName, memberName);
const key = `${cubeName}.${memberName}`;

if (memberType === MemberType.MEASURES) {
measures.push(key);
} else if (memberType === MemberType.DIMENSIONS) {
const granularityNodes = getFieldNodeChildren(memberNode, infos);
if (granularityNodes.length > 0) {
granularityNodes.forEach(granularityNode => {
const granularityName = granularityNode.name.value;
if (granularityName === 'value') {
dimensions.push(key);
} else {
timeDimensions.push({
dimension: key,
granularity: granularityName,
...(dateRangeFilters[key] ? {
dateRange: dateRangeFilters[key],
} : null)
});
}
});
} else {
dimensions.push(`${cubeName}.${memberName}`);
}
}
});

if (Object.keys(dateRangeFilters).length && !timeDimensions.length) {
Object.entries(dateRangeFilters).forEach(([dimension, dateRange]) => {
timeDimensions.push({
dimension,
dateRange
});
});
}
});

return {
...(measures.length && { measures }),
...(dimensions.length && { dimensions }),
...(timeDimensions.length && { timeDimensions }),
...(Object.keys(order).length && { order }),
...(limit && { limit }),
...(offset && { offset }),
...(timezone && { timezone }),
...(filters.length && { filters }),
...(renewQuery && { renewQuery }),
...(ungrouped && { ungrouped }),
};
}

export function getJsonQueryFromGraphQLQuery(query: string, metaConfig: any) {
const ast = gql(query);

const operation: any = ast.definitions.find(
({ kind }) => kind === 'OperationDefinition'
);

const fieldNodes = operation?.selectionSet.selections;

let args = {};
for (const argument of fieldNodes[0].arguments) {
args = { ...args, [argument.name.value]: parseArgumentValue(argument.value) };
}

const resolveInfo: any = {
fieldName: fieldNodes[0]?.name.value || '',
fieldNodes,
// returnType: null,
// parentType: null,
// schema: null,
rootValue: {},
operation,
variableValues: {},
// path: {
// prev: undefined,
// key: ''
// },
fragments: {},
};

return getJsonQuery(metaConfig, args, resolveInfo);
}

export function makeSchema(metaConfig: any): GraphQLSchema {
const types: any[] = [
DateTimeScalar,
Expand Down Expand Up @@ -508,105 +645,8 @@ export function makeSchema(metaConfig: any): GraphQLSchema {
type: 'RootOrderByInput'
}),
},
resolve: async (_, { where, limit, offset, timezone, orderBy, renewQuery, ungrouped }, { req, apiGateway }, infos) => {
const measures: string[] = [];
const dimensions: string[] = [];
const timeDimensions: any[] = [];
let filters: any[] = [];
const order: [string, 'asc' | 'desc'][] = [];

if (where) {
filters = whereArgToQueryFilters(where, undefined, metaConfig);
}

if (orderBy) {
Object.entries<any>(orderBy).forEach(([cubeName, members]) => {
Object.entries<any>(members).forEach(([member, value]) => {
order.push([`${capitalize(cubeName)}.${member}`, value]);
});
});
}

getFieldNodeChildren(infos.fieldNodes[0], infos).forEach(cubeNode => {
const cubeExists = metaConfig.find((cube) => cube.config.name === cubeNode.name.value);

const cubeName = cubeExists ? (cubeNode.name.value) : capitalize(cubeNode.name.value);
const orderByArg = getArgumentValue(cubeNode, 'orderBy', infos.variableValues);
// todo: throw if both RootOrderByInput and [Cube]OrderByInput provided
if (orderByArg) {
Object.keys(orderByArg).forEach(key => {
order.push([`${cubeName}.${key}`, orderByArg[key]]);
});
}

const whereArg = getArgumentValue(cubeNode, 'where', infos.variableValues);
if (whereArg) {
filters = whereArgToQueryFilters(whereArg, cubeName).concat(filters);
}

// Push down all inDateRange filters to time dimensions to leverage pre-aggregations
const dateRangeFilters = {};
filters = filters.filter((f) => {
if (f.operator === 'inDateRange' && !dateRangeFilters[f.member]) {
dateRangeFilters[f.member] = f.values;
return false;
}

return true;
});

getFieldNodeChildren(cubeNode, infos).forEach(memberNode => {
const memberName = memberNode.name.value;
const memberType = getMemberType(metaConfig, cubeName, memberName);
const key = `${cubeName}.${memberName}`;

if (memberType === MemberType.MEASURES) {
measures.push(key);
} else if (memberType === MemberType.DIMENSIONS) {
const granularityNodes = getFieldNodeChildren(memberNode, infos);
if (granularityNodes.length > 0) {
granularityNodes.forEach(granularityNode => {
const granularityName = granularityNode.name.value;
if (granularityName === 'value') {
dimensions.push(key);
} else {
timeDimensions.push({
dimension: key,
granularity: granularityName,
...(dateRangeFilters[key] ? {
dateRange: dateRangeFilters[key],
} : null)
});
}
});
} else {
dimensions.push(`${cubeName}.${memberName}`);
}
}
});

if (Object.keys(dateRangeFilters).length && !timeDimensions.length) {
Object.entries(dateRangeFilters).forEach(([dimension, dateRange]) => {
timeDimensions.push({
dimension,
dateRange
});
});
}
});

const query = {
...(measures.length && { measures }),
...(dimensions.length && { dimensions }),
...(timeDimensions.length && { timeDimensions }),
...(Object.keys(order).length && { order }),
...(limit && { limit }),
...(offset && { offset }),
...(timezone && { timezone }),
...(filters.length && { filters }),
...(renewQuery && { renewQuery }),
...(ungrouped && { ungrouped }),
};
resolve: async (_, args, { req, apiGateway }, info) => {
const query = getJsonQuery(metaConfig, args, info);

const results = await new Promise<any>((resolve, reject) => {
apiGateway.load({
Expand Down
28 changes: 25 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8692,11 +8692,26 @@
dependencies:
"@types/node" "*"

"@types/node@*", "@types/node@12.12.50", "@types/node@^12", "@types/node@^14", "@types/node@^16":
"@types/node@*", "@types/node@^16":
version "16.18.68"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.68.tgz#3155f64a961b3d8d10246c80657f9a7292e3421a"
integrity sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==

"@types/node@12.12.50":
version "12.12.50"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.50.tgz#e9b2e85fafc15f2a8aa8fdd41091b983da5fd6ee"
integrity sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==

"@types/node@^12":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/node@^14":
version "14.18.63"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b"
integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==

"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
Expand Down Expand Up @@ -8739,7 +8754,7 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==

"@types/ramda@0.27.40", "@types/ramda@^0.27.32", "@types/ramda@^0.27.34", "@types/ramda@^0.27.40":
"@types/ramda@^0.27.32", "@types/ramda@^0.27.34", "@types/ramda@^0.27.40":
version "0.27.40"
resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.27.40.tgz#99f307356fe553095ee4d3c2af2b0eb3af7a8413"
integrity sha512-V99ZfTH2tqVYdLDAlgh2uT+N074HPgqnAsMjALKSBqogYd0HbuuGMqNukJ6fk9Ml/Htaus76fsc4Yh3p7q1VdQ==
Expand Down Expand Up @@ -16682,6 +16697,13 @@ graphql-scalars@^1.10.0:
dependencies:
tslib "~2.3.0"

graphql-tag@^2.12.6:
version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==
dependencies:
tslib "^2.1.0"

graphql-ws@^5.7.0:
version "5.7.0"
resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.7.0.tgz#4b9d7a0ee9555804582f27f5d7695d10aafdbdc8"
Expand Down Expand Up @@ -27559,7 +27581,7 @@ tslib@^2, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1,
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==

tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.2:
tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
Expand Down