Cảnh báo Chương này chỉ áp dụng cho cách tiếp cận code first.
Query complexity (độ phức tạp của query) cho phép bạn định nghĩa mức độ phức tạp của từng field, và giới hạn query bằng một độ phức tạp tối đa. Ý tưởng là gán cho mỗi field một con số thể hiện độ phức tạp của nó. Mặc định phổ biến là gán độ phức tạp 1 cho mỗi field. Ngoài ra, cách tính độ phức tạp của một GraphQL query có thể được tùy biến thông qua các complexity estimator (bộ ước lượng độ phức tạp). Một complexity estimator là một hàm đơn giản dùng để tính độ phức tạp cho một field. Bạn có thể thêm bao nhiêu estimator tùy ý vào rule, chúng sẽ được thực thi lần lượt theo thứ tự. Estimator đầu tiên trả về một giá trị số sẽ được dùng làm độ phức tạp cho field đó.
Package @nestjs/graphql tích hợp rất tốt với các thư viện như graphql-query-complexity, thư viện cung cấp giải pháp phân tích chi phí (cost analysis). Với thư viện này, bạn có thể từ chối các query gửi đến GraphQL server của mình nếu chúng bị đánh giá là quá tốn kém để thực thi.
Cài đặt
Để bắt đầu sử dụng, trước tiên ta cài đặt package cần thiết.
$ npm install --save graphql-query-complexity
Bắt đầu
Sau khi cài đặt xong, ta có thể định nghĩa class ComplexityPlugin:
import { GraphQLSchemaHost } from '@nestjs/graphql';
import { Plugin } from '@nestjs/apollo';
import {
ApolloServerPlugin,
BaseContext,
GraphQLRequestListener,
} from '@apollo/server';
import { GraphQLError } from 'graphql';
import {
fieldExtensionsEstimator,
getComplexity,
simpleEstimator,
} from 'graphql-query-complexity';
@Plugin()
export class ComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> {
const maxComplexity = 20;
const { schema } = this.gqlSchemaHost;
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > maxComplexity) {
throw new GraphQLError(
`Query is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}`,
);
}
console.log('Query Complexity:', complexity);
},
};
}
}
Để minh họa, ở đây ta khai báo độ phức tạp tối đa cho phép là 20. Trong ví dụ trên, ta dùng 2 estimator: simpleEstimator và fieldExtensionsEstimator.
simpleEstimator: estimator đơn giản này trả về một độ phức tạp cố định cho mỗi fieldfieldExtensionsEstimator: estimator này lấy giá trị độ phức tạp được khai báo trên từng field của schema
Gợi ý Đừng quên thêm class này vào mảng providers của một module bất kỳ.
Ghi chú người dịch Cơ chế estimator “chạy lần lượt, dùng kết quả đầu tiên trả về giá trị số” khá giống middleware chain trong Express: mỗi hàm có quyền xử lý rồi trả kết quả hoặc nhường cho hàm kế tiếp. Ở đây fieldExtensionsEstimator chạy trước để ưu tiên độ phức tạp bạn khai báo tường minh trên field (qua @Field({ complexity })), còn simpleEstimator chỉ là fallback khi field không khai báo gì. Đây là kỹ thuật chặn tấn công kiểu “query siêu sâu, siêu rộng” thường gặp khi client tự do lồng nested field trong GraphQL — điều mà REST API không gặp phải vì response shape đã cố định.
Độ phức tạp ở cấp field
Với plugin này, giờ ta có thể định nghĩa độ phức tạp cho bất kỳ field nào bằng cách khai báo thuộc tính complexity trong object option truyền vào decorator @Field(), như sau:
@Field({ complexity: 3 })
title: string;
Ngoài ra, bạn cũng có thể định nghĩa một hàm estimator:
@Field({ complexity: (options: ComplexityEstimatorArgs) => ... })
title: string;
Độ phức tạp ở cấp Query/Mutation
Bên cạnh đó, decorator @Query() và @Mutation() cũng có thể khai báo thuộc tính complexity, ví dụ:
@Query({ complexity: (options: ComplexityEstimatorArgs) => options.args.count * options.childComplexity })
items(@Args('count') count: number) {
return this.itemsService.getItems({ count });
}
Ghi chú người dịch Mẫu count * childComplexity rất hữu ích khi field trả về một danh sách: độ phức tạp thực tế phụ thuộc vào số lượng item client yêu cầu (tham số count) nhân với độ phức tạp của các field con mà client chọn lấy trong mỗi item. Đây là cách tránh trường hợp một query items(count: 10000) { veryExpensiveField } lách qua được giới hạn mặc định chỉ vì bản thân field items có complexity thấp.