From 6c3c928c0844b96e207a2afb78840eafbe1df199 Mon Sep 17 00:00:00 2001 From: Scott Twiname Date: Thu, 11 Aug 2022 11:27:42 +1200 Subject: [PATCH] Fork OrderByAggregatesPlugin and modify to be compatible with historical feature (#1242) --- packages/query/src/graphql/graphql.module.ts | 7 +- .../graphql/plugins/PgAggregationPlugin.ts | 3 +- .../plugins/PgOrderByAggregatesPlugin.ts | 167 ++++++++++++++++++ 3 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 packages/query/src/graphql/plugins/PgOrderByAggregatesPlugin.ts diff --git a/packages/query/src/graphql/graphql.module.ts b/packages/query/src/graphql/graphql.module.ts index b1939a6aed..5cbeb2cacc 100644 --- a/packages/query/src/graphql/graphql.module.ts +++ b/packages/query/src/graphql/graphql.module.ts @@ -18,7 +18,7 @@ import {makePluginHook} from 'postgraphile'; import {getPostGraphileBuilder, PostGraphileCoreOptions} from 'postgraphile-core'; import {SubscriptionServer} from 'subscriptions-transport-ws'; import {Config} from '../configure'; -import {PinoConfig} from '../utils/logger'; +import {getLogger, PinoConfig} from '../utils/logger'; import {getYargsOption} from '../yargs'; import {plugins} from './plugins'; import {PgSubscriptionPlugin} from './plugins/PgSubscriptionPlugin'; @@ -26,6 +26,8 @@ import {queryComplexityPlugin} from './plugins/QueryComplexityPlugin'; import {ProjectService} from './project.service'; const {argv} = getYargsOption(); +const logger = getLogger('graphql-module'); + const SCHEMA_RETRY_INTERVAL = 10; //seconds const SCHEMA_RETRY_NUMBER = 5; @Module({ @@ -68,6 +70,9 @@ export class GraphqlModule implements OnModuleInit, OnModuleDestroy { return graphqlSchema; } catch (e) { await delay(SCHEMA_RETRY_INTERVAL); + if (retries === 1) { + logger.error(e); + } return this.buildSchema(dbSchema, options, --retries); } } else { diff --git a/packages/query/src/graphql/plugins/PgAggregationPlugin.ts b/packages/query/src/graphql/plugins/PgAggregationPlugin.ts index cd637752a9..0558b5f60d 100644 --- a/packages/query/src/graphql/plugins/PgAggregationPlugin.ts +++ b/packages/query/src/graphql/plugins/PgAggregationPlugin.ts @@ -11,12 +11,11 @@ import AggregateSpecsPlugin from '@graphile/pg-aggregates/dist/AggregateSpecsPlu import FilterRelationalAggregatesPlugin from '@graphile/pg-aggregates/dist/FilterRelationalAggregatesPlugin'; import InflectionPlugin from '@graphile/pg-aggregates/dist/InflectionPlugin'; import {AggregateSpec, AggregateGroupBySpec} from '@graphile/pg-aggregates/dist/interfaces'; -import OrderByAggregatesPlugin from '@graphile/pg-aggregates/dist/OrderByAggregatesPlugin'; import type {Plugin} from 'graphile-build'; import {makePluginByCombiningPlugins} from 'graphile-utils'; - import {argv} from '../../yargs'; +import OrderByAggregatesPlugin from './PgOrderByAggregatesPlugin'; const aggregate = argv('aggregate') as boolean; diff --git a/packages/query/src/graphql/plugins/PgOrderByAggregatesPlugin.ts b/packages/query/src/graphql/plugins/PgOrderByAggregatesPlugin.ts new file mode 100644 index 0000000000..60549aa458 --- /dev/null +++ b/packages/query/src/graphql/plugins/PgOrderByAggregatesPlugin.ts @@ -0,0 +1,167 @@ +// Copyright 2020-2022 OnFinality Limited authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/* WARNING + * This is a fork of https://github.com/graphile/pg-aggregates/blob/c8dd0f951663d5dacde21da26f3b94b62dc296c5/src/OrderByAggregatesPlugin.ts + * The only modification is to filter out `_id` and `_block_height` attributes to fix a naming conflict + */ + +import {AggregateSpec} from '@graphile/pg-aggregates/dist/interfaces'; +import type {Plugin} from 'graphile-build'; +import type {SQL, QueryBuilder, PgClass, PgEntity} from 'graphile-build-pg'; + +type OrderBySpecIdentity = string | SQL | ((options: {queryBuilder: QueryBuilder}) => SQL); + +type OrderSpec = [OrderBySpecIdentity, boolean] | [OrderBySpecIdentity, boolean, boolean]; +export interface OrderSpecs { + [orderByEnumValue: string]: { + value: { + alias?: string; + specs: Array; + unique: boolean; + }; + }; +} + +const OrderByAggregatesPlugin: Plugin = (builder) => { + builder.hook('GraphQLEnumType:values', (values, build, context) => { + const { + extend, + inflection, + pgIntrospectionResultsByKind: introspectionResultsByKind, + pgOmit: omit, + pgSql: sql, + } = build; + const pgAggregateSpecs: AggregateSpec[] = build.pgAggregateSpecs; + const { + scope: {isPgRowSortEnum}, + } = context; + + const pgIntrospection: PgEntity | undefined = context.scope.pgIntrospection; + + if (!isPgRowSortEnum || !pgIntrospection || pgIntrospection.kind !== 'class') { + return values; + } + + const foreignTable: PgClass = pgIntrospection; + + const foreignKeyConstraints = foreignTable.foreignConstraints.filter((con) => con.type === 'f'); + + const newValues = foreignKeyConstraints.reduce((memo, constraint) => { + if (omit(constraint, 'read')) { + return memo; + } + const table: PgClass | undefined = introspectionResultsByKind.classById[constraint.classId]; + if (!table) { + throw new Error(`Could not find the table that referenced us (constraint: ${constraint.name})`); + } + const keys = constraint.keyAttributes; + const foreignKeys = constraint.foreignKeyAttributes; + if (!keys.every((_) => _) || !foreignKeys.every((_) => _)) { + throw new Error('Could not find key columns!'); + } + if (keys.some((key) => omit(key, 'read'))) { + return memo; + } + if (foreignKeys.some((key) => omit(key, 'read'))) { + return memo; + } + const isUnique = !!table.constraints.find( + (c) => + (c.type === 'p' || c.type === 'u') && + c.keyAttributeNums.length === keys.length && + c.keyAttributeNums.every((n, i) => keys[i].num === n) + ); + if (isUnique) { + // No point aggregating over a relation that's unique + return memo; + } + + const tableAlias = sql.identifier(Symbol(`${foreignTable.namespaceName}.${foreignTable.name}`)); + + // Add count + memo = build.extend( + memo, + orderByAscDesc( + inflection.orderByCountOfManyRelationByKeys(keys, table, foreignTable, constraint), + ({queryBuilder}) => { + const foreignTableAlias = queryBuilder.getTableAlias(); + const conditions: SQL[] = []; + keys.forEach((key, i) => { + conditions.push( + sql.fragment`${tableAlias}.${sql.identifier(key.name)} = ${foreignTableAlias}.${sql.identifier( + foreignKeys[i].name + )}` + ); + }); + return sql.fragment`(select count(*) from ${sql.identifier( + table.namespaceName, + table.name + )} ${tableAlias} where (${sql.join(conditions, ' AND ')}))`; + }, + false + ), + `Adding orderBy count to '${foreignTable.namespaceName}.${foreignTable.name}' using constraint '${constraint.name}'` + ); + + // Filter out attributes relating to historical. This was causing conflicts with `id` and `_id` + const attributes = table.attributes.filter((attr) => attr.name !== '_id' && attr.name !== '_block_height'); + + // Add other aggregates + pgAggregateSpecs.forEach((spec) => { + attributes.forEach((attr) => { + memo = build.extend( + memo, + orderByAscDesc( + inflection.orderByColumnAggregateOfManyRelationByKeys(keys, table, foreignTable, constraint, spec, attr), + ({queryBuilder}) => { + const foreignTableAlias = queryBuilder.getTableAlias(); + const conditions: SQL[] = []; + keys.forEach((key, i) => { + conditions.push( + sql.fragment`${tableAlias}.${sql.identifier(key.name)} = ${foreignTableAlias}.${sql.identifier( + foreignKeys[i].name + )}` + ); + }); + return sql.fragment`(select ${spec.sqlAggregateWrap( + sql.fragment`${tableAlias}.${sql.identifier(attr.name)}` + )} from ${sql.identifier(table.namespaceName, table.name)} ${tableAlias} where (${sql.join( + conditions, + ' AND ' + )}))`; + }, + false + ), + `Adding orderBy ${spec.id} of '${attr.name}' to '${foreignTable.namespaceName}.${foreignTable.name}' using constraint '${constraint.name}'` + ); + }); + }); + + return memo; + }, {} as OrderSpecs); + + return extend(values, newValues, `Adding aggregate orders to '${foreignTable.namespaceName}.${foreignTable.name}'`); + }); +}; + +export function orderByAscDesc(baseName: string, columnOrSqlFragment: OrderBySpecIdentity, unique = false): OrderSpecs { + return { + [`${baseName}_ASC`]: { + value: { + alias: `${baseName}_ASC`, + specs: [[columnOrSqlFragment, true]], + unique, + }, + }, + [`${baseName}_DESC`]: { + value: { + alias: `${baseName}_DESC`, + specs: [[columnOrSqlFragment, false]], + unique, + }, + }, + }; +} + +export default OrderByAggregatesPlugin;