Skip to content

Commit

Permalink
Add support for APIs (#117)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Deadleg authored and danielcondemarin committed Aug 14, 2019
1 parent 1989fe1 commit fe618a7
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 30 deletions.
36 changes: 34 additions & 2 deletions packages/next-aws-lambda/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ describe("next-aws-lambda", () => {
const callback = () => {};
const context = {};

// Mock due to mismatched Function types
// https://github.com/facebook/jest/issues/6329
mockRender = jest.fn();
mockDefault = jest.fn();
const page = {
render: jest.fn()
render: (...args) => mockRender(...args),
default: (...args) => mockDefault(...args)
};
const req = {};
const res = {};
Expand All @@ -22,6 +27,33 @@ describe("next-aws-lambda", () => {

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

expect(page.render).toBeCalledWith(req, res);
expect(mockRender).toBeCalledWith(req, res);
expect(mockDefault).not.toBeCalled();
});
});

describe("next-aws-lambda", () => {
it("passes request and response to next api", () => {
const event = { foo: "bar" };
const callback = () => {};
const context = {};

// Mock due to mismatched Function types
// https://github.com/facebook/jest/issues/6329
mockDefault = jest.fn();
const page = {
default: (...args) => mockDefault(...args)
};
const req = {};
const res = {};

compatLayer.mockReturnValueOnce({
req,
res
});

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

expect(mockDefault).toBeCalledWith(req, res);
});
});
8 changes: 7 additions & 1 deletion packages/next-aws-lambda/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ const reqResMapper = require("./lib/compatLayer");

const handlerFactory = page => (event, _context, callback) => {
const { req, res } = reqResMapper(event, callback);
page.render(req, res);
if (page.render instanceof Function) {
// Is a React component
page.render(req, res);
} else {
// Is an API
page.default(req, res);
}
};

module.exports = handlerFactory;
23 changes: 0 additions & 23 deletions packages/next-aws-lambda/lib/__tests__/compatLayer.request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,29 +190,6 @@ describe("compatLayer.request", () => {
]);
});

it("stream", done => {
const { req } = create({
requestContext: {
path: ""
},
headers: {}
});

let data = "";

req.on("data", chunk => {
data += chunk;
});

req.on("end", () => {
expect(data).toEqual("ok");
done();
});

req.push("ok");
req.push(null);
});

it("text body", done => {
const { req } = create({
requestContext: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,8 @@ describe("compatLayer.response", () => {
done();
}
);
req.pipe(res);
req.push("ok");
req.push(null);

res.end("ok");
});

it("base64 support", done => {
Expand Down
2 changes: 1 addition & 1 deletion packages/next-aws-lambda/lib/compatLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ const reqResMapper = (event, callback) => {
};
if (event.body) {
req.push(event.body, event.isBase64Encoded ? "base64" : undefined);
req.push(null);
}
req.push(null);

function fixApiGatewayMultipleHeaders() {
for (const key of Object.keys(response.multiValueHeaders)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default (req, res) => {{ test: 'test' }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

service: single-api-fixture

provider:
name: aws
runtime: nodejs8.10

stage: dev
region: eu-west-1

plugins:
- index # this works because jest modulePaths adds plugin path, see package.json

package:
# exclude everything
# page handlers are automatically included by the plugin
exclude:
- ./**
111 changes: 111 additions & 0 deletions packages/serverless-nextjs-plugin/__tests__/single-api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const nextBuild = require("next/dist/build");
const path = require("path");
const AdmZip = require("adm-zip");
const readCloudFormationUpdateTemplate = require("../utils/test/readCloudFormationUpdateTemplate");
const testableServerless = require("../utils/test/testableServerless");

jest.mock("next/dist/build");

describe("single api", () => {
const fixturePath = path.join(__dirname, "./fixtures/single-api");

let cloudFormationUpdateResources;

beforeAll(async () => {
nextBuild.default.mockResolvedValue();

await testableServerless(fixturePath, "package");

const cloudFormationUpdateTemplate = await readCloudFormationUpdateTemplate(
fixturePath
);

cloudFormationUpdateResources = cloudFormationUpdateTemplate.Resources;
});

describe("Page lambda function", () => {
let pageLambda;

beforeAll(() => {
pageLambda = cloudFormationUpdateResources.ApiDashapiLambdaFunction;
});

it("creates lambda resource", () => {
expect(pageLambda).toBeDefined();
});

it("has correct handler", () => {
expect(pageLambda.Properties.Handler).toEqual(
"sls-next-build/api/api.render"
);
});
});

describe("Api Gateway", () => {
let apiGateway;

beforeAll(() => {
apiGateway = cloudFormationUpdateResources.ApiGatewayRestApi;
});

it("creates api resource", () => {
expect(apiGateway).toBeDefined();
});

describe("Page route", () => {
it("creates page route resource with correct path", () => {
const apiResource = cloudFormationUpdateResources.ApiGatewayResourceApi;

const apiPostResource =
cloudFormationUpdateResources.ApiGatewayResourceApiApi;

expect(apiResource).toBeDefined();
expect(apiPostResource).toBeDefined();
expect(apiResource.Properties.PathPart).toEqual("api");
expect(apiPostResource.Properties.PathPart).toEqual("api");
});

it("creates GET http method", () => {
const httpMethod =
cloudFormationUpdateResources.ApiGatewayMethodApiApiGet;

expect(httpMethod).toBeDefined();
expect(httpMethod.Properties.HttpMethod).toEqual("GET");
expect(httpMethod.Properties.ResourceId.Ref).toEqual(
"ApiGatewayResourceApiApi"
);
});

it("creates HEAD http method", () => {
const httpMethod =
cloudFormationUpdateResources.ApiGatewayMethodApiApiHead;

expect(httpMethod).toBeDefined();
expect(httpMethod.Properties.HttpMethod).toEqual("HEAD");
expect(httpMethod.Properties.ResourceId.Ref).toEqual(
"ApiGatewayResourceApiApi"
);
});
});
});

describe("Zip artifact", () => {
let zipEntryNames;

beforeAll(() => {
const zip = new AdmZip(
`${fixturePath}/.serverless/single-api-fixture.zip`
);
const zipEntries = zip.getEntries();
zipEntryNames = zipEntries.map(ze => ze.entryName);
});

it("contains next compiled page", () => {
expect(zipEntryNames).toContain(`sls-next-build/api/api.original.js`);
});

it("contains plugin handler", () => {
expect(zipEntryNames).toContain(`sls-next-build/api/api.js`);
});
});
});
4 changes: 4 additions & 0 deletions packages/serverless-nextjs-plugin/resources/cloudfront.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Resources:
- GET
- HEAD
- OPTIONS
- PUT
- PATCH
- POST
- DELETE
TargetOriginId: ApiGatewayOrigin
Compress: "true"
ForwardedValues:
Expand Down

0 comments on commit fe618a7

Please sign in to comment.