From fa586b71fdf86cb9aba92327b0ad379a321f357d Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Sat, 29 Jun 2024 13:32:40 +0600 Subject: [PATCH 1/4] feat(@cubejs-backend/api-gateway): add meta field to gql res --- packages/cubejs-api-gateway/src/gateway.ts | 17 ++++++++++ packages/cubejs-api-gateway/src/graphql.ts | 34 +++++++++++++------ .../cubejs-api-gateway/src/types/request.ts | 13 ++++++- .../cubejs-api-gateway/test/graphql.test.ts | 4 +++ 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 6e05172686074..9fcc5f006fbad 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -40,6 +40,7 @@ import { PreAggJobStatusItem, PreAggJobStatusResponse, SqlApiRequest, MetaResponseResultFn, + GraphQLRequestContext, } from './types/request'; import { CheckAuthInternalOptions, @@ -259,6 +260,21 @@ class ApiGateway { } return graphqlHTTP({ schema, + extensions: (requestInfo) => { + const context = requestInfo.context as GraphQLRequestContext; + const { resultMeta } = context; + + if (!resultMeta) { + return { + resultMeta: undefined + }; + } + + return { + resultMeta, + }; + }, + context: { req, apiGateway: this @@ -1525,6 +1541,7 @@ class ApiGateway { return res; }) ); + // TODO: Add total for all queries response.total = normalizedQuery.total ? Number(total.data[0][QueryAlias.TOTAL_COUNT]) : undefined; diff --git a/packages/cubejs-api-gateway/src/graphql.ts b/packages/cubejs-api-gateway/src/graphql.ts index 24a0302d8a2f2..3793146df27a1 100644 --- a/packages/cubejs-api-gateway/src/graphql.ts +++ b/packages/cubejs-api-gateway/src/graphql.ts @@ -31,7 +31,9 @@ import { } from 'graphql-scalars'; import gql from 'graphql-tag'; +import type { Cube, LoadResponseResult } from '@cubejs-client/core'; import { QueryType, MemberType } from './types/enums'; +import { GraphQLRequestContext } from './types/request'; const DateTimeScalar = asNexusMethod(DateTimeResolver, 'date'); @@ -270,11 +272,11 @@ function whereArgToQueryFilters( metaConfig: any[] = [] ) { const queryFilters: any[] = []; - + Object.keys(whereArg).forEach((key) => { const cubeExists = metaConfig.find((cube) => cube.config.name === key); const normalizedKey = cubeExists ? key : capitalize(key); - + if (['OR', 'AND'].includes(key)) { queryFilters.push({ [key.toLowerCase()]: whereArg[key].reduce( @@ -363,7 +365,7 @@ function parseDates(result: any) { } export function getJsonQuery(metaConfig: any, args: Record, infos: GraphQLResolveInfo) { - const { where, limit, offset, timezone, orderBy, renewQuery, ungrouped } = args; + const { where, limit, offset, timezone, orderBy, renewQuery, ungrouped, total } = args; const measures: string[] = []; const dimensions: string[] = []; @@ -385,7 +387,7 @@ export function getJsonQuery(metaConfig: any, args: Record, infos: 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 @@ -458,6 +460,7 @@ export function getJsonQuery(metaConfig: any, args: Record, infos: ...(Object.keys(order).length && { order }), ...(limit && { limit }), ...(offset && { offset }), + ...(total && { total }), ...(timezone && { timezone }), ...(filters.length && { filters }), ...(renewQuery && { renewQuery }), @@ -471,7 +474,7 @@ export function getJsonQueryFromGraphQLQuery(query: string, metaConfig: any, var const operation: any = ast.definitions.find( ({ kind }) => kind === 'OperationDefinition' ); - + const fieldNodes = operation?.selectionSet.selections; let args = {}; @@ -487,11 +490,11 @@ export function getJsonQueryFromGraphQLQuery(query: string, metaConfig: any, var variableValues, fragments: {}, }; - + return getJsonQuery(metaConfig, args, resolveInfo); } -export function makeSchema(metaConfig: any): GraphQLSchema { +export function makeSchema(metaConfig: { config: Cube }[]): GraphQLSchema { const types: any[] = [ DateTimeScalar, FloatFilter, @@ -505,7 +508,7 @@ export function makeSchema(metaConfig: any): GraphQLSchema { if (cube.public === false) { return false; } - + return ([...cube.config.measures, ...cube.config.dimensions].filter((member) => member.isVisible)).length > 0; } @@ -639,23 +642,27 @@ export function makeSchema(metaConfig: any): GraphQLSchema { offset: intArg(), timezone: stringArg(), renewQuery: booleanArg(), + total: booleanArg(), + resultMeta: booleanArg(), ungrouped: booleanArg(), orderBy: arg({ type: 'RootOrderByInput' }), }, - resolve: async (_, args, { req, apiGateway }, info) => { + resolve: async (_, args, ctx: GraphQLRequestContext, info) => { + const { req, apiGateway } = ctx; const query = getJsonQuery(metaConfig, args, info); - const results = await new Promise((resolve, reject) => { + const results = await new Promise>((resolve, reject) => { apiGateway.load({ query, queryType: QueryType.REGULAR_QUERY, context: req.context, res: (message) => { - if (message.error) { + if ('error' in message && message.error) { reject(new Error(message.error)); } + // @ts-ignore resolve(message); }, apiType: 'graphql', @@ -663,6 +670,11 @@ export function makeSchema(metaConfig: any): GraphQLSchema { }); parseDates(results); + if (args.resultMeta) { + const { data, query: __, transformedQuery, annotation, ...resultMeta } = results; + ctx.resultMeta = ctx.resultMeta || {}; + ctx.resultMeta[info.path.key] = resultMeta; + } return results.data.map(entry => R.toPairs(entry) .reduce((res, pair) => { diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index 34bea2567c22b..ac979adefd8b9 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -6,8 +6,10 @@ */ import type { Request as ExpressRequest } from 'express'; +import type { ApiGateway } from 'src/gateway'; import { RequestType, ApiType, ResultType } from './strings'; import { Query } from './query'; +import type { QueryType } from './enums'; /** * Network request context interface. @@ -60,6 +62,14 @@ interface Request extends ExpressRequest { authInfo?: any, } +interface GraphQLRequestContext { + req: Request & { + context: ExtendedRequestContext; + }, + resultMeta?: object, + apiGateway: ApiGateway +} + /** * Function that should provides basic query conversion mechanic. * Used as a part of a main configuration object of the server-core @@ -123,7 +133,7 @@ type BaseRequest = { */ type QueryRequest = BaseRequest & { query: Record | Record[]; - queryType?: RequestType; + queryType?: RequestType | QueryType; apiType?: ApiType; resType?: ResultType memberToAlias?: Record; @@ -207,6 +217,7 @@ export { RequestContext, RequestExtension, ExtendedRequestContext, + GraphQLRequestContext, Request, SqlApiRequest, QueryRewriteFn, diff --git a/packages/cubejs-api-gateway/test/graphql.test.ts b/packages/cubejs-api-gateway/test/graphql.test.ts index 0aabe00458bc1..6e4b0c672c68e 100644 --- a/packages/cubejs-api-gateway/test/graphql.test.ts +++ b/packages/cubejs-api-gateway/test/graphql.test.ts @@ -150,6 +150,7 @@ describe('GraphQL Schema', () => { const app = express(); app.use('/graphql', jsonParser, (req, res) => { + // @ts-ignore const schema = makeSchema(metaConfig); return graphqlHTTP({ @@ -174,6 +175,7 @@ describe('GraphQL Schema', () => { const GRAPHQL_QUERIES_PATH = `${process.cwd()}/test/graphql-queries/base.gql`; app.use('/graphql', jsonParser, (req, res) => { + // @ts-ignore const schema = makeSchema(metaConfig); return graphqlHTTP({ @@ -196,6 +198,7 @@ describe('GraphQL Schema', () => { }); test('should make valid schema', () => { + // @ts-ignore const schema = makeSchema(metaConfig); expectValidSchema(schema); }); @@ -226,6 +229,7 @@ describe('GraphQL Schema', () => { const app = express(); app.use('/graphql', jsonParser, (req, res) => { + // @ts-ignore const schema = makeSchema(metaConfigSnakeCase); return graphqlHTTP({ From 7a271e81eaf6e2c5cd46ee4fd989ab62130bbff9 Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Wed, 3 Jul 2024 18:33:28 +0600 Subject: [PATCH 2/4] refactor: rename resultMeta to meta --- packages/cubejs-api-gateway/src/gateway.ts | 10 ++++------ packages/cubejs-api-gateway/src/graphql.ts | 16 +++++++++------- packages/cubejs-api-gateway/src/types/request.ts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 9fcc5f006fbad..a22571bda6c26 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -262,16 +262,14 @@ class ApiGateway { schema, extensions: (requestInfo) => { const context = requestInfo.context as GraphQLRequestContext; - const { resultMeta } = context; + const { meta } = context; - if (!resultMeta) { - return { - resultMeta: undefined - }; + if (!meta) { + return undefined; } return { - resultMeta, + meta, }; }, diff --git a/packages/cubejs-api-gateway/src/graphql.ts b/packages/cubejs-api-gateway/src/graphql.ts index 3793146df27a1..207ba549fe260 100644 --- a/packages/cubejs-api-gateway/src/graphql.ts +++ b/packages/cubejs-api-gateway/src/graphql.ts @@ -1,4 +1,4 @@ -import R from 'ramda'; +import R, { pick } from 'ramda'; import moment from 'moment-timezone'; import { @@ -643,7 +643,7 @@ export function makeSchema(metaConfig: { config: Cube }[]): GraphQLSchema { timezone: stringArg(), renewQuery: booleanArg(), total: booleanArg(), - resultMeta: booleanArg(), + meta: booleanArg(), ungrouped: booleanArg(), orderBy: arg({ type: 'RootOrderByInput' @@ -670,11 +670,13 @@ export function makeSchema(metaConfig: { config: Cube }[]): GraphQLSchema { }); parseDates(results); - if (args.resultMeta) { - const { data, query: __, transformedQuery, annotation, ...resultMeta } = results; - ctx.resultMeta = ctx.resultMeta || {}; - ctx.resultMeta[info.path.key] = resultMeta; - } + + if (args.meta) { + ctx.meta = { ...(ctx.meta || {}), + [info.path.key]: pick([ + ...(args.total ? ['total'] : []), + 'dbType', 'extDbType', 'external', 'lastRefreshTime', 'slowQuery'], results) }; + } else if (args.total) ctx.meta = { ...(ctx.meta || {}), [info.path.key]: pick(['total'], results) }; return results.data.map(entry => R.toPairs(entry) .reduce((res, pair) => { diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index ac979adefd8b9..da0ec98b64e36 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -66,7 +66,7 @@ interface GraphQLRequestContext { req: Request & { context: ExtendedRequestContext; }, - resultMeta?: object, + meta?: object, apiGateway: ApiGateway } From f99339df02825cab55a5080ba0a44700b5b78d92 Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Wed, 3 Jul 2024 18:46:48 +0600 Subject: [PATCH 3/4] docs: add docs for gql meta arg --- docs/pages/product/apis-integrations/queries.mdx | 2 +- docs/pages/reference/graphql-api.mdx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/pages/product/apis-integrations/queries.mdx b/docs/pages/product/apis-integrations/queries.mdx index b221ae19d6694..34f229afa35f6 100644 --- a/docs/pages/product/apis-integrations/queries.mdx +++ b/docs/pages/product/apis-integrations/queries.mdx @@ -325,7 +325,7 @@ the query. This is useful for creating user interfaces with [pagination][ref-pagination-recipe]. You can make a total query by using the `total` option with the [REST -API][ref-rest-api-query-format-options]. For the SQL API, you can write an +API][ref-rest-api-query-format-options] or [GraphQL API][ref-ref-graphql-api-args]. For the SQL API, you can write an equivalent query using the `UNION ALL` statement. ### Ungrouped query diff --git a/docs/pages/reference/graphql-api.mdx b/docs/pages/reference/graphql-api.mdx index b2bb09efa3772..386be11ee33a2 100644 --- a/docs/pages/reference/graphql-api.mdx +++ b/docs/pages/reference/graphql-api.mdx @@ -29,12 +29,14 @@ query { - **`where` ([`RootWhereInput`](#root-where-input)):** Represents a SQL `WHERE` clause. - **`limit` (`Int`):** A [row limit][ref-row-limit] for your query. - **`offset` (`Int`):** The number of initial rows to be skipped for your query. The default value is `0`. +- **`total` (`Boolean`):** If set to true, Cube will run a total query and return the total number of rows as if no row limit or offset are set in the query. The default value is false. - **`timezone` (`String`):** The [time zone][ref-time-zone] for your query. You can set the desired time zone in the [TZ Database Name](https://en.wikipedia.org/wiki/Tz_database) format, e.g., `America/Los_Angeles`. - **`renewQuery` (`Boolean`):** If `renewQuery` is set to `true`, Cube will renew all `refreshKey` for queries and query results in the foreground. The default value is `false`. - **`ungrouped` (`Boolean`):** If set to `true`, Cube will run an [ungrouped query][ref-ungrouped-query]. +- **`meta` (`Boolean`):** If set to `true`, Cube will return metadata about the query. [ref-recipe-pagination]: /guides/recipes/queries/pagination From 59d172ebe6aad77242ddd31ed996190aabda7071 Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Wed, 3 Jul 2024 19:31:11 +0600 Subject: [PATCH 4/4] refactor: use existing ramda global import --- packages/cubejs-api-gateway/src/graphql.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/src/graphql.ts b/packages/cubejs-api-gateway/src/graphql.ts index 207ba549fe260..012df32b596c7 100644 --- a/packages/cubejs-api-gateway/src/graphql.ts +++ b/packages/cubejs-api-gateway/src/graphql.ts @@ -1,4 +1,4 @@ -import R, { pick } from 'ramda'; +import R from 'ramda'; import moment from 'moment-timezone'; import { @@ -673,10 +673,10 @@ export function makeSchema(metaConfig: { config: Cube }[]): GraphQLSchema { if (args.meta) { ctx.meta = { ...(ctx.meta || {}), - [info.path.key]: pick([ + [info.path.key]: R.pick([ ...(args.total ? ['total'] : []), 'dbType', 'extDbType', 'external', 'lastRefreshTime', 'slowQuery'], results) }; - } else if (args.total) ctx.meta = { ...(ctx.meta || {}), [info.path.key]: pick(['total'], results) }; + } else if (args.total) ctx.meta = { ...(ctx.meta || {}), [info.path.key]: R.pick(['total'], results) }; return results.data.map(entry => R.toPairs(entry) .reduce((res, pair) => {