Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit fe618a7

Browse files
Deadlegdanielcondemarin
authored andcommitted
Add support for APIs (#117)
There are only a few minor issues with the current implementation to get this going, namely: - Allowing POST, PATCH etc. in Cloudfront. - Checking if the NextJS page is a React component or not. The change to `compatLayer.js` is needed so that if a request without a body is sent to an API the Stream still has some content. Without a `_read() is not implemented` is raised which generates an HTTP 400 response without a body. The stream test case is removed since the stream already has EOF pushed to it. Don't think there is a use case where the input is a stream itself. #116
1 parent 1989fe1 commit fe618a7

File tree

9 files changed

+178
-30
lines changed

9 files changed

+178
-30
lines changed

packages/next-aws-lambda/__tests__/index.test.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ describe("next-aws-lambda", () => {
99
const callback = () => {};
1010
const context = {};
1111

12+
// Mock due to mismatched Function types
13+
// https://github.com/facebook/jest/issues/6329
14+
mockRender = jest.fn();
15+
mockDefault = jest.fn();
1216
const page = {
13-
render: jest.fn()
17+
render: (...args) => mockRender(...args),
18+
default: (...args) => mockDefault(...args)
1419
};
1520
const req = {};
1621
const res = {};
@@ -22,6 +27,33 @@ describe("next-aws-lambda", () => {
2227

2328
compat(page)(event, context, callback);
2429

25-
expect(page.render).toBeCalledWith(req, res);
30+
expect(mockRender).toBeCalledWith(req, res);
31+
expect(mockDefault).not.toBeCalled();
32+
});
33+
});
34+
35+
describe("next-aws-lambda", () => {
36+
it("passes request and response to next api", () => {
37+
const event = { foo: "bar" };
38+
const callback = () => {};
39+
const context = {};
40+
41+
// Mock due to mismatched Function types
42+
// https://github.com/facebook/jest/issues/6329
43+
mockDefault = jest.fn();
44+
const page = {
45+
default: (...args) => mockDefault(...args)
46+
};
47+
const req = {};
48+
const res = {};
49+
50+
compatLayer.mockReturnValueOnce({
51+
req,
52+
res
53+
});
54+
55+
compat(page)(event, context, callback);
56+
57+
expect(mockDefault).toBeCalledWith(req, res);
2658
});
2759
});

packages/next-aws-lambda/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ const reqResMapper = require("./lib/compatLayer");
22

33
const handlerFactory = page => (event, _context, callback) => {
44
const { req, res } = reqResMapper(event, callback);
5-
page.render(req, res);
5+
if (page.render instanceof Function) {
6+
// Is a React component
7+
page.render(req, res);
8+
} else {
9+
// Is an API
10+
page.default(req, res);
11+
}
612
};
713

814
module.exports = handlerFactory;

packages/next-aws-lambda/lib/__tests__/compatLayer.request.test.js

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -190,29 +190,6 @@ describe("compatLayer.request", () => {
190190
]);
191191
});
192192

