-
Notifications
You must be signed in to change notification settings - Fork 7
/
complexity.ts
98 lines (75 loc) · 3.99 KB
/
complexity.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { createMethodDecorator } from "type-graphql";
import { ResolversEnhanceMap } from "./generated";
import { GraphQLResolveInfo } from "graphql";
/* We expose the whole Prisma query functionality to GraphQL users.
As such they can write queries that fetch thousands of entries
By annotating a resolver with @LimitedQuery(), the number of results must be limited, e.g.
query { pupils(take: 100) { ... }}
query { pupils(where: { id: { equals: 1 }}) { ... } }
For nested resolvers the cardinality can also lead to large results even with limitations:
query { pupils(take: 100) { subcourses(take: 100) { } }} -> 10.000 results max
thus the result set of all takes must be below 1000
This is not bulletproof, so some queries might still be heavy, though this should prevent most unintended uses */
type LimitedQueryContext = {
limits?: {
[path: string]: number;
}
};
const ACCUMULATED_LIMIT = 1000;
function enforceAccumulatedLimit(info: GraphQLResolveInfo, context: LimitedQueryContext, cardinality: number) {
if (!context.limits) {
context.limits = {};
}
/* In a nested query such as pupils { subcourses { ... } } this will be called twice,
once for the pupil (annotated with LimitedQuery) and then for each pupil for the subcourses (annotated with LimitEstimated).
Thus this function will be executed once for the pupil, and the limit (specified by TAKE) will be stored in the context.limits
In the subcourse, the path is { key: "subcourses", prev: { key: 0, prev: { key: "pupils"}}}, thus with .prev.prev one can access the limit of the previous association.
If the query retrieves 100 pupils, and 10 subcourses for each pupil, by multiplying we get the maximum number of pupils 100 * 10. On that we enforce the limit
NOTE: Keys of different paths could collide in a query, however we hope that nobody writes such query :)
*/
let accumulatedLimit = (info.path?.prev?.prev && context.limits[ info.path.prev.prev.key ]) ?? 1;
accumulatedLimit *= cardinality;
context.limits[ info.path.key ] = accumulatedLimit;
if (accumulatedLimit > ACCUMULATED_LIMIT) {
const limitInfo = Object.entries(context.limits)
.map(([key, limit]) => `${key}:${limit}`)
.join(", ");
throw new Error(`Overcomplex Query: The nested query might return more than 1000 entries (${limitInfo})`);
}
}
export function LimitedQuery(limit = 100) {
return createMethodDecorator<LimitedQueryContext>(({ args, root, info, context }, next) => {
// mutations are always fine, we only worry about queries
if (info.operation.operation === "mutation") {
return next();
}
const numberLimited = !!args.take;
const numberLimitedLow = args.take <= limit;
const isIDQuery = !!args.where?.id;
if (numberLimited && !numberLimitedLow) {
throw new Error(`Overcomplex Query: Please reduce the TAKE arg to less than ${limit} entries`);
}
if (!isIDQuery && !numberLimitedLow) {
throw new Error(`Overcomplex Query: Please implement pagination with TAKE and SKIP`);
}
const cardinality = isIDQuery ? 1 : args.take;
enforceAccumulatedLimit(info, context, cardinality);
return next();
});
}
/* For nested field resolvers one can still give a brief estimation about the entries expected */
export function LimitEstimated(cardinality: number) {
return createMethodDecorator(({ info, context }, next) => {
enforceAccumulatedLimit(info, context, cardinality);
return next();
});
}
export const complexityEnhanceMap: ResolversEnhanceMap = {
Bbb_meeting: { bbb_meetings: [LimitedQuery()] },
Concrete_notification: { concrete_notifications: [LimitedQuery()] },
Course: { courses: [LimitedQuery()] },
Log: { logs: [LimitedQuery()] },
Pupil: { pupils: [LimitedQuery()] },
Match: { matches: [LimitedQuery()] },
Project_match: { project_matches: [LimitedQuery()] }
};