Skip to content

Commit

Permalink
feat: add deafult query complexity calculations with arguments
Browse files Browse the repository at this point in the history
ref #EX-1723
  • Loading branch information
lgobbi-atix authored and rhyslbw committed Jun 24, 2021
1 parent ebe8088 commit b16c77d
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 16 deletions.
10 changes: 5 additions & 5 deletions packages/api-cardano-db-hasura/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ type Transaction_avg_fields {
mint: Token_avg_fields
size: Float
totalOutput: Float
withdrawals: Withdrawal_ave_fields
withdrawals: Withdrawal_avg_fields
}

type Transaction_max_fields {
Expand Down Expand Up @@ -1333,14 +1333,14 @@ type Epoch_aggregate {
}

type Epoch_aggregate_fields {
ave: Epoch_ave_fields!
avg: Epoch_avg_fields!
count: String!
max: Epoch_max_fields!
min: Epoch_min_fields!
sum: Epoch_sum_fields!
}

type Epoch_ave_fields {
type Epoch_avg_fields {
fees: Float!
output: Float!
transactionsCount: Float!
Expand Down Expand Up @@ -1395,13 +1395,13 @@ type Withdrawal_aggregate {

type Withdrawal_aggregate_fields {
count: String!
ave: Withdrawal_ave_fields!
avg: Withdrawal_avg_fields!
max: Withdrawal_max_fields!
min: Withdrawal_min_fields!
sum: Withdrawal_sum_fields!
}

type Withdrawal_ave_fields {
type Withdrawal_avg_fields {
amount: String
}

Expand Down
41 changes: 33 additions & 8 deletions packages/api-cardano-db-hasura/src/executableSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import {
} from 'graphql-scalars'
import { CardanoNodeClient } from './CardanoNodeClient'
import BigNumber from 'bignumber.js'
import { FieldsComplexityMapping, ComplexityMapping } from './queryComplexity'
import {
FieldsComplexityMapping,
ComplexityMapping,
defaultComplexity,
getDefaultQueryComplexity
} from './queryComplexity'
const GraphQLBigInt = require('graphql-bigint')

export const scalarResolvers = {
Expand All @@ -39,7 +44,7 @@ export async function buildSchema (
hasuraClient: HasuraClient,
genesis: Genesis,
cardanoNodeClient: CardanoNodeClient,
customFieldsComplexity: FieldsComplexityMapping = {}
customFieldsComplexity: FieldsComplexityMapping = defaultComplexity
) {
const throwIfNotInCurrentEra = async (queryName: string) => {
if (!(await hasuraClient.isInCurrentEra())) {
Expand All @@ -50,12 +55,26 @@ export async function buildSchema (
}
const getComplexityExtension = (operation: string, queryName: string) => {
if (operation in customFieldsComplexity) {
const operationMapping = customFieldsComplexity[operation] as ComplexityMapping
if (queryName in operationMapping) {
return operationMapping.extensions
const operationMapping = customFieldsComplexity[
operation
] as ComplexityMapping
if (
queryName in operationMapping &&
operationMapping[queryName].extensions
) {
// If it has a custom complexity then use that one and ignore the base cost,
// otherwise use the default with the base cost
return {
complexity:
operationMapping[queryName].extensions.complexity ||
getDefaultQueryComplexity(
operationMapping[queryName].extensions.baseCost
)
}
}
}
return null
// If not found, then just return the default complexity estimators
return { complexity: getDefaultQueryComplexity() }
}
return makeExecutableSchema({
resolvers: Object.assign({}, scalarResolvers, customFieldsComplexity, {
Expand Down Expand Up @@ -367,7 +386,10 @@ export async function buildSchema (
})
},
selectionSet: null,
extensions: getComplexityExtension('Query', 'stakeDeregistrations_aggregate')
extensions: getComplexityExtension(
'Query',
'stakeDeregistrations_aggregate'
)
},
stakePools: {
resolve: (_root, args, context, info) => {
Expand Down Expand Up @@ -423,7 +445,10 @@ export async function buildSchema (
})
},
selectionSet: null,
extensions: getComplexityExtension('Query', 'stakeRegistrations_aggregate')
extensions: getComplexityExtension(
'Query',
'stakeRegistrations_aggregate'
)
},
transactions: {
resolve: (_root, args, context, info) => {
Expand Down
173 changes: 171 additions & 2 deletions packages/api-cardano-db-hasura/src/queryComplexity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ComplexityEstimator } from 'graphql-query-complexity'
import {
ComplexityEstimator,
ComplexityEstimatorArgs
} from 'graphql-query-complexity'

type ComplexityExtension = {
extensions: {
complexity: number | ComplexityEstimator;
complexity?: number | ComplexityEstimator;
baseCost?: number;
};
};

Expand All @@ -14,3 +18,168 @@ export type FieldsComplexityMapping = {
| ComplexityMapping
| ComplexityExtension;
};

const aggregateFieldsCosts = (
pick: Array<'avg' | 'count' | 'max' | 'min' | 'sum'> = [],
costs: {
count?: number;
avg?: number;
max?: number;
min?: number;
sum?: number;
} = {}
) => {
const defaultCosts: { [key: string]: ComplexityExtension } = {
avg: { extensions: { complexity: costs.avg || 2 } },
count: { extensions: { complexity: costs.count || 1 } },
max: { extensions: { complexity: costs.max || 2 } },
min: { extensions: { complexity: costs.min || 2 } },
sum: { extensions: { complexity: costs.sum || 2 } }
}
if (!pick || !pick.length) return defaultCosts

const pickedFields: { [key: string]: ComplexityExtension } = {}
pick.forEach((field) => {
pickedFields[field] = defaultCosts[field]
})
return pickedFields
}

const booleanOperatorsCostMapping: { [key: string]: number } = {
_eq: 1,
_gt: 1,
_gte: 1,
_in: 1,
_lt: 1,
_lte: 1,
_neq: 1,
_nin: 1,
_is_null: 1,
_ilike: 2,
_like: 2,
_nilike: 2,
_nlike: 2,
_nsimilar: 2,
_similar: 2
}

const calculateWhereComplexity = (where: any): number => {
let cost: number = 0
if (!where) return cost

Object.entries(where).forEach(([key, value]: [string, any]) => {
// Logical operators
if (key === '_not') {
cost += calculateWhereComplexity(value) || 0
return
}
if (key === '_or' || key === '_and') {
Object.values(value).forEach((condition) => {
cost += calculateWhereComplexity(condition) || 0
})
return
}

// Simple filters
const operators = Object.entries(value)
if (
operators.some(([operator, _]) => operator in booleanOperatorsCostMapping)
) {
operators.forEach(([operator, v]) => {
if (operator in booleanOperatorsCostMapping) {
const operationCost = booleanOperatorsCostMapping[operator] || 0
if (operator === '_in' || operator === '_nin') {
if (Array.isArray(v)) {
cost += operationCost * v.length
}
} else {
cost += operationCost
}
}
})
return
}

// If none of the above we assume it's a nested filter
cost += 1 + (calculateWhereComplexity(value) || 0)
})
return cost
}

const calculateOffsetComplexity = (offset: any): number => {
if (!offset || isNaN(Number(offset))) return 0
return Math.ceil(Number(offset) / 100)
}

const calculateOrderByComplexity = (orderBy: any[]): number => {
const cost: number = 0
if (!orderBy || !orderBy.length) return cost

const calculateOrderByFieldComplexity = (field: {
[key: string]: string;
}): number => {
return Object.entries(field).reduce(
(finalCost, [_, value]: [string, any]) => {
return typeof value === 'string'
? finalCost + 1
: finalCost + 1 + (calculateOrderByFieldComplexity(value) || 0)
},
0
)
}
return orderBy.reduce(
(finalCost, field) =>
finalCost + (calculateOrderByFieldComplexity(field) || 0),
cost
)
}

export const defaultComplexity: FieldsComplexityMapping = {
Query: {
activeStake_aggregate: { extensions: { baseCost: 2 } },
assets_aggregate: { extensions: { baseCost: 2 } },
blocks_aggregate: { extensions: { baseCost: 10 } },
delegations_aggregate: { extensions: { baseCost: 2 } },
epochs_aggregate: { extensions: { baseCost: 2 } },
rewards_aggregate: { extensions: { baseCost: 2 } },
stakeDeregistrations_aggregate: { extensions: { baseCost: 2 } },
stakePools_aggregate: { extensions: { baseCost: 3 } },
stakeRegistrations_aggregate: { extensions: { baseCost: 2 } },
tokenMints_aggregate: { extensions: { baseCost: 2 } },
transactions_aggregate: { extensions: { baseCost: 5 } },
utxos_aggregate: { extensions: { baseCost: 4 } },
withdrawals_aggregate: { extensions: { baseCost: 2 } }
},
ActiveStake_aggregate_fields: aggregateFieldsCosts(),
Asset_aggregate_fields: aggregateFieldsCosts(['count']),
Block_aggregate_fields: aggregateFieldsCosts(),
Delegation_aggregate_fields: aggregateFieldsCosts(['count']),
Epoch_aggregate_fields: aggregateFieldsCosts(),
Reward_aggregate_fields: aggregateFieldsCosts(),
StakeDeregistration_aggregate_fields: aggregateFieldsCosts(['count']),
StakePool_aggregate_fields: aggregateFieldsCosts(),
StakeRegistration_aggregate_fields: aggregateFieldsCosts(['count']),
TokenMint_aggregate_fields: aggregateFieldsCosts(),
Transaction_aggregate_fields: aggregateFieldsCosts(),
TransactionOutput_aggregate_fields: aggregateFieldsCosts(),
Withdrawal_aggregate_fields: aggregateFieldsCosts()
}

export const getDefaultQueryComplexity =
(baseQueryCost: number = 1) =>
({ args, childComplexity }: ComplexityEstimatorArgs) => {
const { where, offset, order_by: orderBy } = args

const whereComplexity = calculateWhereComplexity(where) || 0
const offsetComplexity = calculateOffsetComplexity(offset) + 1
const orderByComplexity = calculateOrderByComplexity(orderBy) + 1

// Base query cost shouldn't be less than 1
const base = baseQueryCost < 1 ? 1 : baseQueryCost
return (
base *
((childComplexity + whereComplexity) *
offsetComplexity *
orderByComplexity)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const queryComplexityPlugin = (
})
// Here we can react to the calculated complexity,
// like compare it with max and throw error when the threshold is reached.
if (complexity >= maximumComplexity) {
if (complexity > maximumComplexity) {
throw new QueryTooComplex(complexity, maximumComplexity)
}
// This can be used for logging or to implement rate limiting
Expand Down

0 comments on commit b16c77d

Please sign in to comment.