193-
it("stream", done => {
194-
const { req } = create({
195-
requestContext: {
196-
path: ""
197-
},
198-
headers: {}
199-
});
200-
201-
let data = "";
202-
203-
req.on("data", chunk => {
204-
data += chunk;
205-
});
206-
207-
req.on("end", () => {
208-
expect(data).toEqual("ok");
209-
done();
210-
});
211-
212-
req.push("ok");
213-
req.push(null);
214-
});
215-
216193
it("text body", done => {
217194
const { req } = create({
218195
requestContext: {

packages/next-aws-lambda/lib/__tests__/compatLayer.response.test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,8 @@ describe("compatLayer.response", () => {
195195
done();
196196
}
197197
);
198-
req.pipe(res);
199-
req.push("ok");
200-
req.push(null);
198+
199+
res.end("ok");
201200
});
202201

203202
it("base64 support", done => {

packages/next-aws-lambda/lib/compatLayer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ const reqResMapper = (event, callback) => {
105105
};
106106
if (event.body) {
107107
req.push(event.body, event.isBase64Encoded ? "base64" : undefined);
108-
req.push(null);
109108
}
109+
req.push(null);
110110

111111
function fixApiGatewayMultipleHeaders() {
112112
for (const key of Object.keys(response.multiValueHeaders)) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default (req, res) => {{ test: 'test' }}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
service: single-api-fixture
3+
4+
provider:
5+
name: aws
6+
runtime: nodejs8.10
7+
8+
stage: dev
9+
region: eu-west-1
10+
11+
plugins:
12+
- index # this works because jest modulePaths adds plugin path, see package.json
13+
14+
package:
15+
# exclude everything
16+
# page handlers are automatically included by the plugin
17+
exclude:
18+
- ./**
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const nextBuild = require("next/dist/build");
2+
const path = require("path");
3+
const AdmZip = require("adm-zip");
4+
const readCloudFormationUpdateTemplate = require("../utils/test/readCloudFormationUpdateTemplate");
5+
const testableServerless = require("../utils/test/testableServerless");
6+
7+
jest.mock("next/dist/build");
8+
9+
describe("single api", () => {
10+
const fixturePath = path.join(__dirname, "./fixtures/single-api");
11+
12+
let cloudFormationUpdateResources;
13+
14+
beforeAll(async () => {
15+
nextBuild.default.mockResolvedValue();
16+
17+
await testableServerless(fixturePath, "package");
18+
19+
const cloudFormationUpdateTemplate = await readCloudFormationUpdateTemplate(
20+
fixturePath
21+
);
22+
23+
cloudFormationUpdateResources = cloudFormationUpdateTemplate.Resources;
24+
});
25+
26+
describe("Page lambda function", () => {
27+
let pageLambda;
28+
29+
beforeAll(() => {
30+
pageLambda = cloudFormationUpdateResources.ApiDashapiLambdaFunction;
31+
});
32+
33+
it("creates lambda resource", () => {
34+
expect(pageLambda).toBeDefined();
35+
});
36+
37+
it("has correct handler", () => {
38+
expect(pageLambda.Properties.Handler).toEqual(
39+
"sls-next-build/api/api.render"
40+
);
41+
});
42+
});
43+
44+
describe("Api Gateway", () => {
45+
let apiGateway;
46+
47+
beforeAll(() => {
48+
apiGateway = cloudFormationUpdateResources.ApiGatewayRestApi;
49+
});
50+
51+
it("creates api resource", () => {
52+
expect(apiGateway).toBeDefined();
53+
});
54+
55+
describe("Page route", () => {
56+
it("creates page route resource with correct path", () => {
57+
const apiResource = cloudFormationUpdateResources.ApiGatewayResourceApi;
58+
59+
const apiPostResource =
60+
cloudFormationUpdateResources.ApiGatewayResourceApiApi;
61+
62+
expect(apiResource).toBeDefined();
63+
expect(apiPostResource).toBeDefined();
64+
expect(apiResource.Properties.PathPart).toEqual("api");
65+
expect(apiPostResource.Properties.PathPart).toEqual("api");
66+
});
67+
68+
it("creates GET http method", () => {
69+
const httpMethod =
70+
cloudFormationUpdateResources.ApiGatewayMethodApiApiGet;
71+
72+
expect(httpMethod).toBeDefined();
73+
expect(httpMethod.Properties.HttpMethod).toEqual("GET");
74+
expect(httpMethod.Properties.ResourceId.Ref).toEqual(
75+
"ApiGatewayResourceApiApi"
76+
);
77+
});
78+
79+
it("creates HEAD http method", () => {
80+
const httpMethod =
81+
cloudFormationUpdateResources.ApiGatewayMethodApiApiHead;
82+
83+
expect(httpMethod).toBeDefined();
84+
expect(httpMethod.Properties.HttpMethod).toEqual("HEAD");
85+
expect(httpMethod.Properties.ResourceId.Ref).toEqual(
86+
"ApiGatewayResourceApiApi"
87+
);
88+
});
89+
});
90+
});
91+
92+
describe("Zip artifact", () => {
93+
let zipEntryNames;
94+
95+
beforeAll(() => {
96+
const zip = new AdmZip(
97+
`${fixturePath}/.serverless/single-api-fixture.zip`
98+
);
99+
const zipEntries = zip.getEntries();
100+
zipEntryNames = zipEntries.map(ze => ze.entryName);
101+
});
102+
103+
it("contains next compiled page", () => {
104+
expect(zipEntryNames).toContain(`sls-next-build/api/api.original.js`);
105+
});
106+
107+
it("contains plugin handler", () => {
108+
expect(zipEntryNames).toContain(`sls-next-build/api/api.js`);
109+
});
110+
});
111+
});

packages/serverless-nextjs-plugin/resources/cloudfront.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ Resources:
3030
- GET
3131
- HEAD
3232
- OPTIONS
33+
- PUT
34+
- PATCH
35+
- POST
36+
- DELETE
3337
TargetOriginId: ApiGatewayOrigin
3438
Compress: "true"
3539
ForwardedValues:

0 commit comments

Comments
 (0)