Skip to content

Commit

Permalink
add edge-api-swagger
Browse files Browse the repository at this point in the history
  • Loading branch information
joshbalfour committed Oct 24, 2023
1 parent 0d5226b commit 8fc6767
Show file tree
Hide file tree
Showing 17 changed files with 759 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@reapit-cdk/generate-readme": "workspace:^",
"@reapit-cdk/integration-tests": "workspace:^",
"@types/jest": "^29.5.5",
"@types/swagger-ui-dist": "^3.30.3",
"aws-cdk": "2.100.0",
"aws-sdk-client-mock": "^3.0.0",
"aws-sdk-client-mock-jest": "^3.0.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/constructs/edge-api-swagger/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src
tests
.eslintrc.js
tsconfig.json
53 changes: 53 additions & 0 deletions packages/constructs/edge-api-swagger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@reapit-cdk/edge-api-swagger",
"version": "0.0.0",
"description": "Add a swagger endpoint to your EdgeAPI",
"homepage": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/constructs/edge-api-swagger",
"readme": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/constructs/edge-api-swagger/readme.md",
"bugs": {
"url": "https://github.com/reapit/ts-cdk-constructs/issues"
},
"license": "MIT",
"author": {
"name": "Josh Balfour",
"email": "jbalfour@reapit.com"
},
"repository": {
"url": "https://github.com/reapit/ts-cdk-constructs.git"
},
"scripts": {
"build": "reapit-cdk-tsup",
"check": "yarn run root:check -p $(pwd)",
"lint": "reapit-cdk-eslint",
"test": "yarn run root:test -- $(pwd)",
"prepack": "reapit-version-package && yarn build",
"integ": "yarn run root:integ -- $(pwd)",
"jsii:build": "rpt-cdk-jsii",
"jsii:publish": "rpt-cdk-jsii --publish"
},
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"main": "dist/index.js",
"types": "dist/index.d.ts"
},
"dependencies": {
"openapi3-ts": "^4.1.2",
"swagger-ui-dist": "^5.9.0",
"typescript": "^5.1.3"
},
"peerDependencies": {
"@reapit-cdk/edge-api": "workspace:^",
"aws-cdk-lib": "^2.96.2",
"constructs": "^10.2.70"
},
"devDependencies": {
"@reapit-cdk/eslint-config": "workspace:^",
"@reapit-cdk/integration-tests": "workspace:^",
"@reapit-cdk/jsii": "workspace:^",
"@reapit-cdk/tsup": "workspace:^",
"@reapit-cdk/version-package": "workspace:^",
"aws-cdk-lib": "^2.96.2",
"constructs": "^10.2.70"
}
}
31 changes: 31 additions & 0 deletions packages/constructs/edge-api-swagger/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# @reapit-cdk/active-ruleset


