Skip to content

Commit 5b1e5fb

Browse files
authored
Merge branch 'dev' into jd/deploy2
2 parents f085101 + a1651a5 commit 5b1e5fb

File tree

10 files changed

+748
-275
lines changed

10 files changed

+748
-275
lines changed

.travis.yml

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ language: node_js
22
node_js:
33
- 16
44
- 17
5-
- 18
5+
# - 18
66

77
# run test for the above node versions for branches dev and main
88
branches:
@@ -16,17 +16,13 @@ script:
1616
- 'npm run test'
1717
- 'npm run build'
1818
# specify a job to run
19-
jobs:
20-
include:
21-
- stage: npm release
22-
node_js: "17"
23-
script: echo "Deploying to npm ..."
24-
deploy:
25-
on:
26-
branch: main
27-
tags: true
28-
provider: npm
29-
email: NPM_EMAIL_ADDRESS
30-
api_key: NPM_API_KEY
19+
deploy:
20+
on:
21+
branch: main
22+
tags: true
23+
skip_cleanup: false
24+
provider: npm
25+
email: $NPM_EMAIL_ADDRESS
26+
api_key: $NPM_API_KEY
3127

3228

README.md

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -117,49 +117,62 @@ For queries that return a list, the complexity can be determined by providing a
117117

118118
1. Slicing arguments: lists must be bounded by one integer slicing argument in order to calculate the complexity for the field. This package supports the slicing arguments `first`, `last` and `limit`. The complexity of the list will be the value passed as the argument to the field.
119119

120-
2. Directives: First, `@listCost` must be defined in your schema with `directive @listCost(cost: Int!) on FIELD_DEFINITION`. Then, on any unbounded list field, add `@listCost(cost: Int)` and replace `Int` with the complexity you want applied whenever the list is queried.
120+
2. Directives: To use directives, `@listCost` must be defined in your schema with `directive @listCost(cost: Int!) on FIELD_DEFINITION`. Then, on any field which resolves to an unbounded list, add `@listCost(cost: [Int])` where `[Int]` is the complexity for this field.
121121

122-
(Note: Slicing arguments are preferred! `@listCost` is in place for any reason slicing arguments cannot be used.)
122+
(Note: Slicing arguments are preferred and will override the the `@listCost` directive! `@listCost` is in place as a fall back.)
123+
124+
```graphql
125+
directive @listCost(cost: Int!) on FIELD_DEFINITION
126+
type Human {
127+
id: ID!
128+
}
129+
type Query {
130+
humans: [Human] @listCost(cost: 10)
131+
}
132+
```
123133

124134
## <a name="how-it-works"></a> How It Works
125135

126136
Requests are rate-limited based on the IP address associated with the request.
127137

128-
On server start, the GraphQL (GQL) schema is parsed to build an object that maps GQL types/fields to their corresponding weights. Type weights can be provided during <a href="typeWeights">initial configuration</a>. When a request is received, this object is used to cross reference the fields queried by the user and compute the complexity of each field. The total complexity of the request is the sum of these values.
138+
On startup, the GraphQL (GQL) schema is parsed to build an object that maps GQL types/fields to their corresponding weights. Type weights can be provided during <a href="typeWeights">initial configuration</a>. When a request is received, this object is used to cross reference the fields queried by the user and compute the complexity of each field. The total complexity of the request is the sum of these values.
129139

130140
Complexity is determined, statically (before any resolvers are called) to estimate the upper bound of the response size - a proxy for the work done by the server to build the response. The total complexity is then used to allow/block the request based on popular rate-limiting algorithms.
131141

132142
Requests for each user are processed sequentially by the rate limiter.
133143

134144
Example (with default weights):
135145

