├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── example └── reusable-tls-certificate │ ├── README.md │ ├── cdk.json │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── tslint.json ├── hoc ├── .eslintrc.js ├── .npmignore ├── README.md ├── doc │ └── api.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── config.ts │ ├── gateway.ts │ ├── hoc.ts │ ├── index.ts │ └── staticweb.ts ├── test │ ├── config.spec.ts │ ├── gateway.spec.ts │ └── staticweb.spec.ts └── tsconfig.json └── pure ├── .eslintrc.js ├── .npmignore ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── index.ts ├── test ├── iaac.spec.ts ├── join.spec.ts ├── map.spec.ts ├── root.spec.ts └── use.spec.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | example/foobar/ 2 | node_modules/ 3 | lib/ 4 | coverage/ 5 | cdk.out/ 6 | cdk.context.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: node_js 4 | node_js: 5 | - 12 6 | - 10 7 | 8 | script: 9 | - cd ./pure 10 | - npm install 11 | - npm run lint 12 | - npm run test 13 | 14 | - cd ../hoc 15 | - npm install 16 | - npm run lint 17 | - npm run test 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Dmitry Kolesnikov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 17 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | rel: 3 | @cd pure \ 4 | && rm -Rf node_modules || true \ 5 | && rm -f package-lock.json || true \ 6 | && ncu -u \ 7 | && npm install \ 8 | && npm version patch \ 9 | && npm publish \ 10 | && cd - \ 11 | && git add pure 12 | @cd hoc \ 13 | && rm -Rf node_modules || true \ 14 | && rm -f package-lock.json || true \ 15 | && ncu -u \ 16 | && npm install \ 17 | && npm version patch \ 18 | && npm publish \ 19 | && cd - \ 20 | && git add hoc 21 | @git commit -m "update deps" 22 | @git push 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-cdk-pure 2 | 3 | The library is a toolkit for development of **high-order** and **purely functional** components with [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html). 4 | 5 | [![Build Status](https://secure.travis-ci.org/fogfish/aws-cdk-pure.svg?branch=master)](http://travis-ci.org/fogfish/aws-cdk-pure) 6 | [![Git Hub](https://img.shields.io/github/last-commit/fogfish/aws-cdk-pure.svg)](http://travis-ci.org/fogfish/aws-cdk-pure) 7 | [![npm](https://img.shields.io/npm/v/aws-cdk-pure?label=pure)](https://www.npmjs.com/package/aws-cdk-pure) 8 | [![npm](https://img.shields.io/npm/v/aws-cdk-pure-hoc?label=hoc)](https://www.npmjs.com/package/aws-cdk-pure-hoc) 9 | 10 | 11 | ## Inspiration 12 | 13 | The purely functional extension to AWS CDK and has been inspired by the following posts 14 | * [Composable Cloud Components with AWS CDK](https://i.am.fog.fish/2019/07/28/composable-cloud-components-with-aws-cdk.html) 15 | * [Purely Functional Cloud Components with AWS CDK](https://i.am.fog.fish/2019/08/23/purely-functional-cloud-with-aws-cdk.html). 16 | 17 | `aws-cdk-pure` is an utility for design and development of purely functional and higher-order components. You know React Hooks! Think of it as **hooks for your cloud infrastructure**. 18 | 19 | 20 | ## Getting Started 21 | 22 | This repository implements TypeScript libraries for cloud development. Please see its details and guidelines in corresponding README.md files 23 | 24 | * [aws-cdk-pure](pure) - a core part of the toolkit. It defines types and functional primitives required for hight-order components development. It maintain a slim dependencies towards other library, only `@aws-cdk/core` is used. 25 | 26 | * [aws-cdk-pure-hoc](hoc) - implements reusable purely functional high-order components. These components are building blocks and design patterns for your cloud infrastructure. 27 | 28 | 29 | ## How To Contribute 30 | 31 | The library is [MIT](LICENSE) licensed and accepts contributions via GitHub pull requests: 32 | 33 | 1. Fork it 34 | 2. Create your feature branch (`git checkout -b my-new-feature`) 35 | 3. Commit your changes (`git commit -am 'Added some feature'`) 36 | 4. Push to the branch (`git push origin my-new-feature`) 37 | 5. Create new Pull Request 38 | 39 | The development requires TypeScript and AWS CDK 40 | 41 | ```bash 42 | npm install -g typescript ts-node aws-cdk 43 | ``` 44 | 45 | ```bash 46 | git clone https://github.com/fogfish/aws-cdk-pure 47 | cd aws-cdk-pure 48 | 49 | ## cd either to pure or hoc folder 50 | 51 | npm install 52 | npm run test 53 | npm run lint 54 | npm run build 55 | ``` 56 | 57 | ## License 58 | 59 | [![See LICENSE](https://img.shields.io/github/license/fogfish/aws-cdk-pure.svg?style=for-the-badge)](LICENSE) 60 | -------------------------------------------------------------------------------- /example/reusable-tls-certificate/README.md: -------------------------------------------------------------------------------- 1 | # Reusable TLS Certificate 2 | 3 | This application shows the possible solution on the TLS certificate issue depicted in the blog post: 4 | 5 | [How To Fix Error About Limits of TLS Certificates That Caused by AWS CDK](https://i.am.fog.fish/2020/03/18/how-to-fix-error-about-limits-of-tls-certificates-that-caused-by-aws-cdk.html) 6 | 7 | 8 | A frequent deployment of AWS CDK application might cause an error 9 | 10 | ``` 11 | Error: you have reached your limit of 20 certificates in the last year. 12 | ``` 13 | 14 | The root cause of the error is an automation on TLS Certificate. The following CDK code requests a new certificate from AWS Certificate Manager. 15 | 16 | ```ts 17 | import * as acm from '@aws-cdk/aws-certificatemanager' 18 | 19 | const cert = new acm.DnsValidatedCertificate(this, 'Cert', { domainName, hostedZone }) 20 | ``` 21 | 22 | This app uses cross-stack references to demonstrates the re-use of certificates in actions. 23 | 24 | ``` 25 | cdk deploy -c domain=example.com -c subdomain=myapp application 26 | ``` 27 | 28 | The usage of cross-stack references allows you to deploy a TLS certificate only once using AWS CDK IaC principles. You are not limited with redeployment cycles of actual application. You can safely enforce continuous deployment workflow for your application without any issue with AWS Certificate Manager quotas. 29 | -------------------------------------------------------------------------------- /example/reusable-tls-certificate/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "ts-node src/index", 3 | "requireApproval": "never" 4 | } 5 | -------------------------------------------------------------------------------- /example/reusable-tls-certificate/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: [ 3 | "**/__tests__/**/*.+(ts|tsx|js)", 4 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 5 | ], 6 | "transform": { 7 | "^.+\\.(ts|tsx)$": "ts-jest" 8 | }, 9 | } -------------------------------------------------------------------------------- /example/reusable-tls-certificate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reusable-tls-certificate", 3 | "version": "0.0.0", 4 | "description": "Example of tls certificate limit solution", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "tsc --noEmit", 8 | "lint": "tslint -p tsconfig.json", 9 | "test": "jest --coverage" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/fogfish/aws-cdk-pure.git" 16 | }, 17 | "dependencies": { 18 | "@types/node": "16.11.1", 19 | "aws-cdk-pure": "^1.3.22", 20 | "aws-cdk-pure-hoc": "^1.7.31", 21 | "@aws-cdk/core": "*", 22 | "@aws-cdk/aws-apigateway": "*", 23 | "@aws-cdk/aws-secretsmanager": "*", 24 | "@aws-cdk/assert": "*" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^27.0.2", 28 | "jest": "^27.3.1", 29 | "ts-jest": "^27.0.7", 30 | "prettier": "^2.4.1", 31 | "ts-node": "^10.3.0", 32 | "tslint": "^6.1.3", 33 | "tslint-config-prettier": "^1.18.0", 34 | "typescript": "^4.4.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/reusable-tls-certificate/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core' 2 | import * as pure from 'aws-cdk-pure' 3 | import * as hoc from 'aws-cdk-pure-hoc' 4 | 5 | // 6 | // Global configuration, the tls certificates requires few parameters 7 | // * root domain of the site (e.g. example.com). It must correspond to 8 | // existing entity in AWS Hosted Zone 9 | // * subdomain of web-site or api (e.g. www | api) 10 | // 11 | const app = new cdk.App() 12 | const stack = { 13 | env: { 14 | account: process.env.CDK_DEFAULT_ACCOUNT, 15 | region: process.env.CDK_DEFAULT_REGION, 16 | } 17 | } 18 | const subdomain = String(app.node.tryGetContext('subdomain')) 19 | const domain = String(app.node.tryGetContext('domain')) 20 | 21 | // 22 | // Common stack creates a certificate and exports its. 23 | // The stack is deployed only once during life-cycle of application. 24 | // 25 | const tlsCertificate = pure.join( 26 | new cdk.Stack(app, 'common', stack), 27 | hoc.common.HostedZone(domain).flatMap( 28 | zone => hoc.common.Certificate(`*.${domain}`, zone) 29 | ), 30 | ).certificateArn 31 | 32 | // 33 | // Application stack deploy application resource, which depends on 34 | // existing tlsCertificate. The application stack can be destroyed/deployed 35 | // unlimited amount of time. It would not cause re-create of certificate. 36 | // 37 | pure.join( 38 | new cdk.Stack(app, 'application', stack), 39 | hoc.staticweb.Gateway({ 40 | domain, 41 | subdomain, 42 | tlsCertificate, 43 | sites: [{origin: '', site: 'api'}], 44 | }), 45 | ) 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/reusable-tls-certificate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "outDir": "./lib", 7 | "declaration": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization":false 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "test"] 24 | } 25 | -------------------------------------------------------------------------------- /example/reusable-tls-certificate/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name" : [true, "never-prefix"] 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /hoc/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', 5 | ], 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }, 10 | "rules": { 11 | "semi": "off", 12 | "@typescript-eslint/semi": ["off"], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hoc/.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | src 3 | test 4 | node_modules 5 | tsconfig.json 6 | tslint.json 7 | coverage 8 | jest.config.js 9 | -------------------------------------------------------------------------------- /hoc/README.md: -------------------------------------------------------------------------------- 1 | # aws-cdk-pure-hoc 2 | 3 | This is a TypeScript library implements reusable cloud design patterns as purely functional high-order components. Use them as building blocks for your cloud infrastructure. 4 | 5 | ## Getting started 6 | 7 | The latest version of the library is available at its `master` branch. All development, including new features and bug fixes, take place on the `master` branch using forking and pull requests as described in contribution guidelines. The latest package release is available at npm 8 | 9 | ```bash 10 | npm install --save aws-cdk-pure-hoc 11 | ``` 12 | 13 | ## High Order Components 14 | 15 | The library implements a cloud design patterns using high order components. See the **api specification** and usage snippets at [doc folder](doc/api.md): 16 | 17 | * [config](https://i.am.fog.fish/2019/10/18/retain-confidentiality-in-open-source-infrastructure.html) - The twelve-factor application principles advices environment variables to store the config. The HoC `config` gives you a secure approach to retain confidentiality of your configuration. It is an environment variables managed through Key Vault service. 18 | 19 | * [staticweb](doc/api.md) - a serverless implementation of [static web design pattern](https://aws.amazon.com/getting-started/projects/host-static-website/) using either AWS CloudFront or AWS Gateway API. 20 | 21 | * [gateway](doc/api.md) - configures AWS API Gateway with your custom domain name and proper TLS certificate. 22 | 23 | * [actor](doc/api.md) - AWS Lambda Worked backed with SQS Queue 24 | 25 | 26 | ## License 27 | 28 | [![See LICENSE](https://img.shields.io/github/license/fogfish/aws-cdk-pure.svg?style=for-the-badge)](LICENSE) 29 | -------------------------------------------------------------------------------- /hoc/doc/api.md: -------------------------------------------------------------------------------- 1 | # High Order Infrastructure Components 2 | 3 | * [config](#config) 4 | * [staticweb](#staticweb) 5 | * [gateway](#gateway) 6 | 7 | 8 | ## config 9 | 10 | It gives you a secure approach to retain confidentiality of your configuration by substituting an environment variables with Key Vault service, such as AWS Secret Manager. 11 | 12 | ```typescript 13 | import { config } from 'aws-cdk-pure-hoc' 14 | 15 | config.String('MySecretStore', 'MyConfigKey').flatMap( 16 | (value: string) => /* value is a CloudFormation reference to your secret */ 17 | ) 18 | ``` 19 | 20 | 21 | ## staticweb 22 | 23 | Serverless implementation of Static WebSite using AWS S3 with HTTPS (TLS) support. The HoC implements two approaches using AWS CloudFront or AWS Gateway API. 24 | 25 | The CloudFront-based Static Web is deployed to `us-east-1` only but do not worry about latencies, it has an excellent [global coverage](https://aws.amazon.com/cloudfront/features/). 26 | 27 | ```typescript 28 | import { staticweb } from 'aws-cdk-pure-hoc' 29 | 30 | const site = staticweb.CloudFront({ 31 | domain: 'example.com', 32 | subdomain: 'www', 33 | }) 34 | 35 | const Stack = (): cdk.StackProps => ({ 36 | env: { 37 | account: process.env.CDK_DEFAULT_ACCOUNT, 38 | region: 'us-east-1' 39 | } 40 | }) 41 | const stack = pure.iaac(cdk.Stack)(Stack).effect(x => pure.join(x, site)) 42 | const app = new cdk.App() 43 | pure.join(app, stack) 44 | app.synth() 45 | ``` 46 | 47 | The API Gateway-base Static Web is deployed to any region. It works best if you need to couple delivery of static web and api within same deployment. 48 | 49 | ```typescript 50 | import { staticweb } from 'aws-cdk-pure-hoc' 51 | 52 | const site = staticweb.Gateway({ 53 | domain: 'example.com', 54 | subdomain: 'www', 55 | siteRoot: 'api/myapp' // https://www.example.com/api/myapp static site endpoint 56 | }).effect( 57 | (x: RestApi) => /* add other methods to rest api here */ 58 | ) 59 | ``` 60 | 61 | Please note, the static binary file handling requires special [configuration](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html) to the gateway. HoC uses `binaryMediaTypes` property to specify a list of binary content types. The default list is sufficient for single page react application. 62 | 63 | 64 | ## gateway 65 | 66 | HoC deploys AWS API Gateway with TLS and DNS configurations, which makes api usable with your custom domain name and proper TLS certificate. 67 | 68 | ```typescript 69 | import { gateway } from 'aws-cdk-pure-hoc' 70 | 71 | const api = gateway.Api({ 72 | domain: 'example.com', 73 | subdomain: 'www', 74 | siteRoot: 'api/myapp' // https://www.example.com/api/myapp static site endpoint 75 | }).effect( 76 | (x: RestApi) => /* add other methods to rest api here */ 77 | ) 78 | ``` 79 | 80 | HoC implements a helper function to defined [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policy 81 | 82 | ```typescript 83 | const api = gateway.Api({/* ... */}) 84 | .effect(x => { 85 | gateway.CORS(x.root.addResource('test')) 86 | }) 87 | ``` 88 | -------------------------------------------------------------------------------- /hoc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: [ 3 | "**/__tests__/**/*.+(ts|tsx|js)", 4 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 5 | ], 6 | "transform": { 7 | "^.+\\.(ts|tsx)$": "ts-jest" 8 | }, 9 | } -------------------------------------------------------------------------------- /hoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cdk-pure-hoc", 3 | "version": "1.7.31", 4 | "description": "Purely Functional High Order Cloud Components with AWS CDK", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "lint": "eslint lib/*.ts", 10 | "test": "jest --coverage", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm run test && npm run lint", 13 | "clean": "rm -Rf lib && rm -Rf node_modules" 14 | }, 15 | "keywords": [ 16 | "aws", 17 | "cdk", 18 | "pure", 19 | "functional", 20 | "infrastructure-as-code", 21 | "hoc", 22 | "high-order components" 23 | ], 24 | "author": "", 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/fogfish/aws-cdk-pure.git" 29 | }, 30 | "dependencies": { 31 | "@aws-cdk/assert": "*", 32 | "@aws-cdk/aws-apigateway": "*", 33 | "@aws-cdk/aws-certificatemanager": "*", 34 | "@aws-cdk/aws-cloudfront": "*", 35 | "@aws-cdk/aws-iam": "*", 36 | "@aws-cdk/aws-lambda-event-sources": "*", 37 | "@aws-cdk/aws-route53": "*", 38 | "@aws-cdk/aws-route53-targets": "*", 39 | "@aws-cdk/aws-s3": "*", 40 | "@aws-cdk/aws-secretsmanager": "*", 41 | "@aws-cdk/core": "*", 42 | "@types/node": "16.11.1", 43 | "aws-cdk-pure": "*" 44 | }, 45 | "devDependencies": { 46 | "@types/jest": "^27.0.2", 47 | "@typescript-eslint/eslint-plugin": "^5.1.0", 48 | "@typescript-eslint/parser": "^5.1.0", 49 | "eslint": "^8.0.1", 50 | "eslint-config-prettier": "^8.3.0", 51 | "eslint-plugin-prettier": "^4.0.0", 52 | "jest": "^27.3.1", 53 | "prettier": "^2.4.1", 54 | "ts-jest": "^27.0.7", 55 | "ts-node": "^10.3.0", 56 | "typescript": "^4.4.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /hoc/src/config.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | // Config/Secret Management HoC 9 | // 10 | import * as secret from '@aws-cdk/aws-secretsmanager' 11 | import { IaaC, include, IPure } from 'aws-cdk-pure' 12 | 13 | const defaultBucket = process.env.AWS_IAAC_CONFIG || 'undefined' 14 | const vault = include(secret.Secret.fromSecretAttributes) 15 | 16 | /** 17 | * returns a configuration as string value for given key as it is stored by AWS Secret Manager 18 | * 19 | * @param key name of the key 20 | * @param bucket AWS Secret Manager bucket, the value of AWS_IAAC_CONFIG env var is used as default bucket, 21 | */ 22 | export function String(key: string, bucket: string = defaultBucket): IPure { 23 | return vault(Config(bucket)).map(x => x.secretValueFromJson(key).toString()) 24 | } 25 | 26 | function Config(secretArn: string): IaaC { 27 | const Secret = () => ({ secretArn }) 28 | return Secret 29 | } 30 | -------------------------------------------------------------------------------- /hoc/src/gateway.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | // API Gateway HoC 9 | // 10 | import * as api from '@aws-cdk/aws-apigateway' 11 | import * as acm from '@aws-cdk/aws-certificatemanager' 12 | import * as dns from '@aws-cdk/aws-route53' 13 | import * as target from '@aws-cdk/aws-route53-targets' 14 | import * as cdk from '@aws-cdk/core' 15 | import * as pure from 'aws-cdk-pure' 16 | import * as hoc from './hoc' 17 | 18 | export interface GatewayProps { 19 | /** 20 | * root domain of the site (e.g. example.com). 21 | * It shall correspond to AWS Hosted Zone config 22 | */ 23 | readonly domain: string 24 | 25 | /** 26 | * subdomain of web-site (e.g. www | api) 27 | */ 28 | readonly subdomain?: string 29 | 30 | /** 31 | * The api path visible as a prefix, default is `api` 32 | */ 33 | readonly siteRoot?: string 34 | 35 | /** 36 | * The identity (arn) of certificate used for the site 37 | */ 38 | readonly tlsCertificate?: string 39 | 40 | /** 41 | * CORS specification 42 | */ 43 | readonly corsPreflightOptions?: api.CorsOptions 44 | } 45 | 46 | /** 47 | * AWS API Gateway with DNS and TLS configuration, returns RestApi 48 | * The HoC function creates 49 | * - TLS Certificate 50 | * - AWS Gateway API 51 | * - DNS Configuration 52 | * 53 | * The site is defined as `subdomain.domain` (e.g. www.example.com) 54 | */ 55 | export function Api(props: GatewayProps): pure.IPure { 56 | const zone = hoc.HostedZone(props.domain) 57 | 58 | return pure.use({ zone }) 59 | .flatMap(x => ({ cert: hoc.Certificate(site(props), x.zone, props.tlsCertificate) })) 60 | .flatMap(x => ({ gateway: Gateway(props, x.cert) })) 61 | .flatMap(x => ({ dns: GatewayDNS(props, x.zone, x.gateway) })) 62 | .yield('gateway') 63 | } 64 | 65 | function Gateway(props: GatewayProps, certificate: acm.ICertificate): pure.IPure { 66 | const iaac = pure.iaac(api.RestApi) 67 | const fqdn = site(props) 68 | const GW = { 69 | [fqdn]: (): api.RestApiProps => ({ 70 | deploy: true, 71 | deployOptions: { 72 | stageName: stage(props), 73 | }, 74 | domainName: { 75 | certificate, 76 | domainName: site(props), 77 | }, 78 | endpointTypes: [api.EndpointType.REGIONAL], 79 | failOnWarnings: true, 80 | defaultCorsPreflightOptions: props.corsPreflightOptions, 81 | }) 82 | } 83 | return iaac(GW[fqdn]) 84 | } 85 | 86 | // 87 | function GatewayDNS(props: GatewayProps, zone: dns.IHostedZone, restapi: api.RestApi): pure.IPure { 88 | const iaac = pure.iaac(dns.ARecord) 89 | const ApiDNS = (): dns.ARecordProps => ({ 90 | recordName: site(props), 91 | target: {aliasTarget: new target.ApiGateway(restapi)}, 92 | ttl: cdk.Duration.seconds(60), 93 | zone, 94 | }) 95 | return iaac(ApiDNS) 96 | } 97 | 98 | // 99 | function site(props: GatewayProps): string { 100 | return (props.subdomain) ? `${props.subdomain}.${props.domain}` : props.domain 101 | } 102 | 103 | // 104 | function stage(props: GatewayProps): string { 105 | return props.siteRoot ? props.siteRoot.split('/')[0] : 'api' 106 | } 107 | 108 | export interface AccessControl { 109 | /** 110 | * See Access-Control-Allow-Methods 111 | * default GET, POST, PUT, DELETE, OPTIONS 112 | */ 113 | method?: string[] 114 | 115 | /** 116 | * See Access-Control-Allow-Headers 117 | * default Content-Type, Authorization, Accept, Origin 118 | */ 119 | header?: string[] 120 | 121 | /** 122 | * Access-Control-Allow-Origin 123 | * default * 124 | */ 125 | origin?: string 126 | } 127 | 128 | /** 129 | * add CORS handler to RestApi endpoint 130 | */ 131 | export const CORS = (endpoint: api.Resource, props: AccessControl = {}): api.Resource => { 132 | const mthd = `'${props.method ? props.method.join(',') : 'GET,POST,PUT,DELETE,OPTIONS'}'` 133 | const head = `'${props.header ? props.header.join(',') : 'Content-Type,Authorization,Accept,Origin'}'` 134 | const origin = `'${props.origin || '*'}'` 135 | const mock = new api.MockIntegration({ 136 | integrationResponses: [ 137 | { 138 | responseParameters: { 139 | "method.response.header.Access-Control-Allow-Headers": head, 140 | "method.response.header.Access-Control-Allow-Methods": mthd, 141 | "method.response.header.Access-Control-Allow-Origin": origin, 142 | "method.response.header.Access-Control-Max-Age": "'600'", 143 | }, 144 | selectionPattern: '\\d{3}', 145 | statusCode: '200', 146 | }, 147 | ], 148 | passthroughBehavior: api.PassthroughBehavior.WHEN_NO_MATCH, 149 | requestTemplates: { 150 | "application/json": "{\"statusCode\": 200}" 151 | }, 152 | }) 153 | const method = { 154 | methodResponses: [ 155 | { 156 | responseModels: { 157 | "application/json": new api.EmptyModel(), 158 | }, 159 | responseParameters: { 160 | "method.response.header.Access-Control-Allow-Headers": true, 161 | "method.response.header.Access-Control-Allow-Methods": true, 162 | "method.response.header.Access-Control-Allow-Origin": true, 163 | "method.response.header.Access-Control-Max-Age": true, 164 | }, 165 | statusCode: '200', 166 | }, 167 | ], 168 | } 169 | endpoint.addMethod('OPTIONS', mock, method) 170 | return endpoint 171 | } 172 | 173 | -------------------------------------------------------------------------------- /hoc/src/hoc.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | // Common HoC 9 | import * as acm from '@aws-cdk/aws-certificatemanager' 10 | import * as dns from '@aws-cdk/aws-route53' 11 | import * as lambda from '@aws-cdk/aws-lambda' 12 | import * as cdk from '@aws-cdk/core' 13 | import * as pure from 'aws-cdk-pure' 14 | import * as sys from 'child_process' 15 | 16 | // 17 | // Lookup AWS Route 53 hosted zone for the domain 18 | export function HostedZone(domainName: string): pure.IPure { 19 | const awscdkIssue4592 = (parent: cdk.Construct, id: string, props: dns.HostedZoneProviderProps): dns.IHostedZone => ( 20 | dns.HostedZone.fromLookup(parent, id, props) 21 | ) 22 | const iaac = pure.include(awscdkIssue4592) // dns.HostedZone.fromLookup 23 | const SiteHostedZone = (): dns.HostedZoneProviderProps => ({ domainName }) 24 | return iaac(SiteHostedZone) 25 | } 26 | 27 | // 28 | // Issues AWS TLS Certificate for the domain 29 | export function Certificate(site: string, hostedZone: dns.IHostedZone, arn?: string): pure.IPure { 30 | if (arn) { 31 | const wrap = pure.include(acm.Certificate.fromCertificateArn) 32 | const SiteCA = (): string => arn 33 | return wrap(SiteCA) 34 | } else { 35 | const iaac = pure.iaac(acm.DnsValidatedCertificate) 36 | const SiteCA = (): acm.DnsValidatedCertificateProps => ({ domainName: site, hostedZone }) 37 | return iaac(SiteCA) 38 | } 39 | } 40 | 41 | // 42 | // Bundles Golang Lambda function from source 43 | export function AssetCodeGo(path: string): lambda.Code { 44 | return new lambda.AssetCode('', { bundling: gocc(path) }) 45 | } 46 | 47 | const tryBundle = (outputDir: string, options: cdk.BundlingOptions): boolean => { 48 | if (!options || !options.workingDirectory) { 49 | return false 50 | } 51 | 52 | const pkg = options.workingDirectory.split('/go/src/').join('') 53 | // tslint:disable-next-line:no-console 54 | console.log(`==> go build ${pkg}`) 55 | sys.execSync(`GOCACHE=/tmp/go.amd64 GOOS=linux GOARCH=amd64 go build -o ${outputDir}/main ${pkg}`) 56 | return true 57 | } 58 | 59 | const gocc = (path: string): cdk.BundlingOptions => { 60 | const gopath = process.env.GOPATH || '/go' 61 | const fnpath = path.split(gopath).join('') 62 | 63 | return { 64 | local: { tryBundle }, 65 | image: cdk.BundlingDockerImage.fromRegistry('golang'), 66 | command: ["go", "build", "-o", `${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/main`], 67 | user: 'root', 68 | environment: { 69 | "GOCACHE": "/go/cache", 70 | }, 71 | volumes: [ 72 | { 73 | containerPath: '/go/src', 74 | hostPath: `${gopath}/src`, 75 | }, 76 | ], 77 | workingDirectory: `/go${fnpath}`, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /hoc/src/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | export import common = require('./hoc') 9 | export import config = require('./config') 10 | export import gateway = require('./gateway') 11 | export import staticweb = require('./staticweb') 12 | -------------------------------------------------------------------------------- /hoc/src/staticweb.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | // Static Web HoC 9 | // 10 | import * as api from '@aws-cdk/aws-apigateway' 11 | import * as acm from '@aws-cdk/aws-certificatemanager' 12 | import * as cdn from '@aws-cdk/aws-cloudfront' 13 | import * as iam from '@aws-cdk/aws-iam' 14 | import * as dns from '@aws-cdk/aws-route53' 15 | import * as target from '@aws-cdk/aws-route53-targets' 16 | import * as s3 from '@aws-cdk/aws-s3' 17 | import * as cdk from '@aws-cdk/core' 18 | import * as pure from 'aws-cdk-pure' 19 | import * as hoc from './hoc' 20 | 21 | type DomainName = string 22 | export interface StaticSiteProps { 23 | /** 24 | * root domain of the site (e.g. example.com). 25 | * It shall correspond to AWS Hosted Zone config 26 | */ 27 | readonly domain: DomainName 28 | 29 | /** 30 | * subdomain of web-site (e.g. www | api) 31 | */ 32 | readonly subdomain?: string 33 | 34 | /** 35 | * The identity (arn) of certificate used for the site 36 | */ 37 | readonly tlsCertificate?: string 38 | 39 | /** 40 | * associates path at origin and site prefix 41 | * - origin path is the absolute path at s3 42 | * - site path is the path prefix visible at site url 43 | */ 44 | readonly sites?: {origin: string, site: string}[] 45 | 46 | /** 47 | * List of binary media types, default 48 | * - application/octet-stream 49 | * - binary/octet-stream 50 | * - image/png 51 | * - image/x-icon 52 | * - font/woff2 53 | */ 54 | readonly binaryMediaTypes?: string[] 55 | } 56 | 57 | /****************************************************************************** 58 | * 59 | * Static WebSite with AWS CloudFront 60 | * 61 | ******************************************************************************/ 62 | 63 | /** 64 | * static web site hosting using AWS CloudFront, returns CloudFrontWebDistribution 65 | * The HoC function creates 66 | * - WebSite public S3 bucket 67 | * - TLS Certificate 68 | * - AWS Cloud Front dedicated for your site 69 | * - DNS Configuration 70 | * 71 | * The site is defined as `subdomain.domain` (e.g. www.example.com) 72 | */ 73 | export function CloudFront(props: StaticSiteProps): pure.IPure { 74 | const zone = hoc.HostedZone(props.domain) 75 | const origin = Origin(props) 76 | 77 | return pure.use({ zone, origin }) 78 | .flatMap(x => ({ cert: hoc.Certificate(site(props), x.zone, props.tlsCertificate) })) 79 | .flatMap(x => ({ cdn: CDN(props, x.cert.certificateArn, x.origin) })) 80 | .flatMap(x => ({ dns: CloudFrontDNS(props, x.zone, x.cdn) })) 81 | .yield('cdn') 82 | } 83 | 84 | // 85 | function CDN(props: StaticSiteProps, acmCertRef: string, s3BucketSource: s3.IBucket): pure.IPure { 86 | const iaac = pure.iaac(cdn.CloudFrontWebDistribution) 87 | const baseOrigin = { 88 | behaviors : [ 89 | { 90 | defaultTtl: cdk.Duration.hours(24), 91 | forwardedValues: {queryString: true}, 92 | isDefaultBehavior: true, 93 | maxTtl: cdk.Duration.hours(24), 94 | minTtl: cdk.Duration.seconds(0), 95 | } 96 | ], 97 | s3OriginSource: { 98 | s3BucketSource 99 | }, 100 | } 101 | const rootOrigin = (!props.sites || props.sites.length === 0) ? baseOrigin : { originPath: props.sites[0].origin, ...baseOrigin } 102 | 103 | const SiteCDN = (): cdn.CloudFrontWebDistributionProps => ({ 104 | aliasConfiguration: { 105 | acmCertRef, 106 | names: [ site(props) ], 107 | securityPolicy: cdn.SecurityPolicyProtocol.TLS_V1_2_2018, 108 | sslMethod: cdn.SSLMethod.SNI, 109 | }, 110 | httpVersion: cdn.HttpVersion.HTTP1_1, 111 | originConfigs: [ rootOrigin ] 112 | }) 113 | return iaac(SiteCDN) 114 | } 115 | 116 | // 117 | function CloudFrontDNS(props: StaticSiteProps, zone: dns.IHostedZone, cloud: cdn.CloudFrontWebDistribution): pure.IPure { 118 | const iaac = pure.iaac(dns.ARecord) 119 | const SiteDNS = (): dns.ARecordProps => ({ 120 | recordName: site(props), 121 | target: {aliasTarget: new target.CloudFrontTarget(cloud)}, 122 | ttl: cdk.Duration.seconds(60), 123 | zone, 124 | }) 125 | return iaac(SiteDNS) 126 | } 127 | 128 | /****************************************************************************** 129 | * 130 | * Static WebSite with API Gateway 131 | * 132 | ******************************************************************************/ 133 | 134 | /** 135 | * static web site hosting using AWS API Gateway, returns RestApi 136 | * The HoC function creates 137 | * - WebSite public S3 bucket 138 | * - TLS Certificate 139 | * - AWS Gateway API 140 | * - DNS Configuration 141 | * 142 | * The site is defined as `subdomain.domain` (e.g. www.example.com) 143 | */ 144 | export function Gateway(props: StaticSiteProps): pure.IPure { 145 | const zone = hoc.HostedZone(props.domain) 146 | const origin = Origin(props, false) 147 | 148 | let gateway = pure.use({ zone, origin }) 149 | .flatMap(x => ({ cert: hoc.Certificate(site(props), x.zone, props.tlsCertificate) })) 150 | .flatMap(x => ({ role: OriginAccessPolicy(x.origin) })) 151 | .flatMap(x => ({ gateway: SiteGateway(props, x.cert) })) 152 | .flatMap(x => ({ dns: GatewayDNS(props, x.zone, x.gateway) })) 153 | 154 | if (props.sites) { 155 | props.sites.forEach(spec => 156 | gateway = gateway.flatMap( 157 | x => ({[spec.origin]: StaticContent(x.origin, x.role, x.gateway, spec)}) 158 | ) 159 | ) 160 | } 161 | 162 | return gateway.yield('gateway') 163 | } 164 | 165 | function SiteGateway(props: StaticSiteProps, certificate: acm.ICertificate): pure.IPure { 166 | const iaac = pure.iaac(api.RestApi) 167 | const fqdn = site(props) 168 | const GW = { 169 | [fqdn]: (): api.RestApiProps => ({ 170 | binaryMediaTypes: MediaTypes(props), 171 | deploy: true, 172 | deployOptions: { 173 | stageName: (props.sites && props.sites.length > 0) ? props.sites[0].site.split('/')[0] : 'api' 174 | }, 175 | domainName: { 176 | certificate, 177 | domainName: site(props), 178 | }, 179 | endpointTypes: [api.EndpointType.REGIONAL], 180 | failOnWarnings: true, 181 | }) 182 | } 183 | return iaac(GW[fqdn]) 184 | } 185 | 186 | function MediaTypes(props: StaticSiteProps): string[] { 187 | if (!props.binaryMediaTypes) { 188 | return [ 189 | "application/octet-stream", 190 | "binary/octet-stream", 191 | "image/png", 192 | "image/x-icon", 193 | "image/vnd.microsoft.icon", 194 | "font/woff2", 195 | ] 196 | } 197 | return props.binaryMediaTypes 198 | } 199 | 200 | function OriginAccessPolicy(origin: s3.IBucket): pure.IaaC { 201 | const role = pure.iaac(iam.Role) 202 | const SiteRole = (): iam.RoleProps => ({ 203 | assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') 204 | }) 205 | 206 | const ReadOnly = (): iam.PolicyStatement => ( 207 | new iam.PolicyStatement({ 208 | actions: ['s3:GetObject'], 209 | resources: [`${origin.bucketArn}/*`], 210 | }) 211 | ) 212 | 213 | return role(SiteRole).effect(x => x.addToPolicy(ReadOnly())) 214 | } 215 | 216 | function StaticContent( 217 | origin: s3.IBucket, 218 | role: iam.IRole, 219 | gw: api.RestApi, 220 | root: {origin: string, site: string} 221 | ): pure.IPure { 222 | const iaac = pure.wrap(api.AwsIntegration) 223 | const content = (path: string) => (): api.AwsIntegrationProps => ({ 224 | integrationHttpMethod: 'GET', 225 | options: { 226 | credentialsRole: role, 227 | integrationResponses: [ 228 | { 229 | selectionPattern: 'default', 230 | statusCode: '500', 231 | }, 232 | { 233 | responseParameters: { 234 | "method.response.header.Content-Length": "integration.response.header.Content-Length", 235 | "method.response.header.Content-Type": "integration.response.header.Content-Type", 236 | "method.response.header.Cache-Control": "integration.response.header.Cache-Control", 237 | }, 238 | selectionPattern: '2\\d{2}', 239 | statusCode: '200', 240 | }, 241 | { 242 | selectionPattern: '404', 243 | statusCode: '404', 244 | }, 245 | { 246 | selectionPattern: '4\\d{2}', 247 | statusCode: '403', 248 | } 249 | ], 250 | passthroughBehavior: api.PassthroughBehavior.WHEN_NO_TEMPLATES, 251 | requestParameters: { 252 | "integration.request.path.key": "method.request.path.key" 253 | }, 254 | }, 255 | path, 256 | service: 's3', 257 | }) 258 | const SiteContent = content([origin.bucketName, root.origin, '{key}'].join('/')) 259 | const SiteDefault = content([origin.bucketName, root.origin, 'index.html'].join('/')) 260 | 261 | const spec = { 262 | methodResponses: [ 263 | { 264 | responseParameters: { 265 | "method.response.header.Content-Length": true, 266 | "method.response.header.Content-Type": true, 267 | "method.response.header.Cache-Control": true, 268 | }, 269 | statusCode: '200', 270 | }, 271 | {statusCode: '403'}, 272 | {statusCode: '404'}, 273 | {statusCode: '500'}, 274 | ], 275 | requestParameters: { 276 | "method.request.path.key": true 277 | }, 278 | } 279 | 280 | const segments = root.site.split('/').slice(1) 281 | return pure.use({ content: iaac(SiteContent), default: iaac(SiteDefault) }) 282 | .effect(x => { 283 | const p = segments.reduce( 284 | (acc, seg) => acc.getResource(seg) || acc.addResource(seg), gw.root) 285 | p.addMethod('GET', x.default, spec) 286 | p.addResource('{key+}').addMethod('GET', x.content, spec) 287 | }) 288 | .yield('content') 289 | } 290 | 291 | // 292 | function GatewayDNS(props: StaticSiteProps, zone: dns.IHostedZone, restapi: api.RestApi): pure.IPure { 293 | const iaac = pure.iaac(dns.ARecord) 294 | const SiteDNS = (): dns.ARecordProps => ({ 295 | recordName: site(props), 296 | target: {aliasTarget: new target.ApiGateway(restapi)}, 297 | ttl: cdk.Duration.seconds(60), 298 | zone, 299 | }) 300 | return iaac(SiteDNS) 301 | } 302 | 303 | /****************************************************************************** 304 | * 305 | * Static WebSite Common Components 306 | * 307 | ******************************************************************************/ 308 | 309 | // 310 | // 311 | function site(props: StaticSiteProps): string { 312 | return (props.subdomain) ? `${props.subdomain}.${props.domain}` : props.domain 313 | } 314 | 315 | // 316 | // AWS S3 Bucket for Static Site(s) 317 | function Origin(props: StaticSiteProps, publicReadAccess: boolean = true): pure.IPure { 318 | const iaac = pure.iaac(s3.Bucket) 319 | const SiteS3 = (): s3.BucketProps => ({ 320 | bucketName: site(props), 321 | publicReadAccess, 322 | removalPolicy: cdk.RemovalPolicy.DESTROY, 323 | websiteErrorDocument: 'error.html', 324 | websiteIndexDocument: 'index.html', 325 | }) 326 | return iaac(SiteS3) 327 | } 328 | -------------------------------------------------------------------------------- /hoc/test/config.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as pure from 'aws-cdk-pure' 9 | import * as cdk from '@aws-cdk/core' 10 | import { config } from '../src/index' 11 | 12 | function Config(): pure.IaaC { 13 | return config.String('key', 'arn:aws:secretsmanager:eu-west-1:000000000000:secret:bucket').flatMap(Component) 14 | } 15 | 16 | function Component(type: string): pure.IaaC { 17 | const cf = pure.iaac(cdk.CfnResource) 18 | const MyA = (): cdk.CfnResourceProps => ({ type }) 19 | return cf(MyA) 20 | } 21 | 22 | it('config implements IPure interface', 23 | () => { 24 | const c = config.String('key', 'bucket') 25 | expect( c.effect ) 26 | expect( c.map ) 27 | expect( c.flatMap ) 28 | } 29 | ) 30 | 31 | it('fetch config from secret manager', 32 | () => { 33 | const app = new cdk.App() 34 | const Stack = (): cdk.StackProps => ({ env: {} }) 35 | pure.join(app, 36 | pure.iaac(cdk.Stack)(Stack).effect(x => pure.join(x, Config)) 37 | ) 38 | const stack = app.synth().getStack('Stack') 39 | expect(stack.template).toEqual( 40 | { 41 | Resources: { 42 | MyA: { 43 | Type: '{{resolve:secretsmanager:arn:aws:secretsmanager:eu-west-1:000000000000:secret:bucket:SecretString:key::}}' 44 | } 45 | } 46 | } 47 | ) 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /hoc/test/gateway.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as assert from '@aws-cdk/assert' 9 | import * as api from '@aws-cdk/aws-apigateway' 10 | import * as pure from 'aws-cdk-pure' 11 | import * as cdk from '@aws-cdk/core' 12 | import { gateway } from '../src/index' 13 | 14 | it('gateway.Api implements IPure interface', 15 | () => { 16 | const c = gateway.Api({domain: 'example.com', subdomain: 'www'}) 17 | expect( c.effect ).toBeDefined() 18 | expect( c.map ).toBeDefined() 19 | expect( c.flatMap ).toBeDefined() 20 | } 21 | ) 22 | 23 | it('build gateway.Api', 24 | () => { 25 | const app = new cdk.App() 26 | const stack = new cdk.Stack(app, 'Stack', { 27 | env: { account: '000000000000', region: 'us-east-1'} 28 | }) 29 | 30 | // Note: mock is required to pass the test 31 | // API GW cannot be created w/o any methods 32 | const gw = gateway 33 | .Api({domain: 'example.com', subdomain: 'www', siteRoot: 'api/a/b/c/d'}) 34 | .effect(x => { 35 | x.root.addResource('test').addMethod('GET', new api.MockIntegration({})) 36 | }) 37 | pure.join(stack, gw) 38 | 39 | const elements = [ 40 | 'AWS::IAM::Policy', 41 | 'AWS::Lambda::Function', 42 | 'AWS::CloudFormation::CustomResource', 43 | 'AWS::ApiGateway::RestApi', 44 | 'AWS::ApiGateway::Deployment', 45 | 'AWS::ApiGateway::Stage', 46 | 'AWS::ApiGateway::DomainName', 47 | 'AWS::Route53::RecordSet', 48 | ] 49 | elements.forEach(x => assert.expect(stack).to(assert.haveResource(x))); 50 | } 51 | ) 52 | 53 | it('define CORS policy', 54 | () => { 55 | const app = new cdk.App() 56 | const stack = new cdk.Stack(app, 'Stack', { 57 | env: { account: '000000000000', region: 'us-east-1'} 58 | }) 59 | 60 | const gw = gateway 61 | .Api({domain: 'example.com', subdomain: 'www', siteRoot: 'api/a/b/c/d'}) 62 | .effect(x => { 63 | gateway.CORS(x.root.addResource('test')) 64 | }) 65 | pure.join(stack, gw) 66 | 67 | const elements = [ 68 | 'AWS::IAM::Policy', 69 | 'AWS::Lambda::Function', 70 | 'AWS::CloudFormation::CustomResource', 71 | 'AWS::ApiGateway::RestApi', 72 | 'AWS::ApiGateway::Deployment', 73 | 'AWS::ApiGateway::Stage', 74 | 'AWS::ApiGateway::DomainName', 75 | 'AWS::Route53::RecordSet', 76 | ] 77 | elements.forEach(x => assert.expect(stack).to(assert.haveResource(x))); 78 | } 79 | ) 80 | -------------------------------------------------------------------------------- /hoc/test/staticweb.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as assert from '@aws-cdk/assert' 9 | import * as pure from 'aws-cdk-pure' 10 | import * as cdk from '@aws-cdk/core' 11 | import { staticweb } from '../src/index' 12 | 13 | it('staticweb.CloudFront implements IPure interface', 14 | () => { 15 | const c = staticweb.CloudFront({domain: 'example.com', subdomain: 'www'}) 16 | expect( c.effect ).toBeDefined() 17 | expect( c.map ).toBeDefined() 18 | expect( c.flatMap ).toBeDefined() 19 | } 20 | ) 21 | 22 | it('build Static Web Site with AWS CloudFront', 23 | () => { 24 | const app = new cdk.App() 25 | const stack = new cdk.Stack(app, 'Stack', { 26 | env: { account: '000000000000', region: 'us-east-1'} 27 | }) 28 | pure.join(stack, 29 | staticweb.CloudFront({domain: 'example.com', subdomain: 'www'}) 30 | ) 31 | 32 | const elements = [ 33 | 'AWS::S3::Bucket', 34 | 'AWS::IAM::Policy', 35 | 'AWS::Lambda::Function', 36 | 'AWS::CloudFormation::CustomResource', 37 | 'AWS::CloudFront::Distribution', 38 | 'AWS::Route53::RecordSet', 39 | ] 40 | elements.forEach(x => assert.expect(stack).to(assert.haveResource(x))); 41 | } 42 | ) 43 | 44 | it('staticweb.Gateway implements IPure interface', 45 | () => { 46 | const c = staticweb.Gateway({domain: 'example.com', subdomain: 'www'}) 47 | expect( c.effect ).toBeDefined() 48 | expect( c.map ).toBeDefined() 49 | expect( c.flatMap ).toBeDefined() 50 | } 51 | ) 52 | 53 | it('build Static Web Site with AWS API Gateway', 54 | () => { 55 | const app = new cdk.App() 56 | const stack = new cdk.Stack(app, 'Stack', { 57 | env: { account: '000000000000', region: 'us-east-1'} 58 | }) 59 | pure.join(stack, 60 | staticweb.Gateway({domain: 'example.com', subdomain: 'www', sites: [{origin: '', site: 'api/a/b/c/d'}]}) 61 | ) 62 | 63 | const elements = [ 64 | 'AWS::S3::Bucket', 65 | 'AWS::IAM::Policy', 66 | 'AWS::Lambda::Function', 67 | 'AWS::CloudFormation::CustomResource', 68 | 'AWS::ApiGateway::RestApi', 69 | 'AWS::ApiGateway::Deployment', 70 | 'AWS::ApiGateway::Stage', 71 | 'AWS::ApiGateway::Resource', 72 | 'AWS::ApiGateway::Method', 73 | 'AWS::ApiGateway::DomainName', 74 | 'AWS::Route53::RecordSet', 75 | ] 76 | elements.forEach(x => assert.expect(stack).to(assert.haveResource(x))); 77 | } 78 | ) 79 | 80 | it('build Multiple Static Web Site with AWS API Gateway', 81 | () => { 82 | const app = new cdk.App() 83 | const stack = new cdk.Stack(app, 'Stack', { 84 | env: { account: '000000000000', region: 'us-east-1'} 85 | }) 86 | pure.join(stack, 87 | staticweb.Gateway({ 88 | domain: 'example.com', 89 | subdomain: 'www', 90 | sites: [ 91 | {origin: 'd', site: 'api/a/b/c/d'}, 92 | {origin: 'e', site: 'api/a/b/c/e'}, 93 | ] 94 | }) 95 | ) 96 | 97 | const elements = [ 98 | 'AWS::S3::Bucket', 99 | 'AWS::IAM::Policy', 100 | 'AWS::Lambda::Function', 101 | 'AWS::CloudFormation::CustomResource', 102 | 'AWS::ApiGateway::RestApi', 103 | 'AWS::ApiGateway::Deployment', 104 | 'AWS::ApiGateway::Stage', 105 | 'AWS::ApiGateway::Resource', 106 | 'AWS::ApiGateway::Method', 107 | 'AWS::ApiGateway::DomainName', 108 | 'AWS::Route53::RecordSet', 109 | ] 110 | elements.forEach(x => assert.expect(stack).to(assert.haveResource(x))); 111 | } 112 | ) 113 | -------------------------------------------------------------------------------- /hoc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "outDir": "./lib", 7 | "declaration": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization":false 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "test"] 24 | } 25 | -------------------------------------------------------------------------------- /pure/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', 5 | ], 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }, 10 | "rules": { 11 | "semi": "off", 12 | "@typescript-eslint/semi": ["off"], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pure/.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | src 3 | test 4 | node_modules 5 | tsconfig.json 6 | tslint.json 7 | coverage 8 | jest.config.js 9 | -------------------------------------------------------------------------------- /pure/README.md: -------------------------------------------------------------------------------- 1 | # aws-cdk-pure 2 | 3 | This is a TypeScript toolkit library for development of purely functional and high-order cloud components with AWS CDK. 4 | 5 | 6 | ## Inspiration 7 | 8 | The library is an open-source extension to AWS CDK. It has been inspired by the following posts: 9 | * [Composable Cloud Components with AWS CDK](https://i.am.fog.fish/2019/07/28/composable-cloud-components-with-aws-cdk.html) 10 | * [Purely Functional Cloud Components with AWS CDK](https://i.am.fog.fish/2019/08/23/purely-functional-cloud-with-aws-cdk.html). 11 | 12 | `aws-cdk-pure` is an utility for design and development of purely functional and higher-order components. You know React Hooks! Think of it as **hooks for your cloud infrastructure**. 13 | 14 | 15 | ## Getting started 16 | 17 | The latest version of the library is available at its `master` branch. All development, including new features and bug fixes, take place on the `master` branch using forking and pull requests as described in contribution guidelines. The latest package release is available at npm 18 | 19 | ```bash 20 | npm install --save aws-cdk-pure 21 | ``` 22 | 23 | ## Key features 24 | 25 | AWS Cloud Development Kit do not implement a pure functional approach. The abstraction of cloud resources is exposed using class hierarchy, each class represents a "cloud component" and encapsulates everything AWS CloudFormation needs to create the component. A shift from category of classes to category of pure functions simplifies the development by **scraping boilerplate**. A pure function component of type `IaaC` is a right approach to express semantic of Infrastructure as a Code. Please check the details about the library design considerations [here](https://i.am.fog.fish/2019/08/23/purely-functional-cloud-with-aws-cdk.html). 26 | 27 | 28 | 29 | ### Cloud Formation Stacks and Resources 30 | 31 | The library defines a core types `IaaC | IPure`. They express semantic of Infrastructure as a Code. 32 | 33 | ```typescript 34 | type IaaC = (scope: cdk.Construct) => T 35 | ``` 36 | 37 | Purely functional semantic defines `root` operator. It attaches the pure stack components to the root of CDK application. Then, `join` operator attaches the pure definition of resource to the graph nodes. The logical name of the attached resources is defined by the name of a function. 38 | 39 | ```typescript 40 | import { root, join, IaaC } from 'aws-cdk-pure' 41 | 42 | function RestApi(): cdk.Construct {/* ... */} 43 | 44 | function Storage(): cdk.Construct {/* ... */} 45 | 46 | function CodeBuildBot(stack: cdk.Construct): cdk.Construct { 47 | join(stack, RestApi) 48 | join(stack, Storage) 49 | return stack 50 | } 51 | 52 | const app = new cdk.App() 53 | root(app, CodeBuildBot) 54 | app.synth() 55 | ``` 56 | 57 | 58 | ### Pure Functional Cloud Resource 59 | 60 | The original class-based semantic of AWS CDK defines a common constructor pattern for cloud resources, which takes a scope of the graph, logical name and properties of component. 61 | 62 | ```typescript 63 | type Node = new (scope: Construct, id: string, props: Prop) => Type 64 | ``` 65 | 66 | An overhead exists in class-based approach of resource definition. Firstly, the duplication of logical name - name of function and literal constant. Secondly, we can observe that category of cloud resource is bi-parted graph. The left side is “cloud components”, the right side is they properties (e.g. `Function <-> FunctionProps`). It is possible to infer a type of “cloud components” by type of its property and visa verse using ad-hoc polymorphism. 67 | 68 | Purely functional semantic defines `iaac` operator - type safe factory. It takes a class constructor of “cloud component” as input and returns another function, which builds a type-safe association between “cloud component” and its property (see `type Node`). 69 | 70 | ```typescript 71 | import { join, iaac } from 'aws-cdk-pure' 72 | 73 | const lambda = iaac(Function) 74 | 75 | function WebHook(): FunctionProps { 76 | return { 77 | runtime: lambda.Runtime.NODEJS_10_X, 78 | code: new AssetCode(/* ... */), 79 | /* ... */ 80 | } 81 | } 82 | 83 | join(stack, lambda(WebHook)) 84 | ``` 85 | 86 | In addition to `iaac` operator, the library support other type safe factories such as `wrap` and `include`. The `warp` lifts AWS CDK integrations and targets to pure functional domain. 87 | 88 | ```typescript 89 | import { wrap } from 'aws-cdk-pure' 90 | 91 | const integrate = wrap(LambdaIntegration) 92 | 93 | const method = integrate(lambda(WebHook)) 94 | restapi.root.addResource('test').addMethod('GET', method) 95 | ``` 96 | 97 | The `include` allows to import existing CloudFormation resources to the stack 98 | 99 | ```typescript 100 | import { include } from 'aws-cdk-pure' 101 | 102 | const vpc = include(ec2.Vpc.fromVpcAttributes) 103 | 104 | function Vpc(): VpcAttributes { 105 | return { 106 | vpcId: /* ... */ 107 | } 108 | } 109 | 110 | vpc(Vpc) 111 | ``` 112 | 113 | 114 | ### Runtime Customization of Cloud Resources 115 | 116 | Often, a property of cloud resources needs to be resolved at runtime. You can do it with named closures. 117 | 118 | ```typescript 119 | function WebHook(code: AssetCode): IaaC { 120 | const WebHook = (): FunctionProps => ({ 121 | runtime: lambda.Runtime.NODEJS_10_X, 122 | code, 123 | /* ... */ 124 | }) 125 | return lambda(WebHook) 126 | } 127 | 128 | join(stack, WebHook(new AssetCode(/* ... */))) 129 | ``` 130 | 131 | Another typical use-case is naming of cloud resources after some runtime variables, e.g. name AWS Gateway API instances after domain name. 132 | 133 | ```typescript 134 | function ApiGateway(domain: string): IaaC { 135 | const gateway = iaac(RestApi) 136 | const Gateway = {[domain]: (): RestApiProps => ({/* ...*/})} 137 | 138 | return gateway(Gateway[domain]) 139 | } 140 | ``` 141 | 142 | ### Comprehension 143 | 144 | The library works with `IaaC` category. There is a challenge to use this type with native AWS CDK API. For example, the code fails to compile - addMethod requires an Integration type but `IaaC` is provided. 145 | 146 | ```typescript 147 | const api = restapi(MyApi) 148 | const method = integration(lambda(MyFunction)) 149 | 150 | api.root.addResource('test').addMethod('GET', method) 151 | ``` 152 | 153 | Purely functional semantic resolves this using functional abstractions (e.g. functions and monads). 154 | The type `IPure` supertype of `IaaC` implements functions to apply effects on inner type. 155 | 156 | ```typescript 157 | const lambda: IPure = iaac(Function)(WebHook) 158 | 159 | const result: IPure = lambda.effect( 160 | (x: Function): void => /* applies a side effect to inner type */ 161 | ) 162 | 163 | const result: IPure = lambda.map( 164 | (x: Function): LambdaIntegration => /* transforms a type from A to B */ 165 | ) 166 | 167 | const result: IPure = lambda.flatMap( 168 | (x: Function): IPure => /* transforms a type from A to IaaC */ 169 | ) 170 | 171 | // 172 | // you solves above problem with flatMap 173 | restapi(MyApi) 174 | .flatMap(api => 175 | integration(lambda(MyFunction)).flatMap(method => { 176 | api.root.addResource('test').addMethod('GET', method) 177 | return api 178 | }) 179 | ) 180 | ``` 181 | 182 | Then, the library provides you convenient type-safe approach to deal with product of `IPure` types. The product type is a structure where each member is a `IPure` component (e.g. `{[K in keyof T]: IaaC}`). 183 | 184 | ```typescript 185 | // 186 | // product of `IPure` component 187 | const api = restapi(MyApi) 188 | const method = integration(lambda(MyFunction)) 189 | const product = { api, method } 190 | ``` 191 | 192 | The library implements a helper function `use` to lift a product of components to `IPure` type where you can use various composers. 193 | 194 | ```typescript 195 | use({/* product of IPure */}) 196 | .effect((x: {/* product of Ts */}): void => /* applies a side effect to inner type */) 197 | .flatMap((x: {/* product of Ts */}): {/* product of Tx */} => /* new product of IPure */ ) 198 | .yield(name) // yields back a component 199 | 200 | // 201 | // you solves above problem 202 | use({ api, method }) 203 | .effect(x => x.api.root.addResource('test').addMethod('GET', x.method)) 204 | .yield('api') 205 | ``` 206 | 207 | ### Cross Stack References 208 | 209 | Often, it is required to create resource in one stack and pass its reference to another one. 210 | 211 | ```typescript 212 | const root = new cdk.Stack(/* ... */) 213 | const shared = pure.join(root, MySharedResource()) 214 | 215 | const other = new cdk.Stack(/* ... */) 216 | pure.join(other, MyResource(shared)) 217 | ``` 218 | 219 | ## Example 220 | 221 | You cloud code will looks like following snippet with this library. 222 | Check out [aws-pure-cdk-hoc](../hoc) and [serverless code build bot](https://github.com/fogfish/code-build-bot) for example and reference implementations. 223 | 224 | ```typescript 225 | import { IaaC, root, join, use, iaac, wrap } from 'aws-cdk-pure' 226 | 227 | // 228 | // pure Lambda 229 | function WebHook(): lambda.FunctionProps { 230 | return { 231 | runtime: lambda.Runtime.NODEJS_10_X, 232 | code: new lambda.AssetCode(...), 233 | ... 234 | } 235 | } 236 | 237 | // 238 | // pure API Gateway 239 | function Gateway(): api.RestApiProps { 240 | return { 241 | endpointTypes: [api.EndpointType.REGIONAL], 242 | ... 243 | } 244 | } 245 | 246 | // 247 | // HoC RestApi 248 | function RestApi(): IaaC { 249 | const restapi = iaac(RestApi)(Gateway) 250 | const lambda = iaac(Function)(WebHook) 251 | const webhook = wrap(LambdaIntegration)(lambda) 252 | 253 | return use({ restapi, webhook }) 254 | .effect(x => x.restapi.root.addResource('webhook').addMethod('POST', x.webhook)) 255 | .yield('restapi') 256 | } 257 | 258 | // 259 | // pure stack 260 | const app = new cdk.App() 261 | const Stack = (): cdk.StackProps => ({ env: {} }) 262 | pure.join(app, 263 | pure.iaac(cdk.Stack)(Stack).effect(x => pure.join(x, RestApi)) 264 | ) 265 | app.synth() 266 | ``` 267 | 268 | ## Other libraries 269 | 270 | You've might head about [Punchcard](https://github.com/sam-goodwin/punchcard) 271 | > Punchcard adds to the vision by unifying infrastructure code with runtime code, meaning you can both declare resources and implement logic within one node.js application. 272 | 273 | This library is not looking for unification of infrastructure and business logic code with this pure functional extension to AWS CDK. Instead, it promotes diversity of runtime of your lambda functions. It just shifts IaaC development paradigm from category of classes to category of pure functions. 274 | 275 | 276 | ## License 277 | 278 | [![See LICENSE](https://img.shields.io/github/license/fogfish/aws-cdk-pure.svg?style=for-the-badge)](LICENSE) 279 | -------------------------------------------------------------------------------- /pure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: [ 3 | "**/__tests__/**/*.+(ts|tsx|js)", 4 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 5 | ], 6 | "transform": { 7 | "^.+\\.(ts|tsx)$": "ts-jest" 8 | }, 9 | } -------------------------------------------------------------------------------- /pure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cdk-pure", 3 | "version": "1.3.22", 4 | "description": "Purely Functional Cloud Components with AWS CDK", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "lint": "eslint lib/*.ts", 10 | "test": "jest --coverage", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm run test && npm run lint", 13 | "clean": "rm -Rf lib && rm -Rf node_modules" 14 | }, 15 | "keywords": [ 16 | "aws", 17 | "cdk", 18 | "pure", 19 | "functional", 20 | "infrastructure-as-code" 21 | ], 22 | "author": "", 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/fogfish/aws-cdk-pure.git" 27 | }, 28 | "dependencies": { 29 | "@aws-cdk/core": "*", 30 | "@types/node": "16.11.1" 31 | }, 32 | "devDependencies": { 33 | "@types/jest": "^27.0.2", 34 | "@typescript-eslint/eslint-plugin": "^5.1.0", 35 | "@typescript-eslint/parser": "^5.1.0", 36 | "eslint": "^8.0.1", 37 | "eslint-config-prettier": "^8.3.0", 38 | "eslint-plugin-prettier": "^4.0.0", 39 | "jest": "^27.3.1", 40 | "prettier": "^2.4.1", 41 | "ts-jest": "^27.0.7", 42 | "ts-node": "^10.3.0", 43 | "typescript": "^4.4.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pure/src/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import { App, Construct, Stack } from '@aws-cdk/core' 9 | 10 | // 11 | // 12 | export type IaaC = (parent: Construct) => A 13 | 14 | export interface IPure { 15 | (parent: Construct): A 16 | effect: (f: (x: A) => void) => IPure 17 | map: (f: (x: A) => B) => IPure 18 | flatMap: (f: (x: A) => IaaC) => IPure 19 | } 20 | 21 | /** 22 | * Lifts IaaC type to IPure interface 23 | */ 24 | export function unit(f: IaaC): IPure { 25 | const pure: IPure = f as IPure 26 | 27 | pure.effect = (eff: (x: A) => void) => 28 | unit( 29 | (scope: any) => { 30 | const node = f(scope) 31 | eff(node) 32 | return node 33 | } 34 | ) 35 | 36 | pure.map = (fmap: (x: A) => B) => 37 | unit((scope: any) => fmap(f(scope))) 38 | 39 | pure.flatMap = (fmap: (x: A) => IaaC) => 40 | unit((scope: any) => fmap(f(scope))(scope)) 41 | 42 | return pure 43 | } 44 | 45 | 46 | // 47 | // 48 | type Node = new (scope: Construct, id: string, props: Prop) => Type 49 | 50 | /** 51 | * type safe cloud component factory. It takes a class constructor of "cloud component" 52 | * as input and returns another function, which builds a type-safe association between 53 | * "cloud component" and its property. 54 | * 55 | * @param f "cloud component" class constructor 56 | * @param pure purely functional definition of the component 57 | */ 58 | export function iaac(f: Node): (pure: IaaC, name?: string) => IPure { 59 | return (pure, name) => unit( 60 | (scope) => new f(scope, name || pure.name, pure(scope)) 61 | ) 62 | } 63 | 64 | // 65 | // 66 | type Wrap = new (scope: TypeA, props?: Prop) => TypeB 67 | 68 | /** 69 | * type safe cloud component factory for integrations 70 | * 71 | * @param f "cloud component" class constructor 72 | * @param pure purely functional definition of the component 73 | */ 74 | export function wrap(f: Wrap): (pure: IaaC) => IPure { 75 | return (pure) => unit( 76 | (scope) => new f(pure(scope)) 77 | ) 78 | } 79 | 80 | // 81 | // 82 | type Include = (scope: Construct, id: string, props: Prop) => Type 83 | 84 | /** 85 | * type safe cloud component factory. It takes a fromXXX lookup function of "cloud component" 86 | * as input and returns another function, which builds a type-safe association between 87 | * "cloud component" and its property. 88 | * 89 | * @param f lookup function 90 | * @param pure purely functional definition of the component 91 | */ 92 | export function include(f: Include): (pure: IaaC, name?: string) => IPure { 93 | return (pure, name) => unit( 94 | (scope) => f(scope, name || pure.name, pure(scope)) 95 | ) 96 | } 97 | 98 | // 99 | // 100 | type Product = {[K in keyof T]: IaaC} 101 | type Pairs = {[K in keyof T]: T[K]} 102 | 103 | export interface IEffect> { 104 | (parent: Construct): T 105 | effect: (f: (x: T) => void) => IEffect 106 | flatMap: (f: (x: T) => Product) => IEffect 107 | yield: (k: K) => IPure 108 | } 109 | 110 | function effect(f: IaaC): IEffect { 111 | const pure: IEffect = f as IEffect 112 | pure.flatMap = (fmap: (x: T) => Product) => 113 | effect( 114 | (scope) => { 115 | const node = f(scope) 116 | const object = fmap(node) 117 | const value = {} as B 118 | const keys = Reflect.ownKeys(object) as (keyof B)[] 119 | for (const key of keys) { 120 | value[key] = object[key](scope) 121 | } 122 | return { ...node, ...value } 123 | } 124 | ) 125 | 126 | pure.effect = (eff: (x: T) => void) => 127 | effect( 128 | (scope) => { 129 | const node = f(scope) 130 | eff(node) 131 | return node 132 | } 133 | ) 134 | 135 | pure.yield = (k: K) => unit((node) => f(node)[k]) 136 | 137 | return pure 138 | } 139 | 140 | function compose>(product: Product): IaaC> { 141 | return (scope) => { 142 | const value = {} as T 143 | const keys = Reflect.ownKeys(product) as (keyof T)[] 144 | for (const key of keys) { 145 | value[key] = product[key](scope) 146 | } 147 | return value 148 | } 149 | } 150 | 151 | /** 152 | * The effect is a type-class that operates with product of individual `IaaC`. 153 | * It implements methods to apply effects to product of "cloud components" and 154 | * yields the result back. The effect function operates with pure types `T`. 155 | * The effect returns always `IaaC`. 156 | * 157 | * @param resources product of `IaaC` components 158 | */ 159 | export function use>(resources: Product): IEffect { 160 | return effect(compose(resources)) 161 | } 162 | 163 | /** 164 | * attaches the pure definition of resource to the stack nodes 165 | * 166 | * @param scope the "parent" context 167 | * @param iaac purely functional definition of the component 168 | */ 169 | export function join(scope: Construct, fn: IaaC): T { 170 | const x = fn(scope) as any 171 | return (typeof x === 'function') ? join(scope, x) : x 172 | } 173 | 174 | /** 175 | * Attaches the pure stack components to the root of CDK application. 176 | * 177 | * @param root the root of an entire CDK application 178 | * @param iaac purely functional definition of the stack 179 | * @param name optionally the logical of the stack 180 | */ 181 | export function root(scope: App, fn: IaaC, name?: string): App { 182 | fn(new Stack(scope, name || fn.name)) 183 | return scope 184 | } 185 | -------------------------------------------------------------------------------- /pure/test/iaac.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as pure from '../src/index' 9 | import * as cdk from '@aws-cdk/core' 10 | 11 | const cf = pure.iaac(cdk.CfnResource) 12 | 13 | function MyA(): cdk.CfnResourceProps { 14 | return { type: 'A' } 15 | } 16 | 17 | function MyB(): cdk.CfnResourceProps { 18 | return { type: 'B' } 19 | } 20 | 21 | class Wrap { 22 | constructor(c: cdk.CfnResource) { 23 | c.addOverride('Some', 'Wrap') 24 | } 25 | } 26 | const wf = pure.wrap(Wrap) 27 | 28 | class Lookup { 29 | public static from(scope: cdk.Construct, name: string, props: {type: string}): cdk.CfnResource { 30 | return new cdk.CfnResource(scope, name, { type: props.type }) 31 | } 32 | } 33 | const lf = pure.include(Lookup.from) 34 | 35 | function Stack(scope: cdk.Construct): cdk.Construct { 36 | pure.join(scope, cf(MyA)) 37 | pure.join(scope, cf(MyB)) 38 | return scope 39 | } 40 | 41 | it('attach components to stack using type safe factory', 42 | () => { 43 | const app = new cdk.App() 44 | pure.root(app, Stack, 'IaaC') 45 | const response = app.synth() 46 | const stack = response.getStack('IaaC') 47 | expect(stack.template).toEqual( 48 | { 49 | Resources: { 50 | MyA: { Type: 'A' }, 51 | MyB: { Type: 'B' }, 52 | } 53 | }, 54 | ) 55 | } 56 | ) 57 | 58 | it('attach components to stack using effects', 59 | () => { 60 | const app = new cdk.App() 61 | const Stack = (): cdk.StackProps => ({ env: {} }) 62 | pure.join(app, 63 | pure.iaac(cdk.Stack)(Stack) 64 | .effect(x => { 65 | pure.join(x, cf(MyA)) 66 | pure.join(x, cf(MyB)) 67 | }) 68 | ) 69 | const response = app.synth() 70 | const stack = response.getStack('Stack') 71 | expect(stack.template).toEqual( 72 | { 73 | Resources: { 74 | MyA: { Type: 'A' }, 75 | MyB: { Type: 'B' }, 76 | } 77 | }, 78 | ) 79 | } 80 | ) 81 | 82 | it('attach components to stack using wrap', 83 | () => { 84 | const app = new cdk.App() 85 | const Stack = (): cdk.StackProps => ({ env: {} }) 86 | pure.join(app, 87 | pure.iaac(cdk.Stack)(Stack) 88 | .effect(x => pure.join(x, wf(cf(MyA)))) 89 | ) 90 | const response = app.synth() 91 | const stack = response.getStack('Stack') 92 | expect(stack.template).toEqual( 93 | { 94 | Resources: { 95 | MyA: { Type: 'A', Some: 'Wrap' }, 96 | } 97 | }, 98 | ) 99 | } 100 | ) 101 | 102 | it('attach components to stack using include', 103 | () => { 104 | const app = new cdk.App() 105 | const Stack = (): cdk.StackProps => ({ env: {} }) 106 | const MyD = (): {type: string} => ({ type: 'D' }) 107 | 108 | pure.join(app, 109 | pure.iaac(cdk.Stack)(Stack) 110 | .effect(x => pure.join(x, lf(MyD))) 111 | ) 112 | const response = app.synth() 113 | const stack = response.getStack('Stack') 114 | expect(stack.template).toEqual( 115 | { 116 | Resources: { 117 | MyD: { Type: 'D' }, 118 | } 119 | }, 120 | ) 121 | } 122 | ) -------------------------------------------------------------------------------- /pure/test/join.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as pure from '../src/index' 9 | import * as cdk from '@aws-cdk/core' 10 | 11 | function MyA(node: cdk.Construct): cdk.Construct { 12 | return new cdk.CfnResource(node, 'MyA', { type: 'A' }) 13 | } 14 | 15 | function MyB(node: cdk.Construct): cdk.Construct { 16 | return new cdk.CfnResource(node, 'MyB', { type: 'B' }) 17 | } 18 | 19 | function Stack(scope: cdk.Construct): cdk.Construct { 20 | pure.join(scope, MyA) 21 | pure.join(scope, MyB) 22 | return scope 23 | } 24 | 25 | it('attach components to stack', 26 | () => { 27 | const app = new cdk.App() 28 | pure.root(app, Stack, 'IaaC') 29 | const response = app.synth() 30 | const stack = response.getStack('IaaC') 31 | expect(stack.template).toEqual( 32 | { 33 | Resources: { 34 | MyA: { Type: 'A' }, 35 | MyB: { Type: 'B' }, 36 | } 37 | } 38 | ) 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /pure/test/map.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as pure from '../src/index' 9 | import * as cdk from '@aws-cdk/core' 10 | 11 | const cf = pure.iaac(cdk.CfnResource) 12 | 13 | function MyA(): cdk.CfnResourceProps { 14 | return { type: 'A' } 15 | } 16 | 17 | function MyB(): cdk.CfnResourceProps { 18 | return { type: 'B' } 19 | } 20 | 21 | function MyC(): pure.IaaC { 22 | return cf(MyA) 23 | .flatMap(a => 24 | cf(MyB).effect(b => b.addOverride('Other', a.logicalId)) 25 | ) 26 | } 27 | 28 | function MyD(scope: cdk.Construct): pure.IaaC { 29 | return cf(MyA) 30 | .map(_ => new cdk.CfnResource(scope, 'MyD', { type: 'D' })) 31 | } 32 | 33 | function Stack(scope: cdk.Construct): cdk.Construct { 34 | pure.join(scope, MyC) 35 | return scope 36 | } 37 | 38 | it('apply flatMap to pure functional component', 39 | () => { 40 | const app = new cdk.App() 41 | pure.root(app, Stack, 'IaaC') 42 | const response = app.synth() 43 | const stack = response.getStack('IaaC') 44 | expect(stack.template).toEqual( 45 | { 46 | Resources: { 47 | MyA: { Type: 'A' }, 48 | MyB: { Type: 'B', Other: 'MyA' }, 49 | } 50 | } 51 | ) 52 | } 53 | ) 54 | 55 | it('apply map to pure functional component', 56 | () => { 57 | const app = new cdk.App() 58 | const Stack = (): cdk.StackProps => ({ env: {} }) 59 | pure.join(app, 60 | pure.iaac(cdk.Stack)(Stack).effect(x => pure.join(x, MyD)) 61 | ) 62 | const response = app.synth() 63 | const stack = response.getStack('Stack') 64 | expect(stack.template).toEqual( 65 | { 66 | Resources: { 67 | MyA: { Type: 'A' }, 68 | MyD: { Type: 'D' }, 69 | } 70 | } 71 | ) 72 | } 73 | ) -------------------------------------------------------------------------------- /pure/test/root.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as pure from '../src/index' 9 | import * as cdk from '@aws-cdk/core' 10 | 11 | function Stack(scope: cdk.Construct): cdk.Construct { 12 | return new cdk.CfnResource(scope, 'IaaC', { type: 'MyResourceType' }); 13 | } 14 | 15 | it('create a stack to application', 16 | () => { 17 | const app = new cdk.App() 18 | pure.root(app, Stack) 19 | const response = app.synth() 20 | const stack = response.getStack('Stack') 21 | expect(stack.template).toEqual( 22 | { Resources: { IaaC: { Type: 'MyResourceType' } } } 23 | ) 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /pure/test/use.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2019 Dmitry Kolesnikov 3 | // 4 | // This file may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // https://github.com/fogfish/aws-cdk-pure 7 | // 8 | import * as pure from '../src/index' 9 | import * as cdk from '@aws-cdk/core' 10 | 11 | const cf = pure.iaac(cdk.CfnResource) 12 | 13 | function MyA(): cdk.CfnResourceProps { 14 | return { type: 'A' } 15 | } 16 | 17 | function MyB(): cdk.CfnResourceProps { 18 | return { type: 'B' } 19 | } 20 | 21 | // 22 | function HoCC(a: cdk.CfnResource, b: cdk.CfnResource): pure.IaaC { 23 | const MyC = (): cdk.CfnResourceProps => ({ 24 | type: 'C', 25 | properties: {a: a.logicalId, b: b.logicalId} 26 | }) 27 | return cf(MyC) 28 | } 29 | 30 | function HoCD(c: cdk.CfnResource): pure.IaaC { 31 | const MyD =(): cdk.CfnResourceProps => ({ 32 | type: 'D', 33 | properties: {c: c.logicalId} 34 | }) 35 | return cf(MyD) 36 | } 37 | 38 | // 39 | function HoC1(): pure.IaaC { 40 | const a = cf(MyA) 41 | const b = cf(MyB) 42 | 43 | return pure.use({ a, b }) 44 | .effect((x) => x.a.addOverride('Other', x.b.logicalId)) 45 | .yield('a') 46 | } 47 | 48 | function HoC2(): pure.IaaC { 49 | const a = cf(MyA) 50 | return pure.use({ a }) 51 | .flatMap(_ => ({b: cf(MyB)}) ) 52 | .yield('b') 53 | } 54 | 55 | function HoC3(): pure.IaaC { 56 | const a = cf(MyA) 57 | const b = cf(MyB) 58 | 59 | return pure.use({ a, b }) 60 | .flatMap(x => ({ c: HoCC(x.a, x.b) })) 61 | .flatMap(x => ({ d: HoCD(x.c) })) 62 | .yield('c') 63 | } 64 | 65 | 66 | function MyApp(iaac: pure.IaaC): cdk.App { 67 | const Stack = (): cdk.StackProps => ({ env: {} }) 68 | const stack = pure.iaac(cdk.Stack)(Stack).effect(x => pure.join(x, iaac)) 69 | const app = new cdk.App() 70 | pure.join(app, stack) 71 | return app 72 | } 73 | 74 | // 75 | // 76 | it('apply effects to product of pure functional component', 77 | () => { 78 | const app = MyApp(HoC1()) 79 | const response = app.synth() 80 | const stack = response.getStack('Stack') 81 | expect(stack.template).toEqual( 82 | { 83 | Resources: { 84 | MyA: { Type: 'A', Other: 'MyB' }, 85 | MyB: { Type: 'B' }, 86 | } 87 | } 88 | ) 89 | } 90 | ) 91 | 92 | it('apply flatMap to product of pure functional component', 93 | () => { 94 | const app = MyApp(HoC2()) 95 | const response = app.synth() 96 | const stack = response.getStack('Stack') 97 | expect(stack.template).toEqual( 98 | { 99 | Resources: { 100 | MyA: { Type: 'A' }, 101 | MyB: { Type: 'B' }, 102 | } 103 | } 104 | ) 105 | } 106 | ) 107 | 108 | 109 | it('apply nested flatMap to product of pure functional component', 110 | () => { 111 | const app = MyApp(HoC3()) 112 | const response = app.synth() 113 | const stack = response.getStack('Stack') 114 | expect(stack.template).toEqual( 115 | { 116 | Resources: { 117 | MyA: { Type: 'A' }, 118 | MyB: { Type: 'B' }, 119 | MyC: { Type: 'C', Properties: { a: 'MyA', b: 'MyB' }}, 120 | MyD: { Type: 'D', Properties: { c: 'MyC' }}, 121 | } 122 | } 123 | ) 124 | } 125 | ) 126 | -------------------------------------------------------------------------------- /pure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "outDir": "./lib", 7 | "declaration": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization":false 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "test"] 24 | } 25 | --------------------------------------------------------------------------------