From 106e1024f23b32bbbf40738c804c9281c03c5575 Mon Sep 17 00:00:00 2001 From: KillDozerX2 Date: Mon, 20 Feb 2023 16:05:52 +0530 Subject: [PATCH 1/2] feat: :sparkles: added ci runner for mocha tests Removed dynamoose test as it's a work in progress and requires additional work --- .github/workflows/ci.pull-request.yml | 22 ++++++++++++++++++++++ package.json | 3 ++- tests/dynamoose.test.ts | 5 ----- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.pull-request.yml delete mode 100644 tests/dynamoose.test.ts diff --git a/.github/workflows/ci.pull-request.yml b/.github/workflows/ci.pull-request.yml new file mode 100644 index 0000000..0ee6e60 --- /dev/null +++ b/.github/workflows/ci.pull-request.yml @@ -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 diff --git a/package.json b/package.json index 1c702dd..69d1db4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/dynamoose.test.ts b/tests/dynamoose.test.ts deleted file mode 100644 index f373295..0000000 --- a/tests/dynamoose.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "mocha"; - -describe("Dynamoose Test Suite", () => { - it(); -}); From 6a6c10ec2f8e1034fc20b4339a0ceb28cba9e469 Mon Sep 17 00:00:00 2001 From: KillDozerX2 Date: Tue, 21 Feb 2023 12:45:12 +0530 Subject: [PATCH 2/2] feat: :sparkles: added new helper function + script to create json schema from openapi schema --- src/utils/api-gateway.ts | 129 +++++++++++++++++++++++++++++++++++++- tests/api-gateway.test.ts | 79 +++++++++++++++++++++++ 2 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 tests/api-gateway.test.ts diff --git a/src/utils/api-gateway.ts b/src/utils/api-gateway.ts index bfeec5d..652b53b 100644 --- a/src/utils/api-gateway.ts +++ b/src/utils/api-gateway.ts @@ -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"; @@ -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( @@ -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 { + const res: Exclude = { + type: "object", + required: [], + properties: {}, + additionalProperties, + }; + return res; +} + +function generateNewParameterSchema( + param: OpenAPIV3_1.ParameterObject, + existingSchema?: JSONSchema7, + additionalProperties = true +): Exclude { + const res: Exclude = { + 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; + requestBody?: OpenAPIV3_1.RequestBodyObject; +}): Exclude { + 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; +} diff --git a/tests/api-gateway.test.ts b/tests/api-gateway.test.ts new file mode 100644 index 0000000..694e5b2 --- /dev/null +++ b/tests/api-gateway.test.ts @@ -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; + } = { + 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, + }); + }); +});