-
Notifications
You must be signed in to change notification settings - Fork 13
/
apq.ts
227 lines (187 loc) · 5.57 KB
/
apq.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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import type { Handler } from 'worktop'
import { SHA256, timingSafeEqual } from 'worktop/crypto'
import { encode } from 'worktop/utils'
import { find, save } from '../stores/APQCache'
import { Headers as HTTPHeaders, Scope } from '../utils'
import { GraphQLRequest } from './graphql'
declare const APQ_TTL: string
const defaultAPQTTL = parseInt(APQ_TTL)
declare const ORIGIN_URL: string
const originUrl = ORIGIN_URL
declare const SWR: string
const swr = parseInt(SWR)
declare const IGNORE_ORIGIN_CACHE_HEADERS: string
type APQExtensions = {
persistedQuery: {
version: number
sha256Hash: string
variables?: Record<string, number | string>
}
}
export const apq: Handler = async function (req, res) {
const extensionsRawJson = req.query.get('extensions')
if (!extensionsRawJson) {
return res.send(400, {
name: 'APQValidation',
error: 'Invalid APQ request',
})
}
const { persistedQuery } = JSON.parse(extensionsRawJson) as APQExtensions
if (persistedQuery.version !== 1) {
return res.send(400, {
name: 'APQValidation',
error: 'Unsupported persisted query version',
})
}
const headers: Record<string, string> = {
[HTTPHeaders.fgScope]: Scope.PUBLIC,
}
const operationName = req.query.get('operationName')
const authorizationHeader = req.headers.get(HTTPHeaders.authorization)
const cacheUrl = new URL(req.url)
let cacheKey = ''
if (operationName) {
cacheKey += operationName
}
// append "authorization" value to query and make it part of the cache key
if (authorizationHeader) {
headers[HTTPHeaders.fgScope] = Scope.AUTHENTICATED
cacheKey += operationName ? '/' : '' + (await SHA256(authorizationHeader))
}
cacheUrl.pathname = cacheUrl.pathname + cacheKey
const cacheRequest = new Request(cacheUrl.toString(), {
headers: req.headers,
method: 'GET',
})
const cache = caches.default
let response = await cache.match(cacheRequest)
if (response) {
return response
}
let query = req.query.get('query')
const result = await find(persistedQuery.sha256Hash)
if (result) {
query = result.query
}
// if query could not be found in cache, we will assume
// the next action is to register the APQ
if (!result) {
// check if APQ hash is matching with the query hash
if (query) {
if (
!timingSafeEqual(
encode(await SHA256(query)),
encode(persistedQuery.sha256Hash),
)
) {
return res.send(400, 'provided sha does not match query')
}
// Alias for `event.waitUntil`
// ~> queues background task (does NOT delay response)
req.extend(
save(
persistedQuery.sha256Hash,
{
query,
},
defaultAPQTTL,
),
)
} else {
// when APQ could not be found the client must retry with the original query
return res.send(200, {
data: {
errors: [
{
extensions: {
code: 'PERSISTED_QUERY_NOT_FOUND',
},
},
],
},
})
}
}
let variables = req.query.get('variables')
const q = query!
const body: GraphQLRequest = { query: q }
if (operationName) {
body.operationName = operationName
}
if (variables) {
body.variables = JSON.parse(variables)
}
const forwardedHeaders = new Headers()
forwardedHeaders.append(HTTPHeaders.contentType, 'application/json')
if (authorizationHeader) {
forwardedHeaders.append(HTTPHeaders.authorization, authorizationHeader)
}
let originResponse = await fetch(originUrl, {
body: JSON.stringify(body),
headers: forwardedHeaders,
method: 'POST',
})
// don't cache origin errors
if (!originResponse.ok) {
return res.send(
originResponse.status,
{
name: 'OriginError',
error: `fetch error: ${originResponse.statusText}`,
},
{
[HTTPHeaders.cacheControl]: 'public, no-cache, no-store',
},
)
}
let json = await originResponse.json()
// don't cache graphql errors
if (json?.errors) {
return res.send(
500,
{
name: 'GraphQLError',
errors: json?.errors,
},
{
[HTTPHeaders.cacheControl]: 'public, no-cache, no-store',
},
)
}
const ignoreOriginCacheHeaders = IGNORE_ORIGIN_CACHE_HEADERS === '1'
const cacheControlHeader = originResponse.headers.get(
HTTPHeaders.cacheControl,
)
let cacheMaxAge = APQ_TTL
headers[
HTTPHeaders.cacheControl
] = `public, max-age=${cacheMaxAge}, stale-if-error=${swr}, stale-while-revalidate=${swr}`
headers[HTTPHeaders.contentType] = 'application/json'
headers[HTTPHeaders.fgOriginStatusCode] = originResponse.status.toString()
headers[HTTPHeaders.fgOriginStatusText] = originResponse.statusText.toString()
const cacheTags = [persistedQuery.sha256Hash]
// You can purge your cache by tags
// This is only evaluated on enterprise plan and the header is never visible for customers
if (operationName) {
cacheTags.push(operationName)
}
headers[HTTPHeaders.cfCacheTag] = cacheTags.join(',')
if (ignoreOriginCacheHeaders === false && cacheControlHeader) {
headers[HTTPHeaders.cacheControl] = cacheControlHeader
}
// Alias for `event.waitUntil`
// ~> queues background task (does NOT delay response)
req.extend(
cache.put(
cacheRequest,
new Response(JSON.stringify(json), {
status: originResponse.status,
statusText: originResponse.statusText,
headers: {
...headers,
},
}),
),
)
return res.send(200, json, headers)
}