Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Pull Request

on: [pull_request]

jobs:
build:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"semantic-release": "semantic-release",
"type-check": "tsc --noEmit",
"codegen": "json2ts -i serverless-reference/serverless/reference.json -o src/schema.ts --cwd serverless-reference/serverless",
"package": "serverless package"
"package": "serverless package",
"test": "mocha -r ts-node/register --timeout 999999 --colors ./tests/**/*.test.ts"
},
"main": "./lib/cjs/index.js",
"types": "./lib/cjs/types/index.d.ts",
Expand Down
129 changes: 128 additions & 1 deletion src/utils/api-gateway.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { List } from "ts-toolbelt";
import { FromSchema } from "json-schema-to-ts";
import { OpenAPIV3 } from "openapi-types";
import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
import { APIGatewayProxyResult } from "aws-lambda";
import { JSONSchema7 } from "json-schema-to-ts/lib/types/definitions";

type PropIdToNameMapping = {
header: "headers";
Expand Down Expand Up @@ -96,6 +97,7 @@ type AllowedHeaders = boolean | number | string;

/**
* This function is just a wrapper that builds the response that api gateway wants
* This contains all logic that we have encountered over the years and fixes issues we've run into
* TODO:- Probably might be worth adding the right type for allowed status codes
*/
export function respond(
Expand Down Expand Up @@ -141,3 +143,128 @@ export function respond(
}
return result;
}

/**
* Was having issues constantly creating base schema to later extend
* And typescript was complaining because JsonSchema7 can also be boolean.
* So created a helper function to take care of initialization
*/
function getDefaultSchema(
additionalProperties = true
): Exclude<JSONSchema7, boolean> {
const res: Exclude<JSONSchema7, boolean> = {
type: "object",
required: [],
properties: {},
additionalProperties,
};
return res;
}

function generateNewParameterSchema(
param: OpenAPIV3_1.ParameterObject,
existingSchema?: JSONSchema7,
additionalProperties = true
): Exclude<JSONSchema7, boolean> {
const res: Exclude<JSONSchema7, boolean> = {
type: "object",
required: [],
// @ts-ignore
properties: {
[param.name]: param.schema || {
type: "string",
},
},
additionalProperties,
};
if (!existingSchema) {
existingSchema = {
...res,
};
}
/**
* I was constantly running into issues for having to cast type constantly.
* So I just added a type check here and threw error in the else block
*/
if (typeof existingSchema !== "boolean") {
existingSchema = {
...existingSchema,
...res,
};
if (param.required && existingSchema.required) {
existingSchema.required.push(param.name);
}
} else {
throw Error("Schema of type boolean passed");
}
return existingSchema;
}

/**
* This function generates Json schema for the whole event.
* The event being API Gateway Event from OpenApi definition of an endpoint.
* This is very useful when you only want to maintain a single source of truth.
*/
export function generateEventSchema(pathDefinition: {
parameters?: Array<OpenAPIV3_1.ParameterObject>;
requestBody?: OpenAPIV3_1.RequestBodyObject;
}): Exclude<JSONSchema7, boolean> {
const schema = getDefaultSchema(true);
if (pathDefinition.parameters) {
pathDefinition.parameters.forEach(param => {
if (schema.properties) {
switch (param.in) {
case "query":
schema.properties.queryStringParameters =
generateNewParameterSchema(
param,
schema.properties.queryStringParameters,
false
);
break;
case "path":
schema.properties.pathParameters = generateNewParameterSchema(
param,
schema.properties.pathParameters,
false
);
break;
case "header":
schema.properties.pathParameters = generateNewParameterSchema(
param,
schema.properties.pathParameters
);
break;
}
}
});
// making path parameters and queryString parameters required if their properties are required
schema.required = [
...["pathParameters", "queryStringParameters"].filter(type => {
if (schema.properties && typeof schema.properties === "object") {
const paramDefinition = schema.properties[type];
if (typeof paramDefinition === "object") {
return (
paramDefinition.required && paramDefinition.required.length > 0
);
}
}
return false;
}),
];
}
if (pathDefinition.requestBody) {
// Make body as required since it can be optional in the definition
if (pathDefinition.requestBody.required && schema.required) {
schema.required = [...schema.required, "body"];
}
// move the request body schema into schema object
const bodySchema =
pathDefinition.requestBody.content["application/json"].schema;
if (schema.properties && bodySchema) {
// @ts-ignore
schema.properties["body"] = bodySchema;
}
}
return schema;
}
79 changes: 79 additions & 0 deletions tests/api-gateway.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { assert, expect } from "chai";
import "mocha";
import { OpenAPIV3_1 } from "openapi-types";
import { generateEventSchema } from "../src/utils/api-gateway";

describe("Json Schema Test Suite", () => {
it("Will generate valid base schema for event", () => {
const schema = generateEventSchema({});
assert.isObject(schema);
if (typeof schema === "object") {
assert.isTrue(schema.additionalProperties);
expect(schema.type).to.equal("object");
assert.isObject(schema.properties);
assert.isNotNull(schema.required);
assert.isEmpty(schema.required);
}
});
it("Will generate event schema properly for one parameter", () => {
const source: {
parameters: Array<OpenAPIV3_1.ParameterObject>;
} = {
parameters: [
{
in: "query",
name: "test",
schema: {
type: "string",
},
required: true,
},
],
};
const schema = generateEventSchema(source);
expect(schema).to.deep.equal({
type: "object",
properties: {
queryStringParameters: {
type: "object",
properties: {
test: {
type: "string",
},
},
required: ["test"],
additionalProperties: false,
},
},
required: ["queryStringParameters"],
additionalProperties: true,
});
});
it("Will generate event schema properly for requestBody", () => {
const source: {
requestBody?: OpenAPIV3_1.RequestBodyObject;
} = {
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
},
},
},
required: true,
},
};
const schema = generateEventSchema(source);
expect(schema).to.deep.equal({
type: "object",
properties: {
body: {
type: "object",
},
},
required: ["body"],
additionalProperties: true,
});
});
});
5 changes: 0 additions & 5 deletions tests/dynamoose.test.ts

This file was deleted.