Skip to content

Commit 2794897

Browse files
committed
Refactor serverless part
1 parent 07333e7 commit 2794897

11 files changed

Lines changed: 105 additions & 133 deletions

File tree

api/index.js

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,9 @@
1-
import * as serverless from '../src/serverless.js'
2-
31
/**
4-
* A serverless function handler for the '/api' route, for use with Vercel.
5-
* This handler follows the AWS Lambda API; Vercel deployments are opted-in
6-
* using the "NODEJS_AWS_HANDLER_NAME" environment variable defined in vercel.json.
7-
*
8-
* See:
9-
* - https://vercel.com/docs/serverless-functions/supported-languages#node.js
10-
* - https://vercel.com/docs/runtimes#advanced-usage/advanced-node-js-usage/aws-lambda-api
2+
* Vercel Serverless Function entry point for the '/api' route.
3+
* Uses the fetch Web Standard supported by Vercel.
114
*
12-
* @param {...any} args - The arguments passed by Vercel to the handler, following the AWS Lambda API.
13-
* @returns {Promise<any>} - The response from the serverless handler, with multiValueHeaders converted to headers for Vercel compatibility.
14-
*/
15-
export const handler = async (...args) => {
16-
const response = await serverless.handler(...args)
17-
return convertMultiValueHeaders(response)
18-
}
19-
20-
/*
21-
* At the time of writing the Vercel polyfill for the AWS Lambda API doesn't support .multiValueHeaders.
22-
* This stops us from attaching CORS headers to requests.
23-
* Since all the headers we commonly attach have a single value, we can map them to .headers instead.
5+
* See: https://vercel.com/docs/functions/functions-api-reference
246
*/
25-
const convertMultiValueHeaders = (response) => {
26-
if (response?.multiValueHeaders == null) return response
27-
28-
response.headers = response.headers ?? {}
29-
30-
for (const [key, value] of Object.entries(response.multiValueHeaders)) {
31-
if (value.length === 1) {
32-
response.headers[key] = value[0]
33-
} else {
34-
console.warn(`multiValueHeaders is currently unsupported on Vercel. Header ${key} will be ignored.`)
35-
}
36-
}
7+
import { handler } from '../src/serverless.js'
378

38-
return response
39-
}
9+
export default { fetch: handler }

functions/api.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

netlify.toml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
[build]
22
publish = "dist"
33
command = "npm run build"
4-
functions = "functions/"
54
environment = { NODE_VERSION = "24", NODE_ENV = "production" }
65

7-
[[redirects]]
8-
from = "/api"
9-
to = "/.netlify/functions/api"
10-
status = 200
11-
force = true
12-
136
[template.environment]
147
ACKEE_MONGODB = "ACKEE_MONGODB"
158
ACKEE_USERNAME = "ACKEE_USERNAME"

netlify/functions/api.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Netlify Serverless Function entry point.
3+
* Uses the Web API (Request/Response) supported by Netlify Functions.
4+
*
5+
* See: https://docs.netlify.com/functions/get-started/
6+
*/
7+
export { handler as default } from '../../src/serverless.js'
8+
9+
export const config = {
10+
path: '/api',
11+
}