136-
```javascript
137-
query { // 1 (complexity)
138-
hero (episode: EMPIRE) { // 1
139-
name // 0
140-
id // 0
141-
friends (first: 3) { // 3
142-
name // 0
143-
id // 0
144-
}
145-
}
146-
reviews(episode: EMPIRE, limit: 5) { // 5
147-
stars // stars 0
148-
commentary // commentary 0
149-
}
150-
}
151-
// total complexity of 10
146+
```graphql
147+
query {
148+
# 1 query
149+
hero(episode: EMPIRE) {
150+
# 1 object
151+
name # 0 scalar
152+
id # 0 scalar
153+
friends(first: 3) {
154+
# 3 objects
155+
name # 0 scalar
156+
id # 0 scalar
157+
}
158+
}
159+
reviews(episode: EMPIRE, limit: 5) {
160+
# 5 objects
161+
stars # 0 scalar
162+
commentary # 0 scalar
163+
}
164+
} # total complexity of 10
152165
```
153166

154167
## <a name="response"></a> Response
155168

156169
1. <b>Blocked Requests</b>: blocked requests recieve a response with,
157170

158171
- status of `429` for `Too Many Requests`
159-
- `Retry-After` header with a value of the time to wait in seconds before the request would be approved (`Infinity` if the complexity is greater than rate-limiting capacity).
160-
- A JSON response with the `tokens` available, `complexity` of the query, `depth` of the query, `success` of the query set to `false`, and the UNIX `timestamp` of the request
172+
- `Retry-After` header indicating the time to wait in seconds before the request could be approved (`Infinity` if the complexity is greater than rate-limiting capacity).
173+
- A JSON response with the remaining `tokens` available, `complexity` of the query, `depth` of the query, `success` of the query set to `false`, and the UNIX `timestamp` of the request
161174

162-
2. <b>Successful Requests</b>: successful requests are passed onto the next function in the middleware chain with the following properties saved to `res.locals`
175+
2. <b>Successful Requests</b>: successful requests are passed on to the next function in the middleware chain with the following properties saved to `res.locals`
163176

164177
```javascript
165178
{
@@ -168,15 +181,15 @@ query { // 1 (complexity)
168181
tokens: number, // tokens available after request
169182
compexity: number, // complexity of the query
170183
depth: number, // depth of the query
171-
timestamp: number, // ms
184+
timestamp: number, // UNIX timestamp
172185
}
173186
}
174187
```
175188

176189
## <a name="error-handling"></a> Error Handling
177190

178191
- Incoming queries are validated against the GraphQL schema. If the query is invalid, a response with status code `400` is returned along with an array of GraphQL Errors that were found.
179-
- To avoid disrupting server activity, errors thrown during the analysis and rate-limiting of the query are logged and the request is passed onto the next middleware function in the chain.
192+
- To avoid disrupting server activity, errors thrown during the analysis and rate-limiting of the query are logged and the request is passed onto the next piece of middleware in the chain.
180193

181194
## <a name="future-development"></a> Future Development
182195

package-lock.json

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"repository": {
1717
"type": "git",
18-
"url": "git+https://github.com/oslabs-beta/graph-beaver.git"
18+
"url": "git+https://github.com/oslabs-beta/graphql-gate.git"
1919
},
2020
"keywords": [
2121
"graphql",
@@ -64,6 +64,7 @@
6464
"*.{js,ts,css,md}": "prettier --write --ignore-unknown"
6565
},
6666
"dependencies": {
67+
"@graphql-tools/utils": "^8.8.0",
6768
"graphql": "^16.5.0",
6869
"ioredis": "^5.0.5"
6970
}

src/@types/rateLimit.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ export interface RedisBucket {
2424
timestamp: number;
2525
}
2626

27-
export interface RedisWindow {
27+
export interface FixedWindow {
2828
currentTokens: number;
29-
previousTokens: number;
3029
fixedWindowStart: number;
3130
}
31+
export interface RedisWindow extends FixedWindow {
32+
previousTokens: number;
33+
}
3234

3335
export type RedisLog = RedisBucket[];
3436

src/analysis/buildTypeWeights.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,25 @@ function parseObjectFields(
113113
resolveTo: listType.toString().toLocaleLowerCase(),
114114
};
115115
} else {
116+
// if the @listCost directive is given for the field,
117+
// apply the cost argument's value to the field's weight
118+
const directives = fields[field].astNode?.directives;
119+
120+
if (directives && directives.length > 0) {
121+
directives.forEach((dir) => {
122+
if (dir.name.value === 'listCost') {
123+
if (dir.arguments && dir.arguments[0].value.kind === Kind.INT) {
124+
result.fields[field] = {
125+
resolveTo: listType.toString().toLocaleLowerCase(),
126+
weight: Number(dir.arguments[0].value.value),
127+
};
128+
}
129+
throw new SyntaxError(`@listCost directive improperly configured`);
130+
}
131+
});
132+
}
133+
134+
// if no directive is supplied to list field
116135
fields[field].args.forEach((arg: GraphQLArgument) => {
117136
// If field has an argument matching one of the limiting keywords and resolves to a list
118137
// then the weight of the field should be dependent on both the weight of the resolved type and the limiting argument.
@@ -145,14 +164,16 @@ function parseObjectFields(
145164
return multiplier * (selectionsCost + weight);
146165
// ? what else can get through here
147166
}
167+
148168
// if there is no argument provided with the query, check the schema for a default
149169
if (arg.defaultValue) {
150170
return Number(arg.defaultValue) * (selectionsCost + weight);
151171
}
152172

153-
// FIXME: The list is unbounded. Return the object weight for
173+
// if an unbounded list has no @listCost directive attached
154174
throw new Error(
155-
`ERROR: buildTypeWeights: Unbouned list complexity not supported. Query results should be limited with ${KEYWORDS}`
175+
`ERROR: buildTypeWeights: Use directive @listCost(cost: Int!) on unbounded lists,
176+
or limit query results with ${KEYWORDS}`
156177
);
157178
},
158179
};

0 commit comments

Comments
 (0)