![npm version](https://img.shields.io/npm/v/@reapit-cdk/active-ruleset)
![npm downloads](https://img.shields.io/npm/dm/@reapit-cdk/active-ruleset)
![coverage: 99.02%25](https://img.shields.io/badge/coverage-99.02%25-green)
![Integ Tests: ✔](https://img.shields.io/badge/Integ%20Tests-%E2%9C%94-green)

This construct returns the currently active SES receipt RuleSet, or creates one. This enables you to add rules to it.

## Package Installation:

```sh
yarn add --dev @reapit-cdk/active-ruleset
# or
npm install @reapit-cdk/active-ruleset --save-dev
```

## Usage
```ts
import { CfnOutput, Stack, App } from 'aws-cdk-lib'
import { ActiveRuleset } from '@reapit-cdk/active-ruleset'

const app = new App()
const stack = new Stack(app, 'stack-name')
const activeRuleset = new ActiveRuleset(stack, 'active-ruleset')
new CfnOutput(stack, 'activeRulesetName', {
value: activeRuleset.receiptRuleSet.receiptRuleSetName,
})

```
114 changes: 114 additions & 0 deletions packages/constructs/edge-api-swagger/src/edge-api-swagger-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { EdgeAPI, FrontendEndpoint, endpointIsLambdaEndpoint, endpointIsProxyEndpoint } from '@reapit-cdk/edge-api'
import { Construct } from 'constructs'

import { Bucket } from 'aws-cdk-lib/aws-s3'
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'
import { RemovalPolicy } from 'aws-cdk-lib'
import { generateOpenAPIDocs } from './generation/generate-openapi-docs'
import { EndpointInputItem } from './generation'
import { getAbsoluteFSPath } from 'swagger-ui-dist'
import { InfoObject } from 'openapi3-ts/oas30'

interface EdgeAPISwaggerEndpointProps {
api: EdgeAPI
url: string
pathPattern?: string
info?: InfoObject
}

const swaggerHtml = (urlPrefix: string) => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="SwaggerUI"
/>
<title>SwaggerUI</title>
<link rel="stylesheet" href="${urlPrefix}/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="${urlPrefix}/swagger-ui-bundle.js" crossorigin></script>
<script src="${urlPrefix}/swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: '${urlPrefix}/openapi.json',
dom_id: '#swagger-ui',
});
};
</script>
</body>
</html>`

export class EdgeAPISwaggerEndpoint extends Construct implements FrontendEndpoint {
bucket: Bucket
invalidationItems: string[]
pathPattern: string

constructor(scope: Construct, id: string, props: EdgeAPISwaggerEndpointProps) {
super(scope, id)
this.pathPattern = props.pathPattern ?? '/swagger'

const destinationKeyPrefix = this.pathPattern.replace(/^\/+/g, '')

this.bucket = new Bucket(this, 'bucket', {
websiteIndexDocument: 'index.html',
websiteErrorDocument: destinationKeyPrefix + '/index.html',
removalPolicy: RemovalPolicy.RETAIN, // otherwise deletion will fail on stack destroy due to non-empty bucket
publicReadAccess: true,
blockPublicAccess: {
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
},
})

const openapiJson = generateOpenAPIDocs({
url: props.url,
info: props.info,
endpointsInput: props.api._endpoints.map((endpoint): EndpointInputItem => {
if (endpointIsLambdaEndpoint(endpoint)) {
return {
codePath: endpoint.lambda.codePath,
pathPattern: endpoint.pathPattern,
isFrontend: false,
}
}
if (endpointIsProxyEndpoint(endpoint)) {
return {
isProxy: true,
pathPattern: endpoint.pathPattern,
proxyDestination: endpoint.destination,
}
}
return {
pathPattern: endpoint.pathPattern,
isFrontend: true,
}
}),
})

new BucketDeployment(this, 'deployment', {
sources: [
Source.data('index.html', swaggerHtml(`${props.url}/${destinationKeyPrefix}`)),
Source.jsonData('openapi.json', openapiJson),
Source.asset(getAbsoluteFSPath()),
],
destinationBucket: this.bucket,
destinationKeyPrefix,
retainOnDelete: false,
})

this.invalidationItems = [
'/index.html',
'/openapi.json',
'/swagger-ui-bundle.js',
'/swagger-ui-standalone-preset.js',
'/swagger-ui.css',
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
ContentObject,
InfoObject,
OpenApiBuilder,
PathItemObject,
RequestBodyObject,
ResponseObject,
SchemaObject,
SchemaObjectType,
} from 'openapi3-ts/oas30'
import { EndpointHandlerInfo, ResolvedProperty, ReturnType } from './types'

export const endpointHandlersToOpenApi = (
endpointHandlersInfo: EndpointHandlerInfo[],
url: string,
info?: InfoObject,
) => {
const openApi = new OpenApiBuilder()
openApi.addInfo({
title: 'Edge API',
version: '1.0.0',
...(info ?? {}),
})
openApi.addServer({
url,
})

endpointHandlersInfo.map(transformEndpointHandler).forEach(({ path, pathItem }) => openApi.addPath(path, pathItem))

return openApi
}

const stringIsSchemaObjectType = (str: string): str is SchemaObjectType => {
return ['integer', 'number', 'string', 'boolean', 'array'].includes(str)
}

const propertyToSchema = (property: ResolvedProperty): SchemaObject => {
const type = property.typeName && stringIsSchemaObjectType(property.typeName) ? property.typeName : 'object'
const properties = property.properties?.reduce(
(pv, cv) => {
if (!cv.name) {
return pv
}
return {
...pv,
[cv.name]: propertyToSchema(cv),
}
},
{} as Record<string, SchemaObject>,
)

return {
type,
required: property.properties
? (property.properties
.filter((rp) => !rp.isOptional)
.map((rp) => rp.name)
.filter(Boolean) as string[])
: undefined,
properties,
}
}

const transformBody = (body: ResolvedProperty, isForm?: boolean): RequestBodyObject => {
const contentType = isForm ? 'application/x-www-form-urlencoded' : 'application/json'
const content: ContentObject = {
[contentType]: {
schema: propertyToSchema(body),
},
}
return { content }
}

const returnTypeToResponse = (returnType?: ReturnType): ResponseObject => {
if (!returnType) {
return {
description: 'not typed',
}
}

if (returnType.isRedirection) {
return {
description: 'redirection',
headers: {
location: {
schema: {
type: 'string',
example: 'https://google.com',
},
},
status: {
schema: {
type: 'number',
example: 302,
},
},
},
}
}

if (returnType.isJSONResponse && returnType.response) {
return {
description: 'default',
content: {
'application/json': {
schema: propertyToSchema(returnType.response),
},
},
}
}

return {
description: 'invalid return type',
}
}

const transformEndpointHandler = (endpointInfo: EndpointHandlerInfo): { path: string; pathItem: PathItemObject } => {
const { bodyType, returnType, isFormRequestHandler, description } = endpointInfo
if (bodyType) {
return {
path: endpointInfo.pathPattern,
pathItem: {
post: {
description,
responses: {
default: returnTypeToResponse(returnType),
},
requestBody: transformBody(bodyType, isFormRequestHandler),
},
},
}
}

return {
path: endpointInfo.pathPattern,
pathItem: {
get: {
description,
responses: {
default: returnTypeToResponse(returnType),
},
},
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { InfoObject } from 'openapi3-ts/oas30'
import { getEndpointHandlerInfo, endpointHandlersToOpenApi, EndpointsInput } from '.'

export const generateOpenAPIDocs = ({
endpointsInput,
url,
info,
}: {
endpointsInput: EndpointsInput
url: string
info?: InfoObject
}) => {
const endpointHandlerInfo = getEndpointHandlerInfo(endpointsInput)
return endpointHandlersToOpenApi(endpointHandlerInfo, url, info).getSpec()
}
Loading

0 comments on commit 8fc6767

Please sign in to comment.