package-lock.json

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

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
},
5050
"dependencies": {
5151
"@apollo/server": "^5.4.0",
52-
"@as-integrations/aws-lambda": "^4.0.1",
5352
"@as-integrations/express5": "^1.1.2",
5453
"@graphql-tools/merge": "^9.1.7",
5554
"ackee-tracker": "^5.1.2",

src/server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const handleGraphError = (formattedError, error) => {
4343
}
4444

4545
const attachCorsHeaders = async (request, response, next) => {
46-
const matchingOrigin = await findMatchingOrigin(request, config.allowOrigin, config.autoOrigin)
46+
const matchingOrigin = await findMatchingOrigin(request.headers.origin, config.allowOrigin, config.autoOrigin)
4747

4848
if (matchingOrigin != null) {
4949
response.setHeader('Access-Control-Allow-Origin', matchingOrigin)

src/serverless.js

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { handlers, startServerAndCreateLambdaHandler } from '@as-integrations/aws-lambda'
1+
import { HeaderMap } from '@apollo/server'
22

33
import config from './utils/config.js'
44
import connect from './utils/connect.js'
55
import createApolloServer from './utils/createApolloServer.js'
66
import { createServerlessContext } from './utils/createContext.js'
7-
import fullyQualifiedDomainNames from './utils/fullyQualifiedDomainNames.js'
7+
import findMatchingOrigin from './utils/findMatchingOrigin.js'
88

99
if (config.dbUrl == null) {
1010
throw new Error('MongoDB connection URI missing in environment')
@@ -13,53 +13,88 @@ if (config.dbUrl == null) {
1313
connect(config.dbUrl)
1414

1515
const apolloServer = createApolloServer()
16+
await apolloServer.start()
1617

17-
const resolveAllowedOrigin = async (requestOrigin) => {
18-
if (config.autoOrigin === true) {
19-
const names = await fullyQualifiedDomainNames()
20-
const origins = names.flatMap((name) => [`http://${name}`, `https://${name}`, name])
21-
return origins.includes(requestOrigin) ? requestOrigin : null
18+
const buildCorsHeaders = (allowedOrigin) => {
19+
if (allowedOrigin == null) return {}
20+
21+
return {
22+
'access-control-allow-origin': allowedOrigin,
23+
'access-control-allow-methods': 'GET, POST, PATCH, OPTIONS',
24+
'access-control-allow-headers': 'Content-Type, Authorization, Time-Zone',
25+
'access-control-allow-credentials': 'true',
26+
'access-control-max-age': '3600',
2227
}
28+
}
2329

24-
if (config.allowOrigin === '*') {
25-
return '*'
30+
/**
31+
* Handles incoming requests using the Web API (Request/Response).
32+
* Manages CORS, delegates GraphQL operations to Apollo Server, and returns a Web Response.
33+
* Used by both Vercel and Netlify serverless entry points.
34+
*
35+
* @param {Request} request - The incoming web request.
36+
* @returns {Promise<Response>} - The web response to send back.
37+
*/
38+
export const handler = async (request) => {
39+
const requestOrigin = request.headers.get('origin')
40+
const allowedOrigin = await findMatchingOrigin(requestOrigin, config.allowOrigin, config.autoOrigin)
41+
const corsHeaders = buildCorsHeaders(allowedOrigin)
42+
43+
if (request.method === 'OPTIONS') {
44+
return new Response(null, {
45+
status: 204,
46+
headers: corsHeaders,
47+
})
2648
}
2749

28-
if (config.allowOrigin != null) {
29-
const origins = config.allowOrigin.split(',')
30-
return origins.includes(requestOrigin) ? requestOrigin : null
50+
const url = new URL(request.url)
51+
52+
const headers = new HeaderMap()
53+
for (const [key, value] of request.headers) {
54+
headers.set(key, value)
3155
}
3256

33-
return null
34-
}
57+
const body = request.method === 'POST' ? await request.json() : undefined
58+
59+
const result = await apolloServer.executeHTTPGraphQLRequest({
60+
httpGraphQLRequest: {
61+
method: request.method,
62+
headers,
63+
body,
64+
search: url.search ?? '',
65+
},
66+
context: () => createServerlessContext(request),
67+
})
68+
69+
const responseHeaders = new Headers()
70+
71+
for (const [key, value] of result.headers) {
72+
responseHeaders.set(key, value)
73+
}
74+
75+
for (const [key, value] of Object.entries(corsHeaders)) {
76+
responseHeaders.set(key, value)
77+
}
78+
79+
if (result.body.kind === 'complete') {
80+
return new Response(result.body.string, {
81+
status: result.status ?? 200,
82+
headers: responseHeaders,
83+
})
84+
}
85+
86+
const stream = new ReadableStream({
87+
async start(controller) {
88+
for await (const chunk of result.body.asyncIterator) {
89+
controller.enqueue(new TextEncoder().encode(chunk))
90+
}
3591

36-
export const handler = startServerAndCreateLambdaHandler(
37-
apolloServer,
38-
handlers.createAPIGatewayProxyEventRequestHandler(),
39-
{
40-
context: createServerlessContext,
41-
middleware: [
42-
async (event) => {
43-
// Set request context which is missing on Vercel:
44-
// https://stackoverflow.com/questions/71360059/apollo-server-lambda-unable-to-determine-event-source-based-on-event
45-
if (event.requestContext == null) event.requestContext = {}
46-
47-
const requestOrigin = event.headers?.origin || event.headers?.Origin
48-
const allowedOrigin = await resolveAllowedOrigin(requestOrigin)
49-
50-
return (result) => {
51-
if (allowedOrigin != null) {
52-
result.headers = {
53-
...result.headers,
54-
'access-control-allow-origin': allowedOrigin,
55-
'access-control-allow-methods': 'GET, POST, PATCH, OPTIONS',
56-
'access-control-allow-headers': 'Content-Type, Authorization, Time-Zone',
57-
'access-control-allow-credentials': 'true',
58-
'access-control-max-age': '3600',
59-
}
60-
}
61-
}
62-
},
63-
],
64-
},
65-
)
92+
controller.close()
93+
},
94+
})
95+
96+
return new Response(stream, {
97+
status: result.status ?? 200,
98+
headers: responseHeaders,
99+
})
100+
}

src/utils/createContext.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import createDate from './createDate.js'
55
import { isSet } from './ignoreCookie.js'
66
import isAuthenticated from './isAuthenticated.js'
77

8-
export const createServerlessContext = (integrationContext) => {
9-
return createContext(integrationContext.event.headers['client-ip'], integrationContext.event.headers)
8+
export const createServerlessContext = (request) => {
9+
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip')
10+
const headers = Object.fromEntries(request.headers)
11+
return createContext(ip, headers)
1012
}
1113

1214
export const createExpressContext = ({ req }) => {

src/utils/findMatchingOrigin.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
import fullyQualifiedDomainNames from './fullyQualifiedDomainNames.js'
22

3-
const findOrigin = (request, origins) => {
4-
return origins.find((origin) => origin === request.headers.origin)
5-
}
6-
7-
export default async (request, allowedOrigins, autoOrigin) => {
3+
export default async (requestOrigin, allowedOrigins, autoOrigin) => {
84
if (autoOrigin === true) {
95
const names = await fullyQualifiedDomainNames()
106
const origins = names.flatMap((name) => [`http://${name}`, `https://${name}`])
11-
return findOrigin(request, origins)
7+
return origins.includes(requestOrigin) ? requestOrigin : null
128
}
139

1410
if (allowedOrigins === '*') return '*'
1511

1612
if (allowedOrigins != null) {
1713
const origins = allowedOrigins.split(',')
18-
return findOrigin(request, origins)
14+
return origins.includes(requestOrigin) ? requestOrigin : null
1915
}
16+
17+
return null
2018
}

0 commit comments

Comments
 (0)