├── .github ├── dependabot.yml └── workflows │ └── dependabot.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── event-driven-developers-tale ├── .gitignore ├── .graphqlconfig.yml ├── .vscode │ └── settings.json ├── LICENSE ├── README.md ├── amplify │ ├── .config │ │ └── project-config.json │ ├── README.md │ ├── backend │ │ ├── api │ │ │ └── vacationtracker │ │ │ │ ├── parameters.json │ │ │ │ ├── resolvers │ │ │ │ ├── Mutation.submitVacationRequest.req.vtl │ │ │ │ ├── Mutation.submitVacationRequest.res.vtl │ │ │ │ ├── Mutation.updateVacationRequest.req.vtl │ │ │ │ ├── Mutation.updateVacationRequest.res.vtl │ │ │ │ └── README.md │ │ │ │ ├── schema.graphql │ │ │ │ ├── stacks │ │ │ │ └── CustomResources.json │ │ │ │ └── transform.conf.json │ │ ├── auth │ │ │ └── vacationtrackerd86ca2cd │ │ │ │ ├── parameters.json │ │ │ │ └── vacationtrackerd86ca2cd-cloudformation-template.yml │ │ ├── backend-config.json │ │ ├── hosting │ │ │ └── amplifyhosting │ │ │ │ └── amplifyhosting-template.json │ │ └── tags.json │ ├── cli.json │ ├── hooks │ │ └── README.md │ └── team-provider-info.json ├── functions │ ├── createVacationRequest │ │ ├── .aws-sam │ │ │ └── build.toml │ │ ├── .npmignore │ │ ├── app.js │ │ ├── events │ │ │ └── event.json │ │ ├── package.json │ │ └── template.yaml │ ├── updateVacationRequest │ │ ├── .npmignore │ │ ├── app.js │ │ ├── events │ │ │ └── event.json │ │ ├── package.json │ │ └── template.yaml │ └── vacationRequestValidation │ │ ├── .npmignore │ │ ├── app.js │ │ ├── events │ │ └── event.json │ │ ├── package.json │ │ └── template.yaml ├── images │ ├── addtogroup.png │ ├── eventbridgeatlas.png │ ├── eventbridgeregistry.png │ ├── eventstormingsessionexample.png │ ├── register.png │ ├── submitVacationRequestFlow.png │ └── xray.png ├── infrastructure │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── bin │ │ └── infrastructure.js │ ├── cdk.json │ ├── lib │ │ └── infrastructure-stack.js │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── graphql │ │ └── schema.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── components │ │ ├── AddCategory.js │ │ ├── AddVacation.js │ │ ├── CategoryItem.js │ │ ├── CustomTable.js │ │ ├── Modal.js │ │ ├── NavBar.js │ │ └── ProfileLink.js │ ├── graphql │ │ ├── mutations.js │ │ ├── queries.js │ │ ├── schema.json │ │ └── subscriptions.js │ ├── images │ │ └── profile-pic.jpg │ ├── index.css │ ├── index.js │ └── pages │ │ ├── People.js │ │ ├── Profile.js │ │ ├── Settings.js │ │ ├── Teams.js │ │ └── Vacations.js └── tailwind.config.js ├── hexagonal-architectures ├── .gitignore ├── README.md ├── events │ ├── create-product-evb-wrong-payload.json │ ├── create-product-evb.json │ ├── create-product-wrong-payload.json │ ├── create-product.json │ ├── delete-product.json │ ├── get-product.json │ └── get-products.json ├── images │ ├── diagram.png │ └── hex.png ├── jest.config.js ├── junit.xml ├── package-lock.json ├── package.json ├── src │ ├── adapters │ │ ├── api-gateway.ts │ │ └── event-bridge.ts │ ├── domain │ │ └── Products.ts │ ├── functions │ │ ├── delete-product │ │ │ └── delete-product.ts │ │ ├── get-product │ │ │ └── get-product.ts │ │ ├── get-products │ │ │ └── get-products.ts │ │ └── put-product │ │ │ └── put-product.ts │ ├── model │ │ └── product.ts │ ├── store │ │ ├── dynamodb │ │ │ └── dynamodb-store.ts │ │ └── product-store.ts │ └── tests │ │ └── domain.test.ts ├── template.yaml ├── tsconfig.json └── webpack.config.js ├── lambda-powertools-feature-flags ├── README.md ├── app-config-store │ ├── cdk │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin │ │ │ └── cdk.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib │ │ │ └── cdk-stack.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── test │ │ │ └── cdk.test.ts │ │ └── tsconfig.json │ └── cfn │ │ ├── feature-config.json │ │ └── template.yaml ├── app │ ├── .coveragerc │ ├── .gitignore │ ├── Makefile │ ├── Pipfile │ ├── Pipfile.lock │ ├── README.md │ ├── events │ │ └── hello_world_event.json │ ├── products │ │ ├── __init__.py │ │ ├── app.py │ │ └── requirements.txt │ ├── samconfig.toml │ ├── template.yaml │ └── tests │ │ ├── conftest.py │ │ ├── sytem │ │ ├── __init__.py │ │ └── test_app.py │ │ └── unit │ │ ├── __init__.py │ │ ├── test_app.py │ │ └── test_feature_flags.py └── docs │ ├── feature_toggles.drawio │ └── feature_toggles.png ├── latest_episode.json ├── micro-frontends-module-federation ├── .gitignore ├── LICENSE ├── README.md ├── accountdetails │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── AccountDetails.js │ └── webpack.config.js ├── appshell │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.js │ │ ├── Main.js │ │ ├── bootstrap.js │ │ └── index.js │ └── webpack.config.js ├── atriom.dat ├── catalogue │ ├── imgs │ │ ├── black1.jpeg │ │ ├── black2.jpeg │ │ └── white.jpeg │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── Catalogue.js │ │ ├── Details.js │ │ ├── Home.js │ │ └── Product.js │ └── webpack.config.js ├── modFedDiagram.png ├── modFedExample.png ├── myaccount │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── MyAccount.js │ └── webpack.config.js ├── paymentdetails │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── PaymentDetails.js │ └── webpack.config.js └── signin │ ├── package-lock.json │ ├── package.json │ ├── src │ └── SignIn.js │ └── webpack.config.js ├── serverless-lambda-java └── README.md └── spa-blue-green-deployments ├── README.md ├── app ├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CNAME ├── LICENSE.txt ├── README.md ├── angular.json ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json ├── karma.conf.js ├── logo.png ├── ngsw-config.json ├── package.json ├── protractor.conf.js ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── article │ │ │ ├── article-comment.component.html │ │ │ ├── article-comment.component.ts │ │ │ ├── article-resolver.service.ts │ │ │ ├── article-routing.module.ts │ │ │ ├── article.component.html │ │ │ ├── article.component.ts │ │ │ ├── article.module.ts │ │ │ └── markdown.pipe.ts │ │ ├── auth │ │ │ ├── auth-routing.module.ts │ │ │ ├── auth.component.html │ │ │ ├── auth.component.ts │ │ │ ├── auth.module.ts │ │ │ └── no-auth-guard.service.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ ├── index.ts │ │ │ ├── interceptors │ │ │ │ ├── http.token.interceptor.ts │ │ │ │ └── index.ts │ │ │ ├── models │ │ │ │ ├── article-list-config.model.ts │ │ │ │ ├── article.model.ts │ │ │ │ ├── comment.model.ts │ │ │ │ ├── errors.model.ts │ │ │ │ ├── index.ts │ │ │ │ ├── profile.model.ts │ │ │ │ └── user.model.ts │ │ │ └── services │ │ │ │ ├── api.service.ts │ │ │ │ ├── articles.service.ts │ │ │ │ ├── auth-guard.service.ts │ │ │ │ ├── comments.service.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.service.ts │ │ │ │ ├── profiles.service.ts │ │ │ │ ├── tags.service.ts │ │ │ │ └── user.service.ts │ │ ├── editor │ │ │ ├── editable-article-resolver.service.ts │ │ │ ├── editor-routing.module.ts │ │ │ ├── editor.component.html │ │ │ ├── editor.component.ts │ │ │ └── editor.module.ts │ │ ├── home │ │ │ ├── home-auth-resolver.service.ts │ │ │ ├── home-routing.module.ts │ │ │ ├── home.component.css │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── home.module.ts │ │ ├── index.ts │ │ ├── profile │ │ │ ├── profile-articles.component.html │ │ │ ├── profile-articles.component.ts │ │ │ ├── profile-favorites.component.html │ │ │ ├── profile-favorites.component.ts │ │ │ ├── profile-resolver.service.ts │ │ │ ├── profile-routing.module.ts │ │ │ ├── profile.component.html │ │ │ ├── profile.component.ts │ │ │ └── profile.module.ts │ │ ├── settings │ │ │ ├── settings-routing.module.ts │ │ │ ├── settings.component.html │ │ │ ├── settings.component.ts │ │ │ └── settings.module.ts │ │ └── shared │ │ │ ├── article-helpers │ │ │ ├── article-list.component.css │ │ │ ├── article-list.component.html │ │ │ ├── article-list.component.ts │ │ │ ├── article-meta.component.html │ │ │ ├── article-meta.component.ts │ │ │ ├── article-preview.component.html │ │ │ ├── article-preview.component.ts │ │ │ └── index.ts │ │ │ ├── buttons │ │ │ ├── favorite-button.component.html │ │ │ ├── favorite-button.component.ts │ │ │ ├── follow-button.component.html │ │ │ ├── follow-button.component.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── layout │ │ │ ├── footer.component.html │ │ │ ├── footer.component.ts │ │ │ ├── header.component.html │ │ │ ├── header.component.ts │ │ │ └── index.ts │ │ │ ├── list-errors.component.html │ │ │ ├── list-errors.component.ts │ │ │ ├── shared.module.ts │ │ │ └── show-authed.directive.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── .npmignore │ │ └── icons │ │ │ ├── icon-128x128.png │ │ │ ├── icon-144x144.png │ │ │ ├── icon-152x152.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-384x384.png │ │ │ ├── icon-512x512.png │ │ │ ├── icon-72x72.png │ │ │ └── icon-96x96.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── manifest.webmanifest │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── typings.d.ts ├── tsconfig.json └── yarn.lock └── cdk ├── .gitignore ├── README.md ├── cdk.go ├── cdk.json ├── go.mod └── go.sum /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-approve 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | 7 | jobs: 8 | dependabot: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.actor == 'dependabot[bot]' }} 11 | steps: 12 | - name: Dependabot metadata 13 | id: metadata 14 | uses: dependabot/fetch-metadata@v2.4.0 15 | with: 16 | github-token: "${{ secrets.GITHUB_TOKEN }}" 17 | 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{ (steps.metadata.outputs.update-type == 'version-update:semver-minor') || (steps.metadata.outputs.update-type == 'version-update:semver-patch') }} 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{ github.event.pull_request.html_url }} 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /event-driven-developers-tale/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | #amplify-do-not-edit-begin 27 | amplify/\#current-cloud-backend 28 | amplify/.config/local-* 29 | amplify/logs 30 | amplify/mock-data 31 | amplify/backend/amplify-meta.json 32 | amplify/backend/awscloudformation 33 | amplify/backend/.temp 34 | build/ 35 | dist/ 36 | node_modules/ 37 | aws-exports.js 38 | awsconfiguration.json 39 | amplifyconfiguration.json 40 | amplifyconfiguration.dart 41 | amplify-build-config.json 42 | amplify-gradle-config.json 43 | amplifytools.xcconfig 44 | .secret-* 45 | **.sample 46 | #amplify-do-not-edit-end 47 | -------------------------------------------------------------------------------- /event-driven-developers-tale/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | vacationtracker: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /event-driven-developers-tale/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "amplify/.config": true, 4 | "amplify/**/*-parameters.json": true, 5 | "amplify/**/amplify.state": true, 6 | "amplify/**/transform.conf.json": true, 7 | "amplify/#current-cloud-backend": true, 8 | "amplify/backend/amplify-meta.json": true, 9 | "amplify/backend/awscloudformation": true 10 | } 11 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tiago Barbosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "vacationtracker", 3 | "version": "3.1", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "vacationtracker", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authvacationtrackerd86ca2cd", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/resolvers/Mutation.submitVacationRequest.req.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Set default values. ** 2 | $util.qr($context.args.input.put("id", $util.defaultIfNull($ctx.args.input.id, $util.autoId()))) 3 | #set( $createdAt = $util.time.nowISO8601() ) 4 | ## Automatically set the createdAt timestamp. ** 5 | $util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $createdAt))) 6 | ## Automatically set the updatedAt timestamp. ** 7 | $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $createdAt))) 8 | ## Automatically set the owner. ** 9 | $util.qr($context.args.input.put("owner", $util.defaultIfNull($ctx.args.input.owner, $ctx.identity.username))) 10 | 11 | $util.qr($ctx.stash.put("typeName", "Mutation")) 12 | $util.qr($ctx.stash.put("fieldName", "submitVacationRequest")) 13 | ## [End] Set default values. ** 14 | 15 | ## [Start] Invoke AWS Lambda data source: CreateVacationRequestLambdaDataSource. ** 16 | { 17 | "version": "2018-05-29", 18 | "operation": "Invoke", 19 | "payload": { 20 | "typeName": "$ctx.stash.get("typeName")", 21 | "fieldName": "$ctx.stash.get("fieldName")", 22 | "arguments": $util.toJson($ctx.arguments), 23 | "identity": $util.toJson($ctx.identity), 24 | "source": $util.toJson($ctx.source), 25 | "request": $util.toJson($ctx.request), 26 | "prev": $util.toJson($ctx.prev) 27 | } 28 | } 29 | ## [End] Invoke AWS Lambda data source: CreateVacationRequestLambdaDataSource. ** -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/resolvers/Mutation.submitVacationRequest.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Handle error or return result. ** 2 | #if( $ctx.error ) 3 | $util.error($ctx.error.message, $ctx.error.type) 4 | #end 5 | $util.toJson($ctx.result) 6 | ## [End] Handle error or return result. ** -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/resolvers/Mutation.updateVacationRequest.req.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Set default values. ** 2 | #set( $createdAt = $util.time.nowISO8601() ) 3 | ## Automatically set the updatedAt timestamp. ** 4 | $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $createdAt))) 5 | ## Automatically set the owner. ** 6 | $util.qr($context.args.input.put("owner", $util.defaultIfNull($ctx.args.input.owner, $ctx.identity.username))) 7 | 8 | $util.qr($ctx.stash.put("typeName", "Mutation")) 9 | $util.qr($ctx.stash.put("fieldName", "updateVacationRequest")) 10 | ## [End] Set default values. ** 11 | 12 | ## [Start] Invoke AWS Lambda data source: UpdateVacationRequestLambdaDataSource. ** 13 | { 14 | "version": "2018-05-29", 15 | "operation": "Invoke", 16 | "payload": { 17 | "typeName": "$ctx.stash.get("typeName")", 18 | "fieldName": "$ctx.stash.get("fieldName")", 19 | "arguments": $util.toJson($ctx.arguments), 20 | "identity": $util.toJson($ctx.identity), 21 | "source": $util.toJson($ctx.source), 22 | "request": $util.toJson($ctx.request), 23 | "prev": $util.toJson($ctx.prev) 24 | } 25 | } 26 | ## [End] Invoke AWS Lambda data source: UpdateVacationRequestLambdaDataSource. ** -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/resolvers/Mutation.updateVacationRequest.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Handle error or return result. ** 2 | #if( $ctx.error ) 3 | $util.error($ctx.error.message, $ctx.error.type) 4 | #end 5 | $util.toJson($ctx.result) 6 | ## [End] Handle error or return result. ** -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/resolvers/README.md: -------------------------------------------------------------------------------- 1 | Any resolvers that you add in this directory will override the ones automatically generated by Amplify CLI and will be directly copied to the cloud. 2 | For more information, visit [https://docs.amplify.aws/cli/graphql-transformer/resolvers](https://docs.amplify.aws/cli/graphql-transformer/resolvers) -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": ["true", "false"] 50 | } 51 | }, 52 | "Outputs": { 53 | "EmptyOutput": { 54 | "Description": "An empty output. You may delete this if you have at least one resource above.", 55 | "Value": "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/api/vacationtracker/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "vacationtrackerd86ca2cd": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [], 7 | "customAuth": false, 8 | "frontendAuthConfig": { 9 | "loginMechanisms": [], 10 | "signupAttributes": [ 11 | "EMAIL" 12 | ], 13 | "passwordProtectionSettings": { 14 | "passwordPolicyMinLength": 8, 15 | "passwordPolicyCharacters": [] 16 | }, 17 | "mfaConfiguration": "OFF", 18 | "mfaTypes": [ 19 | "SMS" 20 | ], 21 | "verificationMechanisms": [ 22 | "EMAIL" 23 | ] 24 | } 25 | } 26 | }, 27 | "api": { 28 | "vacationtracker": { 29 | "service": "AppSync", 30 | "providerPlugin": "awscloudformation", 31 | "output": { 32 | "authConfig": { 33 | "defaultAuthentication": { 34 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 35 | "userPoolConfig": { 36 | "userPoolId": "authvacationtrackerd86ca2cd" 37 | } 38 | }, 39 | "additionalAuthenticationProviders": [ 40 | { 41 | "authenticationType": "AWS_IAM" 42 | } 43 | ] 44 | } 45 | } 46 | } 47 | }, 48 | "function": {}, 49 | "hosting": { 50 | "amplifyhosting": { 51 | "service": "amplifyhosting", 52 | "providerPlugin": "awscloudformation", 53 | "type": "manual" 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/hosting/amplifyhosting/amplifyhosting-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "{\"createdOn\":\"Mac\",\"createdBy\":\"Amplify\",\"createdWith\":\"9.0.0\",\"stackType\":\"hosting-amplifyhosting\",\"metadata\":{}}", 4 | "Parameters": { 5 | "env": { 6 | "Type": "String" 7 | }, 8 | "appId": { 9 | "Type": "String" 10 | }, 11 | "type": { 12 | "Type": "String" 13 | } 14 | }, 15 | "Conditions": { 16 | "isManual": { 17 | "Fn::Equals": [ 18 | { 19 | "Ref": "type" 20 | }, 21 | "manual" 22 | ] 23 | } 24 | }, 25 | "Resources": { 26 | "AmplifyBranch": { 27 | "Condition": "isManual", 28 | "Type": "AWS::Amplify::Branch", 29 | "Properties": { 30 | "BranchName": { 31 | "Ref": "env" 32 | }, 33 | "AppId": { 34 | "Ref": "appId" 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "improvepluralization": true, 6 | "validatetypenamereservedwords": true, 7 | "useexperimentalpipelinedtransformer": false, 8 | "enableiterativegsiupdates": true, 9 | "secondarykeyasgsi": true, 10 | "skipoverridemutationinputtypes": true, 11 | "securityEnhancementNotification": false, 12 | "showfieldauthnotification": false 13 | }, 14 | "frontend-ios": { 15 | "enablexcodeintegration": true 16 | }, 17 | "auth": { 18 | "enablecaseinsensitivity": true, 19 | "useinclusiveterminology": true, 20 | "breakcirculardependency": true, 21 | "forcealiasattributes": false 22 | }, 23 | "codegen": { 24 | "useappsyncmodelgenplugin": true, 25 | "usedocsgeneratorplugin": true, 26 | "usetypesgeneratorplugin": true, 27 | "cleangeneratedmodelsdirectory": true, 28 | "retaincasestyle": true, 29 | "addtimestampfields": true, 30 | "handlelistnullabilitytransparently": true, 31 | "emitauthprovider": true, 32 | "generateindexrules": true, 33 | "enabledartnullsafety": true 34 | }, 35 | "appsync": { 36 | "generategraphqlpermissions": true 37 | }, 38 | "project": { 39 | "overrides": true 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Command Hooks 2 | 3 | Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. 4 | 5 | To get started, add your script files based on the expected naming convention in this directory. 6 | 7 | Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks 8 | -------------------------------------------------------------------------------- /event-driven-developers-tale/amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-vacationtracker-dev-225120-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::414275540131:role/amplify-vacationtracker-dev-225120-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::414275540131:role/amplify-vacationtracker-dev-225120-authRole", 7 | "Region": "eu-west-1", 8 | "DeploymentBucketName": "amplify-vacationtracker-dev-225120-deployment", 9 | "UnauthRoleName": "amplify-vacationtracker-dev-225120-unauthRole", 10 | "StackName": "amplify-vacationtracker-dev-225120", 11 | "StackId": "arn:aws:cloudformation:eu-west-1:414275540131:stack/amplify-vacationtracker-dev-225120/0f148800-1fdd-11ec-8d94-0a397a0898a1", 12 | "AmplifyAppId": "d1lhx9dm0heri0" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "vacationtrackerd86ca2cd": {} 17 | }, 18 | "hosting": { 19 | "amplifyhosting": { 20 | "appId": "d1lhx9dm0heri0", 21 | "type": "manual" 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/createVacationRequest/.aws-sam/build.toml: -------------------------------------------------------------------------------- 1 | # This file is auto generated by SAM CLI build command 2 | 3 | [function_build_definitions] 4 | [function_build_definitions.e6005a19-85bf-47ca-8891-6d16e4eb1a21] 5 | codeuri = "/Users/tiagobar/Documents/GitHub/vacation-tracker/functions/createVacationRequest" 6 | runtime = "nodejs14.x" 7 | source_md5 = "" 8 | packagetype = "Zip" 9 | functions = ["CreateVacationRequestFunction"] 10 | 11 | [layer_build_definitions] 12 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/createVacationRequest/.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/createVacationRequest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello_world", 3 | "version": "1.0.0", 4 | "description": "hello world sample for NodeJS", 5 | "main": "app.js", 6 | "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", 7 | "author": "SAM CLI", 8 | "license": "MIT", 9 | "dependencies": { 10 | "aws-sdk": "^2.1009.0", 11 | "aws-xray-sdk": "^3.3.3" 12 | }, 13 | "scripts": { 14 | "build": "npm install" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/createVacationRequest/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | createVacationRequest 5 | 6 | Sample SAM Template for createVacationRequest 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | Environment: 13 | Variables: 14 | TABLE_NAME: VacationRequest-tthjzp4lebavxlmqbniom57coi-dev 15 | EVENT_BUS_NAME: VacationTrackerEvents 16 | 17 | Resources: 18 | CreateVacationRequestFunction: 19 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 20 | Properties: 21 | CodeUri: . 22 | Handler: app.handler 23 | Runtime: nodejs14.x 24 | 25 | Outputs: 26 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 27 | # Find out more about other implicit resources you can reference within SAM 28 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 29 | CreateVacationRequestFunction: 30 | Description: "Create Vacation Request Lambda Function ARN" 31 | Value: !GetAtt CreateVacationRequestFunction.Arn 32 | CreateVacationRequestFunctionIamRole: 33 | Description: "Implicit IAM Role created for Create Vacation Request function" 34 | Value: !GetAtt CreateVacationRequestFunction.Arn 35 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/updateVacationRequest/.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/updateVacationRequest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello_world", 3 | "version": "1.0.0", 4 | "description": "hello world sample for NodeJS", 5 | "main": "app.js", 6 | "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", 7 | "author": "SAM CLI", 8 | "license": "MIT", 9 | "dependencies": { 10 | "aws-sdk": "^2.1006.0", 11 | "aws-xray-sdk": "^3.3.3" 12 | }, 13 | "scripts": { 14 | "test": "mocha tests/unit/" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/updateVacationRequest/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | createVacationRequest 5 | 6 | Sample SAM Template for createVacationRequest 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | Environment: 13 | Variables: 14 | TABLE_NAME: VacationRequest-tthjzp4lebavxlmqbniom57coi-dev 15 | EVENT_BUS_NAME: VacationTrackerEvents 16 | 17 | Resources: 18 | UpdateVacationRequestFunction: 19 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 20 | Properties: 21 | CodeUri: . 22 | Handler: app.handler 23 | Runtime: nodejs14.x 24 | 25 | Outputs: 26 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 27 | # Find out more about other implicit resources you can reference within SAM 28 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 29 | UpdateVacationRequestFunction: 30 | Description: "Update Vacation Request Lambda Function ARN" 31 | Value: !GetAtt UpdateVacationRequestFunction.Arn 32 | UpdateVacationRequestFunctionIamRole: 33 | Description: "Implicit IAM Role created for Update Vacation Request function" 34 | Value: !GetAtt UpdateVacationRequestFunction.Arn 35 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/vacationRequestValidation/.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/vacationRequestValidation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello_world", 3 | "version": "1.0.0", 4 | "description": "hello world sample for NodeJS", 5 | "main": "app.js", 6 | "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", 7 | "author": "SAM CLI", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@aws-sdk/client-appsync": "^3.36.0", 11 | "apollo-link-http": "^1.5.17", 12 | "aws-appsync": "^4.1.2", 13 | "aws-sdk": "^2.1005.0", 14 | "aws-xray-sdk": "^3.3.3", 15 | "es6-promise": "^4.2.8", 16 | "graphql-tag": "^2.12.5", 17 | "isomorphic-fetch": "^3.0.0", 18 | "node-fetch": "^3.1.1" 19 | }, 20 | "scripts": { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /event-driven-developers-tale/functions/vacationRequestValidation/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | createVacationRequest 5 | 6 | Sample SAM Template for createVacationRequest 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | Environment: 13 | Variables: 14 | APP_SYNC_API_URL: https://yeo5h7yxp5gulf7wpl5hq6dvsa.appsync-api.eu-west-1.amazonaws.com/graphql 15 | 16 | Resources: 17 | ValidateVacationRequestFunction: 18 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 19 | Properties: 20 | CodeUri: . 21 | Handler: app.handler 22 | Runtime: nodejs14.x 23 | 24 | Outputs: 25 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 26 | # Find out more about other implicit resources you can reference within SAM 27 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 28 | ValidateVacationRequestFunction: 29 | Description: "Validate Vacation Request Function ARN" 30 | Value: !GetAtt ValidateVacationRequestFunction.Arn 31 | ValidateVacationRequestFunctionIamRole: 32 | Description: "Implicit IAM Role created for Validate Vacation Request function" 33 | Value: !GetAtt ValidateVacationRequestFunctionRole.Arn 34 | -------------------------------------------------------------------------------- /event-driven-developers-tale/images/addtogroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/addtogroup.png -------------------------------------------------------------------------------- /event-driven-developers-tale/images/eventbridgeatlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/eventbridgeatlas.png -------------------------------------------------------------------------------- /event-driven-developers-tale/images/eventbridgeregistry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/eventbridgeregistry.png -------------------------------------------------------------------------------- /event-driven-developers-tale/images/eventstormingsessionexample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/eventstormingsessionexample.png -------------------------------------------------------------------------------- /event-driven-developers-tale/images/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/register.png -------------------------------------------------------------------------------- /event-driven-developers-tale/images/submitVacationRequestFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/submitVacationRequestFlow.png -------------------------------------------------------------------------------- /event-driven-developers-tale/images/xray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/images/xray.png -------------------------------------------------------------------------------- /event-driven-developers-tale/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # CDK asset staging directory 4 | .cdk.staging 5 | cdk.out 6 | -------------------------------------------------------------------------------- /event-driven-developers-tale/infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | # CDK asset staging directory 2 | .cdk.staging 3 | cdk.out 4 | -------------------------------------------------------------------------------- /event-driven-developers-tale/infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK JavaScript project! 2 | 3 | This is a blank project for JavaScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. The build step is not required when using JavaScript. 6 | 7 | ## Useful commands 8 | 9 | * `npm run test` perform the jest unit tests 10 | * `cdk deploy` deploy this stack to your default AWS account/region 11 | * `cdk diff` compare deployed stack with current state 12 | * `cdk synth` emits the synthesized CloudFormation template 13 | -------------------------------------------------------------------------------- /event-driven-developers-tale/infrastructure/bin/infrastructure.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cdk = require('@aws-cdk/core'); 4 | const { InfrastructureStack } = require('../lib/infrastructure-stack'); 5 | 6 | const app = new cdk.App(); 7 | new InfrastructureStack(app, 'InfrastructureStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /event-driven-developers-tale/infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "node bin/infrastructure.js", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, 15 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 16 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /event-driven-developers-tale/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infrastructure", 3 | "version": "0.1.0", 4 | "bin": { 5 | "infrastructure": "bin/infrastructure.js" 6 | }, 7 | "scripts": { 8 | "build": "echo \"The build step is not required when using JavaScript!\" && exit 0", 9 | "cdk": "cdk", 10 | "test": "jest" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "^2.68.0", 14 | "aws-cdk": "^1.148.0", 15 | "jest": "^29.7.0" 16 | }, 17 | "dependencies": { 18 | "@aws-cdk/aws-events": "^1.19.0", 19 | "@aws-cdk/aws-events-targets": "^1.19.0", 20 | "@aws-cdk/aws-iam": "^1.148.0", 21 | "@aws-cdk/aws-lambda": "^1.19.0", 22 | "@aws-cdk/aws-s3-assets": "^1.19.0", 23 | "@aws-cdk/core": "^1.32.2", 24 | "esbuild": "^0.25.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /event-driven-developers-tale/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vacation-tracker", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^6.10.0", 7 | "@aws-sdk/client-dynamodb": "^3.782.0", 8 | "@emotion/react": "^11.14.0", 9 | "@emotion/styled": "^11.14.0", 10 | "@heroicons/react": "^2.2.0", 11 | "@mui/material": "^6.4.8", 12 | "@testing-library/jest-dom": "^6.6.3", 13 | "@testing-library/react": "^16.2.0", 14 | "@testing-library/user-event": "^14.6.1", 15 | "aws-amplify": "^6.13.6", 16 | "aws-sdk": "^2.1165.0", 17 | "aws-xray-sdk": "^3.3.6", 18 | "hello_world": "file:functions/createVacationRequest", 19 | "moment": "^2.29.3", 20 | "nanoevents": "^9.1.0", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-moment": "^1.1.2", 24 | "react-router-dom": "^6.28.0", 25 | "react-scripts": "^5.0.1", 26 | "react-uuid": "^2.0.0", 27 | "web-vitals": "^4.2.4" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "autoprefixer": "^10.4.21", 55 | "postcss": "^8.5.3", 56 | "tailwindcss": "^3.4.17" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /event-driven-developers-tale/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/public/favicon.ico -------------------------------------------------------------------------------- /event-driven-developers-tale/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /event-driven-developers-tale/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/public/logo192.png -------------------------------------------------------------------------------- /event-driven-developers-tale/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /event-driven-developers-tale/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/components/AddCategory.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { API } from 'aws-amplify' 3 | import { createCategory } from '../graphql/mutations' 4 | 5 | function AddCategory({ emmiter }) { 6 | 7 | const [name, setName] = useState("") 8 | 9 | async function addCategory() { 10 | const newCategory = { 11 | name: name 12 | } 13 | 14 | await API.graphql({ 15 | query: createCategory, 16 | variables: { 17 | input: newCategory 18 | }, 19 | authMode: 'AMAZON_COGNITO_USER_POOLS' 20 | }) 21 | 22 | emmiter.emit("showModal", false, "", "", null) 23 | } 24 | 25 | function cancel(){ 26 | emmiter.emit("showModal", false, "", "", null) 27 | } 28 | 29 | return ( 30 | 31 |
32 |
33 | 34 | setName(event.target.value)} /> 35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | ) 45 | } 46 | 47 | export default AddCategory 48 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/components/CategoryItem.js: -------------------------------------------------------------------------------- 1 | function CategoryItem({category}) { 2 | return ( 3 |
4 | {category.name} 5 |
6 | ) 7 | } 8 | 9 | export default CategoryItem 10 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/components/Modal.js: -------------------------------------------------------------------------------- 1 | function Modal({ showModal, setShowModal, header, subtitle, content }) { 2 | return ( 3 | <> 4 | {showModal && 5 |
6 |
7 |
8 |

{header}

9 | {subtitle} 10 | 11 |
12 |
13 | {content} 14 |
15 |
16 |
17 | } 18 | 19 | ) 20 | } 21 | 22 | export default Modal 23 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const submitVacationRequest = /* GraphQL */ ` 5 | mutation SubmitVacationRequest($input: CreateVacationRequestInput!) { 6 | submitVacationRequest(input: $input) { 7 | id 8 | category 9 | description 10 | approvalStatus 11 | startDate 12 | endDate 13 | approvedBy 14 | owner 15 | rejectionReason 16 | createdAt 17 | updatedAt 18 | } 19 | } 20 | `; 21 | export const updateVacationRequest = /* GraphQL */ ` 22 | mutation UpdateVacationRequest($input: UpdateVacationRequestInput!) { 23 | updateVacationRequest(input: $input) { 24 | id 25 | category 26 | description 27 | approvalStatus 28 | startDate 29 | endDate 30 | approvedBy 31 | owner 32 | rejectionReason 33 | createdAt 34 | updatedAt 35 | } 36 | } 37 | `; 38 | export const deleteVacationRequest = /* GraphQL */ ` 39 | mutation DeleteVacationRequest( 40 | $input: DeleteVacationRequestInput! 41 | $condition: ModelVacationRequestConditionInput 42 | ) { 43 | deleteVacationRequest(input: $input, condition: $condition) { 44 | id 45 | category 46 | description 47 | approvalStatus 48 | startDate 49 | endDate 50 | approvedBy 51 | owner 52 | rejectionReason 53 | createdAt 54 | updatedAt 55 | } 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const getVacationRequest = /* GraphQL */ ` 5 | query GetVacationRequest($id: ID!) { 6 | getVacationRequest(id: $id) { 7 | id 8 | category 9 | description 10 | approvalStatus 11 | startDate 12 | endDate 13 | approvedBy 14 | owner 15 | rejectionReason 16 | createdAt 17 | updatedAt 18 | } 19 | } 20 | `; 21 | export const listVacationRequests = /* GraphQL */ ` 22 | query ListVacationRequests( 23 | $filter: ModelVacationRequestFilterInput 24 | $limit: Int 25 | $nextToken: String 26 | ) { 27 | listVacationRequests( 28 | filter: $filter 29 | limit: $limit 30 | nextToken: $nextToken 31 | ) { 32 | items { 33 | id 34 | category 35 | description 36 | approvalStatus 37 | startDate 38 | endDate 39 | approvedBy 40 | owner 41 | rejectionReason 42 | createdAt 43 | updatedAt 44 | } 45 | nextToken 46 | } 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const onVacationRequestNotification = /* GraphQL */ ` 5 | subscription OnVacationRequestNotification($owner: String) { 6 | onVacationRequestNotification(owner: $owner) { 7 | id 8 | category 9 | description 10 | approvalStatus 11 | startDate 12 | endDate 13 | approvedBy 14 | owner 15 | rejectionReason 16 | createdAt 17 | updatedAt 18 | } 19 | } 20 | `; 21 | export const onDeleteVacationRequest = /* GraphQL */ ` 22 | subscription OnDeleteVacationRequest { 23 | onDeleteVacationRequest { 24 | id 25 | category 26 | description 27 | approvalStatus 28 | startDate 29 | endDate 30 | approvedBy 31 | owner 32 | rejectionReason 33 | createdAt 34 | updatedAt 35 | } 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/images/profile-pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/event-driven-developers-tale/src/images/profile-pic.jpg -------------------------------------------------------------------------------- /event-driven-developers-tale/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import Amplify from 'aws-amplify'; 6 | import awsExports from './aws-exports'; 7 | Amplify.configure(awsExports); 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/pages/People.js: -------------------------------------------------------------------------------- 1 | function People() { 2 | return ( 3 |
4 |

PEOPLE

5 |

Work in progress

6 |
7 | ) 8 | } 9 | 10 | export default People 11 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/pages/Profile.js: -------------------------------------------------------------------------------- 1 | function Profile() { 2 | return ( 3 |
4 |

PROFILE

5 |

Work in progress

6 |
7 | ) 8 | } 9 | 10 | export default Profile 11 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/pages/Settings.js: -------------------------------------------------------------------------------- 1 | // import { useState, useEffect } from 'react' 2 | // import { listCategories } from '../graphql/queries' 3 | // import { API } from 'aws-amplify' 4 | // import CategoryItem from '../components/CategoryItem' 5 | // import AddCategory from '../components/AddCategory' 6 | 7 | function Settings({emmiter}) { 8 | // const [categories, setCategories] = useState([]) 9 | 10 | // useEffect(()=> { 11 | // getCategories() 12 | // }, []) 13 | 14 | // async function getCategories(){ 15 | // const result = await API.graphql({query: listCategories, authMode: 'AMAZON_COGNITO_USER_POOLS'}) 16 | // setCategories(result.data.listCategories.items) 17 | // } 18 | 19 | // function showAddCategory(){ 20 | // emmiter.emit("showModal", true, "Create Category", "Create a new category to help employees classify their vacations.", ) 21 | // } 22 | 23 | 24 | 25 | return ( 26 |
27 |

SETTINGS

28 | {/* CATEGORIES */} 29 | {/* Header */} 30 | {/*
31 |

Categories

32 |
33 | 34 |
35 |
*/} 36 | 37 | {/* List Categories */} 38 | {/* { categories.map((category, index) => { 39 | return ( 40 | 41 | ) 42 | }) 43 | 44 | } */} 45 | 46 |
47 | ) 48 | } 49 | 50 | export default Settings 51 | -------------------------------------------------------------------------------- /event-driven-developers-tale/src/pages/Teams.js: -------------------------------------------------------------------------------- 1 | function Teams() { 2 | return ( 3 |
4 |

TEAMS

5 |

Work in progress

6 |
7 | ) 8 | } 9 | 10 | export default Teams 11 | -------------------------------------------------------------------------------- /event-driven-developers-tale/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | content: [ 4 | './public/**/*.html', 5 | './src/**/*.{js,jsx,ts,tsx,vue}', 6 | ], 7 | theme: { 8 | extend: { 9 | margin: { 10 | '-120': '-500px', 11 | } 12 | }, 13 | }, 14 | variants: { 15 | extend: { 16 | opacity: ['disabled'] 17 | }, 18 | }, 19 | plugins: [], 20 | } 21 | -------------------------------------------------------------------------------- /hexagonal-architectures/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .aws-sam/ 3 | samconfig.toml 4 | env.json 5 | .DS_Store 6 | src/.DS_Store 7 | .vscode -------------------------------------------------------------------------------- /hexagonal-architectures/README.md: -------------------------------------------------------------------------------- 1 | # Serverless Typescript Demo 2 | 3 | This is a simple serverless application built in Typescript and uses Node.js runtime. It consists of an [Amazon API Gateway](https://aws.amazon.com/api-gateway/) backed by four [AWS Lambda](https://aws.amazon.com/lambda/) functions and an [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) table for storage. 4 | 5 |

6 | Architecture diagram 7 |

8 | 9 | We will explore the usage of [hexagonal architecture](https://www.youtube.com/watch?v=kRFg6fkVChQ) pattern to decouple the entry points, from the main domain logic and the storage logic. 10 | As an example we will see how to implement an adapter for Amazon API Gateway and another adapter for Amazon EventBridge and how to easily switch between them with minor code changes. 11 | 12 | ![hexagonal architecture diagram](images/hex.png) 13 | 14 | ## Requirements 15 | 16 | - [AWS CLI](https://aws.amazon.com/cli/) 17 | - [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 18 | - [Node.js 14](https://nodejs.org/) 19 | 20 | ### Deployment 21 | 22 | Deploy the demo to your AWS account using [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started.html). 23 | 24 | ```bash 25 | npm install 26 | npm run build 27 | # sam deploy --guided # if running for the first time. 28 | sam deploy 29 | ``` 30 | 31 | The `npm run build` commmand will first build the products TypeScript project. Then the command `sam deploy` use the SAM Template to deploy the resources to your account. 32 | 33 | SAM will create an output of the API Gateway endpoint URL for future use in our load tests. 34 | 35 | ## License 36 | 37 | This library is licensed under the MIT-0 License. See the LICENSE file. 38 | -------------------------------------------------------------------------------- /hexagonal-architectures/events/create-product-evb-wrong-payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 3 | "detail-type": "ProductCreationRequested", 4 | "source": "serverless.demo", 5 | "account": "123456789012", 6 | "time": "1970-01-01T00:00:00Z", 7 | "region": "us-east-1", 8 | "resources": ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"], 9 | "detail": { 10 | "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 11 | "name": "product cdc73f9d-aea9-11e3-9d5a-835b769c0d9c" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /hexagonal-architectures/events/create-product-evb.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 3 | "detail-type": "ProductCreationRequested", 4 | "source": "serverless.demo", 5 | "account": "123456789012", 6 | "time": "1970-01-01T00:00:00Z", 7 | "region": "us-east-1", 8 | "resources": ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"], 9 | "detail": { 10 | "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 11 | "name": "product cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 12 | "price": 100.20 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hexagonal-architectures/events/create-product-wrong-payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"id\":\"100\",\"name\":\"product 100\"}", 3 | "resource": "/{proxy+}", 4 | "path": "/product", 5 | "httpMethod": "PUT", 6 | "headers": { 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate, sdch", 9 | "Accept-Language": "en-US,en;q=0.8", 10 | "Cache-Control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Host": "1234567890.execute-api.{dns_suffix}", 18 | "Upgrade-Insecure-Requests": "1", 19 | "User-Agent": "Custom User Agent String", 20 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 21 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 22 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 23 | "X-Forwarded-Port": "443", 24 | "X-Forwarded-Proto": "https" 25 | }, 26 | "requestContext": { 27 | "accountId": "123456789012", 28 | "resourceId": "123456", 29 | "stage": "prod", 30 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 31 | "identity": { 32 | "cognitoIdentityPoolId": null, 33 | "accountId": null, 34 | "cognitoIdentityId": null, 35 | "caller": null, 36 | "apiKey": null, 37 | "sourceIp": "127.0.0.1", 38 | "cognitoAuthenticationType": null, 39 | "cognitoAuthenticationProvider": null, 40 | "userArn": null, 41 | "userAgent": "Custom User Agent String", 42 | "user": null 43 | }, 44 | "resourcePath": "/{proxy+}", 45 | "httpMethod": "PUT", 46 | "apiId": "1234567890" 47 | } 48 | } -------------------------------------------------------------------------------- /hexagonal-architectures/events/create-product.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"id\":\"cdc73f9d-aea9-11e3-9d5a-835b769c0d9c\",\"name\":\"product cdc73f9d-aea9-11e3-9d5a-835b769c0d9c\",\"price\":100.20}", 3 | "resource": "/{proxy+}", 4 | "path": "/product", 5 | "httpMethod": "PUT", 6 | "headers": { 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 8 | "Accept-Encoding": "gzip, deflate, sdch", 9 | "Accept-Language": "en-US,en;q=0.8", 10 | "Cache-Control": "max-age=0", 11 | "CloudFront-Forwarded-Proto": "https", 12 | "CloudFront-Is-Desktop-Viewer": "true", 13 | "CloudFront-Is-Mobile-Viewer": "false", 14 | "CloudFront-Is-SmartTV-Viewer": "false", 15 | "CloudFront-Is-Tablet-Viewer": "false", 16 | "CloudFront-Viewer-Country": "US", 17 | "Host": "1234567890.execute-api.{dns_suffix}", 18 | "Upgrade-Insecure-Requests": "1", 19 | "User-Agent": "Custom User Agent String", 20 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 21 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 22 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 23 | "X-Forwarded-Port": "443", 24 | "X-Forwarded-Proto": "https" 25 | }, 26 | "requestContext": { 27 | "accountId": "123456789012", 28 | "resourceId": "123456", 29 | "stage": "prod", 30 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 31 | "identity": { 32 | "cognitoIdentityPoolId": null, 33 | "accountId": null, 34 | "cognitoIdentityId": null, 35 | "caller": null, 36 | "apiKey": null, 37 | "sourceIp": "127.0.0.1", 38 | "cognitoAuthenticationType": null, 39 | "cognitoAuthenticationProvider": null, 40 | "userArn": null, 41 | "userAgent": "Custom User Agent String", 42 | "user": null 43 | }, 44 | "resourcePath": "/{proxy+}", 45 | "httpMethod": "PUT", 46 | "apiId": "1234567890" 47 | } 48 | } -------------------------------------------------------------------------------- /hexagonal-architectures/events/delete-product.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"test\":\"body\"}", 3 | "resource": "/{proxy+}", 4 | "path": "/product", 5 | "httpMethod": "DELETE", 6 | "pathParameters": { 7 | "id": "100" 8 | }, 9 | "headers": { 10 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 11 | "Accept-Encoding": "gzip, deflate, sdch", 12 | "Accept-Language": "en-US,en;q=0.8", 13 | "Cache-Control": "max-age=0", 14 | "CloudFront-Forwarded-Proto": "https", 15 | "CloudFront-Is-Desktop-Viewer": "true", 16 | "CloudFront-Is-Mobile-Viewer": "false", 17 | "CloudFront-Is-SmartTV-Viewer": "false", 18 | "CloudFront-Is-Tablet-Viewer": "false", 19 | "CloudFront-Viewer-Country": "US", 20 | "Host": "1234567890.execute-api.{dns_suffix}", 21 | "Upgrade-Insecure-Requests": "1", 22 | "User-Agent": "Custom User Agent String", 23 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 24 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 25 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 26 | "X-Forwarded-Port": "443", 27 | "X-Forwarded-Proto": "https" 28 | }, 29 | "requestContext": { 30 | "accountId": "123456789012", 31 | "resourceId": "123456", 32 | "stage": "prod", 33 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 34 | "identity": { 35 | "cognitoIdentityPoolId": null, 36 | "accountId": null, 37 | "cognitoIdentityId": null, 38 | "caller": null, 39 | "apiKey": null, 40 | "sourceIp": "127.0.0.1", 41 | "cognitoAuthenticationType": null, 42 | "cognitoAuthenticationProvider": null, 43 | "userArn": null, 44 | "userAgent": "Custom User Agent String", 45 | "user": null 46 | }, 47 | "resourcePath": "/{proxy+}", 48 | "httpMethod": "DELETE", 49 | "apiId": "1234567890" 50 | } 51 | } -------------------------------------------------------------------------------- /hexagonal-architectures/events/get-product.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/{proxy+}", 3 | "path": "/product", 4 | "httpMethod": "GET", 5 | "pathParameters": { 6 | "id": "5" 7 | }, 8 | "headers": { 9 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 10 | "Accept-Encoding": "gzip, deflate, sdch", 11 | "Accept-Language": "en-US,en;q=0.8", 12 | "Cache-Control": "max-age=0", 13 | "CloudFront-Forwarded-Proto": "https", 14 | "CloudFront-Is-Desktop-Viewer": "true", 15 | "CloudFront-Is-Mobile-Viewer": "false", 16 | "CloudFront-Is-SmartTV-Viewer": "false", 17 | "CloudFront-Is-Tablet-Viewer": "false", 18 | "CloudFront-Viewer-Country": "US", 19 | "Host": "1234567890.execute-api.{dns_suffix}", 20 | "Upgrade-Insecure-Requests": "1", 21 | "User-Agent": "Custom User Agent String", 22 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 23 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 24 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 25 | "X-Forwarded-Port": "443", 26 | "X-Forwarded-Proto": "https" 27 | }, 28 | "requestContext": { 29 | "accountId": "123456789012", 30 | "resourceId": "123456", 31 | "stage": "prod", 32 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 33 | "identity": { 34 | "cognitoIdentityPoolId": null, 35 | "accountId": null, 36 | "cognitoIdentityId": null, 37 | "caller": null, 38 | "apiKey": null, 39 | "sourceIp": "127.0.0.1", 40 | "cognitoAuthenticationType": null, 41 | "cognitoAuthenticationProvider": null, 42 | "userArn": null, 43 | "userAgent": "Custom User Agent String", 44 | "user": null 45 | }, 46 | "resourcePath": "/{proxy+}", 47 | "httpMethod": "GET", 48 | "apiId": "1234567890" 49 | } 50 | } -------------------------------------------------------------------------------- /hexagonal-architectures/events/get-products.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/{proxy+}", 3 | "path": "/product", 4 | "httpMethod": "GET", 5 | "headers": { 6 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 7 | "Accept-Encoding": "gzip, deflate, sdch", 8 | "Accept-Language": "en-US,en;q=0.8", 9 | "Cache-Control": "max-age=0", 10 | "CloudFront-Forwarded-Proto": "https", 11 | "CloudFront-Is-Desktop-Viewer": "true", 12 | "CloudFront-Is-Mobile-Viewer": "false", 13 | "CloudFront-Is-SmartTV-Viewer": "false", 14 | "CloudFront-Is-Tablet-Viewer": "false", 15 | "CloudFront-Viewer-Country": "US", 16 | "Host": "1234567890.execute-api.{dns_suffix}", 17 | "Upgrade-Insecure-Requests": "1", 18 | "User-Agent": "Custom User Agent String", 19 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 20 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 21 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 22 | "X-Forwarded-Port": "443", 23 | "X-Forwarded-Proto": "https" 24 | }, 25 | "requestContext": { 26 | "accountId": "123456789012", 27 | "resourceId": "123456", 28 | "stage": "prod", 29 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 30 | "identity": { 31 | "cognitoIdentityPoolId": null, 32 | "accountId": null, 33 | "cognitoIdentityId": null, 34 | "caller": null, 35 | "apiKey": null, 36 | "sourceIp": "127.0.0.1", 37 | "cognitoAuthenticationType": null, 38 | "cognitoAuthenticationProvider": null, 39 | "userArn": null, 40 | "userAgent": "Custom User Agent String", 41 | "user": null 42 | }, 43 | "resourcePath": "/{proxy+}", 44 | "httpMethod": "GET", 45 | "apiId": "1234567890" 46 | } 47 | } -------------------------------------------------------------------------------- /hexagonal-architectures/images/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/hexagonal-architectures/images/diagram.png -------------------------------------------------------------------------------- /hexagonal-architectures/images/hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/hexagonal-architectures/images/hex.png -------------------------------------------------------------------------------- /hexagonal-architectures/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /hexagonal-architectures/junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /hexagonal-architectures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-typescript-demo", 3 | "version": "0.1.0", 4 | "bin": { 5 | "serverless-typescript-demo": "bin/serverless-typescript-demo.js" 6 | }, 7 | "scripts": { 8 | "build": "webpack-cli", 9 | "watch": "webpack-cli -w", 10 | "test": "jest", 11 | "test:coverage": "jest --coverage", 12 | "test:watch": "jest --watch" 13 | }, 14 | "devDependencies": { 15 | "@types/aws-lambda": "^8.10.149", 16 | "@types/jest": "^27.5.0", 17 | "@types/node": "^22.15.29", 18 | "@types/uuid": "^9.0.8", 19 | "aws-sam-webpack-plugin": "^0.15.1", 20 | "jest": "^27.5.1", 21 | "ts-jest": "^27.1.4", 22 | "ts-loader": "^9.5.2", 23 | "typescript": "^4.9.5", 24 | "webpack": "^5.99.9", 25 | "webpack-cli": "^5.1.4" 26 | }, 27 | "dependencies": { 28 | "@aws-sdk/client-dynamodb": "^3.826.0", 29 | "@aws-sdk/lib-dynamodb": "^3.828.0", 30 | "aws-sdk": "^2.1692.0", 31 | "prettier": "^3.5.3", 32 | "source-map-support": "^0.5.21", 33 | "ts-json-object": "^0.4.1", 34 | "uuid": "^11.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hexagonal-architectures/src/adapters/event-bridge.ts: -------------------------------------------------------------------------------- 1 | import { EventBridgeHandler } from "aws-lambda"; 2 | import { CreateProduct } from "../domain/Products"; 3 | import { Product } from "../model/product"; 4 | 5 | enum ProductEventTypes { 6 | CreationRequested = "ProductCreationRequested", 7 | Created = "ProductCreated", 8 | Deleted = "ProductDeleted" 9 | } 10 | 11 | export const CreateProductEBAdapter = (next: CreateProduct): EventBridgeHandler => async (event) => { 12 | try { 13 | 14 | if (event["detail-type"] === ProductEventTypes.CreationRequested) { 15 | 16 | const product = new Product(event.detail); 17 | 18 | try { 19 | await next(product); 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | } 24 | else { 25 | console.warn(`Product not provided`); 26 | } 27 | } catch (error) { 28 | console.error(error); 29 | } 30 | } -------------------------------------------------------------------------------- /hexagonal-architectures/src/domain/Products.ts: -------------------------------------------------------------------------------- 1 | import { ProductStore } from "../store/product-store"; 2 | import { Product } from "../model/product"; 3 | 4 | export type GetProduct = (id: string) => Promise; 5 | export type GetProducts = () => Promise; 6 | export type DeleteProduct = (id: string) => Promise; 7 | export type CreateProduct = (product: Product) => Promise; 8 | 9 | export const getProduct = (store: ProductStore): GetProduct => async (id: string): Promise => { 10 | try { 11 | console.info(`Fetching product ${id}`) 12 | const result = await store.getProduct(id); 13 | if (!result) { 14 | console.warn(`No product with id: ${id}`); 15 | return {} as Product; 16 | } 17 | 18 | return result; 19 | 20 | } catch (error) { 21 | console.error(error); 22 | throw error; 23 | } 24 | } 25 | 26 | export const getProducts = (store: ProductStore): GetProducts => async (): Promise => { 27 | try { 28 | const result = await store.getProducts(); 29 | if (!result) { 30 | console.warn(`No products available.`); 31 | return []; 32 | } 33 | 34 | return result; 35 | 36 | } catch (error) { 37 | console.error(error); 38 | throw error; 39 | } 40 | } 41 | 42 | export const deleteProduct = (store: ProductStore): DeleteProduct => async (id: string): Promise => { 43 | try { 44 | console.info(`Deleting product ${id}`) 45 | await store.deleteProduct(id); 46 | } catch (error) { 47 | console.error(error); 48 | throw error; 49 | } 50 | } 51 | 52 | export const createProduct = (store: ProductStore): CreateProduct => async (product: Product): Promise => { 53 | try { 54 | console.info(`Adding product with Id: ${product.id}`) 55 | await store.putProduct(product); 56 | console.info(`Product '${product.id}' successfully added`); 57 | } catch (error) { 58 | console.error(error); 59 | throw error; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /hexagonal-architectures/src/functions/delete-product/delete-product.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDbStore } from "../../store/dynamodb/dynamodb-store"; 2 | import { deleteProduct } from "../../domain/Products"; 3 | import { DeleteProductAPIGWAdapter } from "../../adapters/api-gateway"; 4 | 5 | const tableName = process.env.TABLE; 6 | 7 | const store = new DynamoDbStore(tableName!); 8 | const domain = deleteProduct(store); 9 | 10 | export const handler = DeleteProductAPIGWAdapter(domain); -------------------------------------------------------------------------------- /hexagonal-architectures/src/functions/get-product/get-product.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDbStore } from "../../store/dynamodb/dynamodb-store"; 2 | import { getProduct } from "../../domain/Products"; 3 | import { GetProductAPIGWAdapter } from "../../adapters/api-gateway"; 4 | 5 | const tableName = process.env.TABLE; 6 | 7 | const store = new DynamoDbStore(tableName!); 8 | const domain = getProduct(store); 9 | 10 | export const handler = GetProductAPIGWAdapter(domain); 11 | 12 | -------------------------------------------------------------------------------- /hexagonal-architectures/src/functions/get-products/get-products.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDbStore } from "../../store/dynamodb/dynamodb-store"; 2 | import { getProducts } from "../../domain/Products"; 3 | import { GetProductsAPIGWAdapter } from "../../adapters/api-gateway"; 4 | 5 | const tableName = process.env.TABLE; 6 | 7 | const store = new DynamoDbStore(tableName!); 8 | const domain = getProducts(store); 9 | 10 | export const handler = GetProductsAPIGWAdapter(domain); -------------------------------------------------------------------------------- /hexagonal-architectures/src/functions/put-product/put-product.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDbStore } from "../../store/dynamodb/dynamodb-store"; 2 | import { createProduct } from "../../domain/Products"; 3 | import { CreateProductAPIGWAdapter } from "../../adapters/api-gateway"; 4 | // import { CreateProductEBAdapter } from "../../adapters/event-bridge"; 5 | 6 | const tableName = process.env.TABLE; 7 | 8 | const store = new DynamoDbStore(tableName!); 9 | const domain = createProduct(store); 10 | 11 | export const handler = CreateProductAPIGWAdapter(domain); 12 | // export const handler = CreateProductEBAdapter(domain); -------------------------------------------------------------------------------- /hexagonal-architectures/src/model/product.ts: -------------------------------------------------------------------------------- 1 | import { JSONObject, required } from "ts-json-object" 2 | 3 | export class Product extends JSONObject { 4 | 5 | @required 6 | // @ts-ignore 7 | id:string 8 | @required 9 | // @ts-ignore 10 | name:string 11 | @required 12 | // @ts-ignore 13 | price:number 14 | } -------------------------------------------------------------------------------- /hexagonal-architectures/src/store/product-store.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "../model/product"; 2 | 3 | export interface ProductStore { 4 | getProduct: (id: string) => Promise; 5 | putProduct: (product: Product) => Promise; 6 | deleteProduct: (id: string) => Promise; 7 | getProducts: () => Promise; 8 | } -------------------------------------------------------------------------------- /hexagonal-architectures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | } 14 | } -------------------------------------------------------------------------------- /hexagonal-architectures/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const AwsSamPlugin = require("aws-sam-webpack-plugin"); 3 | 4 | const awsSamPlugin = new AwsSamPlugin(); 5 | 6 | module.exports = { 7 | // Loads the entry object from the AWS::Serverless::Function resources in your 8 | // SAM config. Setting this to a function will 9 | entry: () => awsSamPlugin.entry(), 10 | 11 | // Write the output to the .aws-sam/build folder 12 | output: { 13 | filename: (chunkData) => awsSamPlugin.filename(chunkData), 14 | libraryTarget: "commonjs2", 15 | path: path.resolve(".") 16 | }, 17 | 18 | // Create source maps 19 | devtool: "source-map", 20 | 21 | // Resolve .ts and .js extensions 22 | resolve: { 23 | extensions: [".ts", ".js"] 24 | }, 25 | 26 | // Target node 27 | target: "node", 28 | 29 | // AWS recommends always including the aws-sdk in your Lambda package but excluding can significantly reduce 30 | // the size of your deployment package. If you want to always include it then comment out this line. It has 31 | // been included conditionally because the node10.x docker image used by SAM local doesn't include it. 32 | externals: process.env.NODE_ENV === "development" ? [] : ["aws-sdk"], 33 | 34 | // Set the webpack mode 35 | mode: process.env.NODE_ENV || "production", 36 | 37 | // Add the TypeScript loader 38 | module: { 39 | rules: [{ test: /\.tsx?$/, loader: "ts-loader" }] 40 | }, 41 | 42 | // Add the AWS SAM Webpack plugin 43 | plugins: [awsSamPlugin] 44 | }; -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project! 2 | 3 | This is a blank project for TypeScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import { CdkStack } from '../lib/cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | new CdkStack(app, 'CdkStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); 22 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, 15 | "@aws-cdk/aws-lambda:recognizeVersionProps": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/assert": "1.203.0", 15 | "@types/jest": "^26.0.10", 16 | "@types/node": "10.17.27", 17 | "aws-cdk": "^1.148.0", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.1.5", 20 | "ts-node": "^9.0.0", 21 | "typescript": "~3.9.7" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-appconfig": "^1.117.0", 25 | "@aws-cdk/aws-s3-assets": "^1.117.0", 26 | "@aws-cdk/core": "1.117.0", 27 | "source-map-support": "^0.5.16" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as Cdk from '../lib/cdk-stack'; 4 | 5 | test('Empty Stack', () => { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new Cdk.CdkStack(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cfn/feature-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "premium_features": { 3 | "default": false, 4 | "rules": { 5 | "customer tier equals premium": { 6 | "when_match": true, 7 | "conditions": [ 8 | { 9 | "action": "EQUALS", 10 | "key": "tier", 11 | "value": "premium" 12 | } 13 | ] 14 | } 15 | } 16 | }, 17 | "feature2": { 18 | "default": true 19 | } 20 | } -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app-config-store/cfn/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: A sample template 3 | Resources: 4 | FeatureStoreApp: 5 | Type: AWS::AppConfig::Application 6 | Properties: 7 | Description: "AppConfig Appliction for feature toggles" 8 | Name: my-app 9 | 10 | FeatureStoreDevEnv: 11 | Type: AWS::AppConfig::Environment 12 | Properties: 13 | ApplicationId: !Ref FeatureStoreApp 14 | Description: "Development Environment for the App Config Store" 15 | Name: "development" 16 | 17 | FeatureStoreConfigProfile: 18 | Type: AWS::AppConfig::ConfigurationProfile 19 | Properties: 20 | ApplicationId: !Ref FeatureStoreApp 21 | Name: "MyTestProfile" 22 | LocationUri: "hosted" 23 | 24 | HostedConfigVersion: 25 | Type: AWS::AppConfig::HostedConfigurationVersion 26 | Properties: 27 | ApplicationId: !Ref FeatureStoreApp 28 | ConfigurationProfileId: !Ref FeatureStoreConfigProfile 29 | Description: 'A sample hosted configuration version' 30 | Content: | 31 | { 32 | "premium_features": { 33 | "default": false, 34 | "rules": { 35 | "customer tier equals premium": { 36 | "when_match": true, 37 | "conditions": [ 38 | { 39 | "action": "EQUALS", 40 | "key": "tier", 41 | "value": "premium" 42 | } 43 | ] 44 | } 45 | } 46 | }, 47 | "feature2": { 48 | "default": true 49 | } 50 | } 51 | ContentType: 'application/json' 52 | 53 | ConfigDeployment: 54 | Type: AWS::AppConfig::Deployment 55 | Properties: 56 | ApplicationId: !Ref FeatureStoreApp 57 | ConfigurationProfileId: !Ref FeatureStoreConfigProfile 58 | ConfigurationVersion: !Ref HostedConfigVersion 59 | DeploymentStrategyId: "AppConfig.AllAtOnce" 60 | EnvironmentId: !Ref FeatureStoreDevEnv -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = */build/*,tests/* 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | raise NotImplementedError.* 8 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | 7 | [packages] 8 | boto3 = "*" 9 | aws-lambda-powertools = "*" 10 | 11 | [dev-packages] 12 | boto3 = "*" 13 | pytest = "*" 14 | pytest-cov = "*" 15 | requests = "*" 16 | 17 | [requires] 18 | python_version = "3.8" 19 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/lambda-powertools-feature-flags/app/products/__init__.py -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/products/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | aws-lambda-powertools -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | [default] 3 | [default.deploy] 4 | [default.deploy.parameters] 5 | stack_name = "sam-product-app" 6 | s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-18j4nbwfp8srq" 7 | s3_prefix = "sam-product-app" 8 | region = "eu-central-1" 9 | capabilities = "CAPABILITY_IAM" 10 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from uuid import uuid4 3 | 4 | import pytest 5 | 6 | 7 | class MockContext(object): 8 | 9 | def __init__(self, function_name): 10 | self.function_name = function_name 11 | self.function_version = "v$LATEST" 12 | self.memory_limit_in_mb = 512 13 | self.invoked_function_arn = f"arn:aws:lambda:us-east-1:ACCOUNT:function:{self.function_name}" 14 | self.aws_request_id = str(uuid4) 15 | 16 | 17 | @pytest.fixture 18 | def lambda_context(): 19 | return MockContext("dummy_function") 20 | 21 | 22 | @pytest.fixture() 23 | def apigw_event(): 24 | """ Generates API GW Event""" 25 | with open("./events/hello_world_event.json", "r") as fp: 26 | return json.load(fp) 27 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/tests/sytem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/lambda-powertools-feature-flags/app/tests/sytem/__init__.py -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/tests/sytem/test_app.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import requests 3 | 4 | boto3.setup_default_session(profile_name='amelnyk-Admin') 5 | 6 | REGION = 'eu-central-1' 7 | STACK_NAME = 'sam-product-app' 8 | 9 | 10 | def get_api_gw_url(): 11 | cfn = boto3.resource('cloudformation') 12 | stack = cfn.Stack(STACK_NAME) 13 | return next(x["OutputValue"] for x in stack.outputs if x["OutputKey"] == "ProductsApigwURL") 14 | 15 | 16 | def test_call_api_returns_products(): 17 | api_url = get_api_gw_url() 18 | products = requests.get(f"{api_url}/products").json() 19 | assert len(products) == 2 20 | 21 | 22 | def test_call_api_return_product_when_given_id(): 23 | api_url = get_api_gw_url() 24 | product = requests.get(f"{api_url}/products/1").json() 25 | assert product == [{"productId": "1", "name": "Mechanical Keyboard 9000", "price": 3549, "discount": "10%"}] 26 | 27 | 28 | def test_call_api_return_empty_when_no_id_given(): 29 | api_url = get_api_gw_url() 30 | resp = requests.get(f"{api_url}/products/3").json() 31 | assert resp == [] 32 | 33 | -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/app/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/lambda-powertools-feature-flags/app/tests/unit/__init__.py -------------------------------------------------------------------------------- /lambda-powertools-feature-flags/docs/feature_toggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/lambda-powertools-feature-flags/docs/feature_toggles.png -------------------------------------------------------------------------------- /latest_episode.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest_episode": "https://www.twitch.tv/videos/1519229030" 3 | } 4 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/.gitignore: -------------------------------------------------------------------------------- 1 | accountdetails/dist/ 2 | accountdetails/node_modules 3 | 4 | appshell/dist/ 5 | appshell/node_modules 6 | 7 | catalogue/dist/ 8 | catalogue/node_modules 9 | 10 | myaccount/dist/ 11 | myaccount/node_modules 12 | 13 | paymentdetails/dist/ 14 | paymentdetails/node_modules 15 | 16 | signin/dist/ 17 | signin/node_modules -------------------------------------------------------------------------------- /micro-frontends-module-federation/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/README.md: -------------------------------------------------------------------------------- 1 | # Micro-Frontends with Module Federation 2 | 3 | ## Micro-Frontends definition 4 | 5 | Micro-Frontends are the **technical representation of a business subdomain**, they allow **independent implementations** with the **same or different technology**. 6 | 7 | Finally, they should **minimize the code shared** with other subdomains and they are **own by a single team** 8 | 9 | ### Module Federation 10 | 11 | [Module federation](https://webpack.js.org/concepts/module-federation/) is a webpack plugin. It allows a JavaScript application to dynamically run code from another bundle/build, on both client and server. 12 | 13 | ![Module Federation](modFedExample.png) 14 | 15 | ### UI example 16 | In this example we are going to create: 17 | 18 | - an application shell for hosting all the micro-frontends 19 | - a sign in micro-frontend 20 | - a catalogue micro-frontend with multiple views 21 | - a my account micro-frontend that is loading 2 micro-frontends: account details and payments details 22 | 23 | ![Module Federation](modFedDiagram.png) -------------------------------------------------------------------------------- /micro-frontends-module-federation/accountdetails/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catalogue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-cli serve", 8 | "build": "webpack --mode production", 9 | "serve": "serve dist -p 3005", 10 | "clean": "rm -rf dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "react": "^18.0.2", 17 | "react-dom": "^18.0.2" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.13.15", 21 | "@babel/preset-react": "^7.13.13", 22 | "babel-loader": "^9.1.0", 23 | "html-webpack-plugin": "^5.3.1", 24 | "serve": "^14.2.3", 25 | "webpack": "^5.33.2", 26 | "webpack-cli": "^5.1.4", 27 | "webpack-dev-server": "^5.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/accountdetails/src/AccountDetails.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | 3 | const AccountDetails = (props) => { 4 | 5 | const [lastPaymentDate, setPaymentChanged] = useState("Jan 2021") 6 | 7 | props.emitter.on("paymentChanged", date => setPaymentChanged(date)) 8 | 9 | return ( 10 |
11 |

Account Details

12 |
    13 |
  • name: Luca
  • 14 |
  • surname: Mezzalira
  • 15 |
  • email: guesswho@lm.com
  • 16 |
  • member since: Jan 2021
  • 17 |
  • last payment changed: {lastPaymentDate}
  • 18 |
  • Change account details
  • 19 |
20 |
21 | 22 | ); 23 | } 24 | 25 | export default AccountDetails; 26 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/accountdetails/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { ModuleFederationPlugin } = require("webpack").container; 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./src/AccountDetails", 6 | mode: "development", 7 | devServer: { 8 | static: { 9 | directory: path.join(__dirname, "dist"), 10 | }, 11 | port: 3005, 12 | }, 13 | output: { 14 | publicPath: "auto", 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | loader: "babel-loader", 21 | exclude: /node_modules/, 22 | options: { 23 | presets: ["@babel/preset-react"], 24 | }, 25 | }, 26 | ], 27 | }, 28 | plugins: [ 29 | new ModuleFederationPlugin({ 30 | name: "AccountDetails", 31 | filename: "remoteEntry.js", 32 | exposes:{ 33 | "./AccountDetails": "./src/AccountDetails" 34 | }, 35 | shared: { 36 | "@material-ui/core": { 37 | singleton: true, 38 | }, 39 | "@material-ui/styles": { 40 | singleton: true 41 | }, 42 | "react-dom": { 43 | singleton: true, 44 | }, 45 | react: { 46 | singleton: true, 47 | }, 48 | }, 49 | }) 50 | ], 51 | }; -------------------------------------------------------------------------------- /micro-frontends-module-federation/appshell/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appshell", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-cli serve", 8 | "build": "webpack --mode production", 9 | "serve": "serve dist -p 3001", 10 | "clean": "rm -rf dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@material-ui/core": "^4.12.4", 17 | "@material-ui/icons": "^4.11.3", 18 | "clsx": "^2.1.1", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "react-router-dom": "^6.28.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.26.9", 25 | "@babel/preset-react": "^7.26.3", 26 | "atriom-plugin": "^1.1.2", 27 | "babel-loader": "^9.2.1", 28 | "html-webpack-plugin": "^5.6.3", 29 | "serve": "^13.0.4", 30 | "webpack": "^5.98.0", 31 | "webpack-bundle-analyzer": "^4.10.2", 32 | "webpack-cli": "^5.1.4", 33 | "webpack-dev-server": "^5.2.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/appshell/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/appshell/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {BrowserRouter as Router} from "react-router-dom"; 3 | import Main from "./Main"; 4 | 5 | const App = () => { 6 | return( 7 | 8 |
9 |
10 | ) 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/appshell/src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | ReactDOM.render(, document.getElementById("root")); -------------------------------------------------------------------------------- /micro-frontends-module-federation/appshell/src/index.js: -------------------------------------------------------------------------------- 1 | import("./bootstrap"); -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/imgs/black1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/micro-frontends-module-federation/catalogue/imgs/black1.jpeg -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/imgs/black2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/micro-frontends-module-federation/catalogue/imgs/black2.jpeg -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/imgs/white.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/micro-frontends-module-federation/catalogue/imgs/white.jpeg -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catalogue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-cli serve", 8 | "build": "webpack --mode production", 9 | "serve": "serve dist -p 3002", 10 | "clean": "rm -rf dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@material-ui/core": "^4.11.3", 17 | "@material-ui/icons": "^4.11.2", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-router-dom": "^6.3.0" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.13.15", 24 | "@babel/preset-react": "^7.13.13", 25 | "babel-loader": "^9.1.0", 26 | "copy-webpack-plugin": "^12.0.2", 27 | "html-webpack-plugin": "^5.3.1", 28 | "serve": "^14.2.3", 29 | "webpack": "^5.33.2", 30 | "webpack-cli": "^5.1.4", 31 | "webpack-dev-server": "^5.0.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/src/Catalogue.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Switch, Route, useRouteMatch} from "react-router-dom"; 3 | import Home from "./Home"; 4 | import Details from "./Details"; 5 | 6 | 7 | const Catalogue = () => { 8 | let { path } = useRouteMatch(); 9 | 10 | return( 11 |
12 |

13 | Shop 14 |

15 | 16 | 17 | 18 | 19 |
20 | ) 21 | } 22 | 23 | export default Catalogue; -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/src/Details.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Link, useParams} from "react-router-dom"; 3 | 4 | const Details = () => { 5 | 6 | const {productId} = useParams() 7 | 8 | return( 9 |
10 | {`Details page, product id: ${productId}`} 11 |
12 | All products 13 |
14 | ) 15 | } 16 | 17 | export default Details; -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/src/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {useRouteMatch} from "react-router-dom"; 3 | import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'; 4 | import Product from "./Product"; 5 | 6 | const catalogueData = [ 7 | { 8 | id: 123, 9 | productName: "Black no.1 T-Shirt", 10 | image: "http://localhost:3002/imgs/black1.jpeg", 11 | cost: "$3.99" 12 | }, 13 | { 14 | id: 234, 15 | productName: "Black no.2 T-Shirt", 16 | image: "http://localhost:3002/imgs/black2.jpeg", 17 | cost: "$4.99" 18 | }, 19 | { 20 | id: 456, 21 | productName: "White T-Shirt", 22 | image: "http://localhost:3002/imgs/white.jpeg", 23 | cost: "$5.99" 24 | } 25 | ] 26 | 27 | const generateClassName = createGenerateClassName({ 28 | seed:'catalogue' 29 | }); 30 | 31 | const Home = () => { 32 | let { path } = useRouteMatch(); 33 | 34 | return( 35 | 36 |
37 | { 38 | catalogueData.map(item => { 39 | return ( 40 | 41 | ) 42 | }) 43 | } 44 |
45 |
46 | ) 47 | } 48 | 49 | export default Home; -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/src/Product.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import Paper from '@material-ui/core/Paper'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import InfoIcon from '@material-ui/icons/Info'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | paper: { 10 | margin: 10, 11 | float: "left", 12 | padding: 8, 13 | textAlign: "center", 14 | width: 330, 15 | height: 350 16 | } 17 | })); 18 | 19 | const Product = (props) => { 20 | const classes = useStyles(); 21 | let history = useHistory(); 22 | 23 | const productDetails = () => { 24 | history.push(props.url); 25 | } 26 | 27 | return( 28 | 29 | 30 |
31 |

32 | {props.data.productName} 33 |

34 | {props.data.cost} 35 | 36 | 37 | 38 |
39 |
40 | ) 41 | } 42 | 43 | export default Product; -------------------------------------------------------------------------------- /micro-frontends-module-federation/catalogue/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { ModuleFederationPlugin } = require("webpack").container; 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const path = require("path"); 4 | 5 | module.exports = { 6 | entry: "./src/Catalogue", 7 | mode: "development", 8 | devServer: { 9 | static: { 10 | directory: path.join(__dirname, "dist"), 11 | }, 12 | port: 3002, 13 | }, 14 | output: { 15 | publicPath: "auto", 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.jsx?$/, 21 | loader: "babel-loader", 22 | exclude: /node_modules/, 23 | options: { 24 | presets: ["@babel/preset-react"], 25 | }, 26 | }, 27 | ], 28 | }, 29 | plugins: [ 30 | new CopyPlugin({ 31 | patterns: [ 32 | { from: "imgs", to: "imgs" } 33 | ], 34 | }), 35 | new ModuleFederationPlugin({ 36 | name: "Catalogue", 37 | filename: "remoteEntry.js", 38 | exposes:{ 39 | "./Catalogue": "./src/Catalogue" 40 | }, 41 | shared: { 42 | "@material-ui/core": { 43 | singleton: true, 44 | }, 45 | "@material-ui/styles": { 46 | singleton: true 47 | }, 48 | "react-router-dom": { 49 | singleton: true, 50 | }, 51 | "react-dom": { 52 | singleton: true, 53 | }, 54 | react: { 55 | singleton: true, 56 | }, 57 | }, 58 | }) 59 | ], 60 | }; -------------------------------------------------------------------------------- /micro-frontends-module-federation/modFedDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/micro-frontends-module-federation/modFedDiagram.png -------------------------------------------------------------------------------- /micro-frontends-module-federation/modFedExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/micro-frontends-module-federation/modFedExample.png -------------------------------------------------------------------------------- /micro-frontends-module-federation/myaccount/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myaccount", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-cli serve", 8 | "build": "webpack --mode production", 9 | "serve": "serve dist -p 3004", 10 | "clean": "rm -rf dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "nanoevents": "^7.0.1", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "react-router-dom": "^6.28.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.26.9", 23 | "@babel/preset-react": "^7.26.3", 24 | "babel-loader": "^9.2.1", 25 | "html-webpack-plugin": "^5.6.3", 26 | "serve": "^14.2.4", 27 | "webpack": "^5.98.0", 28 | "webpack-cli": "^5.1.4", 29 | "webpack-dev-server": "^5.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/myaccount/src/MyAccount.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Link} from "react-router-dom"; 3 | import {createNanoEvents} from "nanoevents"; 4 | const AccountDetails = React.lazy(() => import("AccountDetails/AccountDetails")); 5 | const PaymentDetails = React.lazy(() => import("PaymentDetails/PaymentDetails")); 6 | 7 | const AuthenticatedView = (props) => { 8 | 9 | return( 10 | 11 | 12 |
13 | 14 |
15 | ) 16 | } 17 | 18 | const MyAccount = () => { 19 | const token = window.sessionStorage.getItem("token"); 20 | let view; 21 | if(token){ 22 | const emitter = createNanoEvents(); 23 | view = 24 | } else { 25 | view =

Please sign in before accessing this section

26 | } 27 | 28 | return( 29 |
30 |

31 | My Account 32 |

33 |

34 | {view} 35 |

36 |
37 | ) 38 | } 39 | 40 | export default MyAccount; -------------------------------------------------------------------------------- /micro-frontends-module-federation/myaccount/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { ModuleFederationPlugin } = require("webpack").container; 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./src/MyAccount", 6 | mode: "development", 7 | devServer: { 8 | static: { 9 | directory: path.join(__dirname, "dist"), 10 | }, 11 | port: 3004, 12 | }, 13 | output: { 14 | publicPath: "auto", 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | loader: "babel-loader", 21 | exclude: /node_modules/, 22 | options: { 23 | presets: ["@babel/preset-react"], 24 | }, 25 | }, 26 | ], 27 | }, 28 | plugins: [ 29 | new ModuleFederationPlugin({ 30 | name: "MyAccount", 31 | filename: "remoteEntry.js", 32 | exposes:{ 33 | "./MyAccount": "./src/MyAccount" 34 | }, 35 | remotes: { 36 | AccountDetails: "AccountDetails@http://localhost:3005/remoteEntry.js", 37 | PaymentDetails: "PaymentDetails@http://localhost:3006/remoteEntry.js" 38 | }, 39 | shared: { 40 | "@material-ui/core": { 41 | singleton: true, 42 | }, 43 | "@material-ui/styles": { 44 | singleton: true 45 | }, 46 | "react-router-dom": { 47 | singleton: true, 48 | }, 49 | "react-dom": { 50 | singleton: true, 51 | }, 52 | react: { 53 | singleton: true, 54 | } 55 | } 56 | }) 57 | ], 58 | }; -------------------------------------------------------------------------------- /micro-frontends-module-federation/paymentdetails/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptiondetails", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-cli serve", 8 | "build": "webpack --mode production", 9 | "serve": "serve dist -p 3006", 10 | "clean": "rm -rf dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "react": "^18.3.0", 17 | "react-dom": "^18.3.1" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.26.10", 21 | "@babel/preset-react": "^7.26.3", 22 | "babel-loader": "^9.2.1", 23 | "html-webpack-plugin": "^5.6.3", 24 | "serve": "^13.0.4", 25 | "webpack": "^5.98.0", 26 | "webpack-cli": "^5.1.4", 27 | "webpack-dev-server": "^5.2.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/paymentdetails/src/PaymentDetails.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PaymentDetails = (props) => { 4 | 5 | const onPaymentChanged = () => { 6 | props.emitter.emit("paymentChanged", "May 2021"); 7 | } 8 | 9 | return ( 10 |
11 |

Payment Details

12 | 17 |
18 | ); 19 | } 20 | 21 | export default PaymentDetails; 22 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/paymentdetails/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { ModuleFederationPlugin } = require("webpack").container; 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./src/PaymentDetails", 6 | mode: "development", 7 | devServer: { 8 | static: { 9 | directory: path.join(__dirname, "dist"), 10 | }, 11 | port: 3006, 12 | }, 13 | output: { 14 | publicPath: "auto", 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | loader: "babel-loader", 21 | exclude: /node_modules/, 22 | options: { 23 | presets: ["@babel/preset-react"], 24 | }, 25 | }, 26 | ], 27 | }, 28 | plugins: [ 29 | new ModuleFederationPlugin({ 30 | name: "PaymentDetails", 31 | filename: "remoteEntry.js", 32 | exposes:{ 33 | "./PaymentDetails": "./src/PaymentDetails" 34 | }, 35 | shared: { 36 | "@material-ui/core": { 37 | singleton: true, 38 | }, 39 | "@material-ui/styles": { 40 | singleton: true 41 | }, 42 | "react-dom": { 43 | singleton: true, 44 | }, 45 | react: { 46 | singleton: true, 47 | }, 48 | }, 49 | }) 50 | ], 51 | }; -------------------------------------------------------------------------------- /micro-frontends-module-federation/signin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-cli serve", 8 | "build": "webpack --mode production", 9 | "serve": "serve dist -p 3003", 10 | "clean": "rm -rf dist" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@material-ui/core": "^4.11.3", 17 | "react": "^18.1.0", 18 | "react-dom": "^18.1.0", 19 | "react-router-dom": "^6.3.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.13.15", 23 | "@babel/preset-react": "^7.13.13", 24 | "babel-loader": "^9.1.0", 25 | "html-webpack-plugin": "^5.3.1", 26 | "serve": "^14.2.3", 27 | "webpack": "^5.33.2", 28 | "webpack-cli": "^5.1.4", 29 | "webpack-dev-server": "^5.0.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/signin/src/SignIn.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import Typography from '@material-ui/core/Typography'; 4 | import TextField from '@material-ui/core/TextField'; 5 | import Button from '@material-ui/core/Button'; 6 | import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles'; 7 | 8 | const faketoken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 9 | 10 | const generateClassName = createGenerateClassName({ 11 | seed:'signin' 12 | }); 13 | 14 | const SignIn = () => { 15 | let history = useHistory(); 16 | 17 | const onSignIn = () => { 18 | window.sessionStorage.setItem("token", faketoken); 19 | history.push("/shop"); 20 | } 21 | 22 | return ( 23 | 24 |
25 |
26 | 27 | Username: 28 | 29 | 30 |
31 |
32 | 33 | Password: 34 | 35 | 38 |
39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default SignIn; 47 | -------------------------------------------------------------------------------- /micro-frontends-module-federation/signin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { ModuleFederationPlugin } = require("webpack").container; 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./src/SignIn", 6 | mode: "development", 7 | devServer: { 8 | static: { 9 | directory: path.join(__dirname, "dist"), 10 | }, 11 | port: 3003, 12 | }, 13 | output: { 14 | publicPath: "auto", 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | loader: "babel-loader", 21 | exclude: /node_modules/, 22 | options: { 23 | presets: ["@babel/preset-react"], 24 | }, 25 | }, 26 | ], 27 | }, 28 | plugins: [ 29 | new ModuleFederationPlugin({ 30 | name: "SignIn", 31 | filename: "remoteEntry.js", 32 | exposes:{ 33 | "./SignIn": "./src/SignIn" 34 | }, 35 | shared: { 36 | "@material-ui/core": { 37 | singleton: true, 38 | }, 39 | "@material-ui/styles": { 40 | singleton: true 41 | }, 42 | "react-router-dom": { 43 | singleton: true, 44 | }, 45 | "react-dom": { 46 | singleton: true, 47 | }, 48 | react: { 49 | singleton: true, 50 | }, 51 | }, 52 | }) 53 | ], 54 | }; -------------------------------------------------------------------------------- /serverless-lambda-java/README.md: -------------------------------------------------------------------------------- 1 | # Serverless AWS Lambda with Java 2 | 3 | In this session Melina Schweizer, Senior Specialist Solutions Architect at AWS, will walk us through Java development on AWS Lambda. 4 | 5 | ## AWS Lambda and Java best pratices 6 | 7 | - reduce deployment and coldstarts by avoidigin heavy frameworks like spring an hibernate 8 | - reuse resources during multiple invocations by moving the initiaisation outside of the main handler 9 | - make your code more testable by moving the business logic outside of the handler 10 | 11 | ## Resources and Links 12 | 13 | - [https://medium.com/i-love-my-local-farmer-engineering-blog](https://medium.com/i-love-my-local-farmer-engineering-blog) 14 | - [https://github.com/aws-samples/i-love-my-local-farmer](https://github.com/aws-samples/i-love-my-local-farmer) 15 | - [https://medium.com/i-love-my-local-farmer-engineering-blog/how-to-use-java-in-your-db-connected-aws-lambdas-211c1f9c53aa](https://medium.com/i-love-my-local-farmer-engineering-blog/how-to-use-java-in-your-db-connected-aws-lambdas-211c1f9c53aa) 16 | - [https://docs.aws.amazon.com/lambda/latest/dg/lambda-java.html](https://docs.aws.amazon.com/lambda/latest/dg/lambda-java.html) 17 | - [https://github.com/awslabs/aws-lambda-powertools-java](https://github.com/awslabs/aws-lambda-powertools-java) 18 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/README.md: -------------------------------------------------------------------------------- 1 | # SPA blue/green deployment on AWS 2 | 3 | This repository has a demo on how to deploy a SPA (Angular) using CodePipeline, CodeBuild and 4 | a custom Step Functions that does Blue/Green Deployment. 5 | 6 | The IaC was written using [AWS CDK for Go](https://aws.amazon.com/blogs/developer/getting-started-with-the-aws-cloud-development-kit-and-go/). 7 | 8 | **NOTICE**: Go support is still in Developer Preview. This implies that APIs may 9 | change while we address early feedback from the community. We would love to hear 10 | about your experience through GitHub issues. 11 | 12 | ## Structure 13 | 14 | - `app`: the sample Angular application 15 | - `cdk`: the IaC project written in Go -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | # Googlebot uses an older version of Chrome 9 | # For additional information see: https://developers.google.com/search/docs/guides/rendering 10 | 11 | > 0.5% 12 | last 2 versions 13 | Firefox ESR 14 | not dead 15 | not IE 9-11 # For IE 9-11 support, remove 'not'. 16 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates" 21 | ], 22 | "rules": { 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "prefix": "app", 27 | "style": "kebab-case", 28 | "type": "element" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "prefix": "app", 35 | "style": "camelCase", 36 | "type": "attribute" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | .vscode/ 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.angular/cache 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | yarn-error.log 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12.3.1" 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | before_script: 10 | - yarn install --frozen-lockfile 11 | 12 | env: 13 | - NG_CLI_ANALYTICS=ci 14 | 15 | script: 16 | - yarn build 17 | 18 | deploy: 19 | provider: pages 20 | skip-cleanup: true 21 | github-token: $GITHUB_TOKEN # Set in travis-ci.org dashboard, marked secure 22 | keep-history: true 23 | on: 24 | branch: master 25 | local_dir: dist 26 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/CNAME: -------------------------------------------------------------------------------- 1 | angular.realworld.io 2 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Khaled Osman] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Ng2RealApp } from './app.po'; 2 | 3 | describe('ng-demo App', () => { 4 | let page: Ng2RealApp; 5 | 6 | beforeEach(() => { 7 | page = new Ng2RealApp(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('conduit'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class Ng2RealApp { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('.logo-font')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: ['html', 'lcovonly'], 20 | fixWebpackSourcePaths: true 21 | }, 22 | 23 | reporters: 24 | config.angularCli && config.angularCli.codeCoverage 25 | ? ['progress', 'coverage-istanbul'] 26 | : ['progress', 'kjhtml'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ['Chrome'], 32 | singleRun: false 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/logo.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/*.css", 13 | "/*.js", 14 | "/manifest.webmanifest" 15 | ] 16 | } 17 | }, { 18 | "name": "assets", 19 | "installMode": "lazy", 20 | "updateMode": "prefetch", 21 | "resources": { 22 | "files": [ 23 | "/assets/**", 24 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 25 | ] 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { QuicklinkModule, QuicklinkStrategy } from 'ngx-quicklink'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'settings', 8 | loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule) 9 | }, 10 | { 11 | path: 'profile', 12 | loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule) 13 | }, 14 | { 15 | path: 'editor', 16 | loadChildren: () => import('./editor/editor.module').then(m => m.EditorModule) 17 | }, 18 | { 19 | path: 'article', 20 | loadChildren: () => import('./article/article.module').then(m => m.ArticleModule) 21 | } 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [ 26 | QuicklinkModule, 27 | RouterModule.forRoot(routes, { 28 | // preload all modules; optionally we could 29 | // implement a custom preloading strategy for just some 30 | // of the modules (PRs welcome 😉) 31 | preloadingStrategy: QuicklinkStrategy, 32 | relativeLinkResolution: 'legacy' 33 | })], 34 | exports: [RouterModule] 35 | }) 36 | export class AppRoutingModule {} 37 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { UserService } from "./core"; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class AppComponent implements OnInit { 11 | constructor(private userService: UserService) {} 12 | 13 | ngOnInit() { 14 | this.userService.populate(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { HomeModule } from './home/home.module'; 7 | import { 8 | FooterComponent, 9 | HeaderComponent, 10 | SharedModule 11 | } from './shared'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { CoreModule } from './core/core.module'; 14 | import { ServiceWorkerModule } from '@angular/service-worker'; 15 | import { environment } from '../environments/environment'; 16 | 17 | @NgModule({ 18 | declarations: [AppComponent, FooterComponent, HeaderComponent], 19 | imports: [ 20 | BrowserModule, 21 | CoreModule, 22 | SharedModule, 23 | HomeModule, 24 | AuthModule, 25 | AppRoutingModule, 26 | ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) 27 | ], 28 | providers: [], 29 | bootstrap: [AppComponent] 30 | }) 31 | export class AppModule {} 32 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/article/article-comment.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ comment.body }} 5 |

6 |
7 | 22 |
23 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/article/article-comment.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | 3 | import { Comment, User, UserService } from '../core'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-article-comment', 8 | templateUrl: './article-comment.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ArticleCommentComponent implements OnInit, OnDestroy { 12 | constructor( 13 | private userService: UserService, 14 | private cd: ChangeDetectorRef 15 | ) {} 16 | 17 | private subscription: Subscription; 18 | 19 | @Input() comment: Comment; 20 | @Output() deleteComment = new EventEmitter(); 21 | 22 | canModify: boolean; 23 | 24 | ngOnInit() { 25 | // Load the current user's data 26 | this.subscription = this.userService.currentUser.subscribe( 27 | (userData: User) => { 28 | this.canModify = (userData.username === this.comment.author.username); 29 | this.cd.markForCheck(); 30 | } 31 | ); 32 | } 33 | 34 | ngOnDestroy() { 35 | this.subscription.unsubscribe(); 36 | } 37 | 38 | deleteClicked() { 39 | this.deleteComment.emit(true); 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/article/article-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Article, ArticlesService, UserService } from '../core'; 6 | import { catchError } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ArticleResolver implements Resolve
{ 12 | constructor( 13 | private articlesService: ArticlesService, 14 | private router: Router, 15 | private userService: UserService 16 | ) {} 17 | 18 | resolve( 19 | route: ActivatedRouteSnapshot, 20 | state: RouterStateSnapshot 21 | ): Observable { 22 | 23 | return this.articlesService.get(route.params['slug']) 24 | .pipe(catchError((err) => this.router.navigateByUrl('/'))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/article/article-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ArticleComponent } from './article.component'; 4 | import { ArticleResolver } from './article-resolver.service'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: ':slug', 9 | component: ArticleComponent, 10 | resolve: { 11 | article: ArticleResolver 12 | } 13 | } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class ArticleRoutingModule {} 21 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/article/article.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { ArticleComponent } from './article.component'; 4 | import { ArticleCommentComponent } from './article-comment.component'; 5 | import { MarkdownPipe } from './markdown.pipe'; 6 | import { SharedModule } from '../shared'; 7 | import { ArticleRoutingModule } from './article-routing.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | SharedModule, 12 | ArticleRoutingModule 13 | ], 14 | declarations: [ 15 | ArticleComponent, 16 | ArticleCommentComponent, 17 | MarkdownPipe 18 | ], 19 | 20 | providers: [ 21 | ] 22 | }) 23 | export class ArticleModule {} 24 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/article/markdown.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as marked from 'marked'; 3 | 4 | @Pipe({name: 'markdown'}) 5 | export class MarkdownPipe implements PipeTransform { 6 | transform(content: string): string { 7 | return marked(content, { sanitize: true }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AuthComponent } from './auth.component'; 4 | import { NoAuthGuard } from './no-auth-guard.service'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'login', 9 | component: AuthComponent, 10 | canActivate: [NoAuthGuard] 11 | }, 12 | { 13 | path: 'register', 14 | component: AuthComponent, 15 | canActivate: [NoAuthGuard] 16 | } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [RouterModule.forChild(routes)], 21 | exports: [RouterModule] 22 | }) 23 | export class AuthRoutingModule {} 24 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |

{{ title }}

7 |

8 | Have an account? 9 | Need an account? 10 |

11 | 12 |
13 |
14 |
15 | 21 |
22 |
23 | 28 |
29 |
30 | 35 |
36 | 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | 5 | import { Errors, UserService } from '../core'; 6 | 7 | @Component({ 8 | selector: 'app-auth-page', 9 | templateUrl: './auth.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class AuthComponent implements OnInit { 13 | authType: String = ''; 14 | title: String = ''; 15 | errors: Errors = {errors: {}}; 16 | isSubmitting = false; 17 | authForm: FormGroup; 18 | 19 | constructor( 20 | private route: ActivatedRoute, 21 | private router: Router, 22 | private userService: UserService, 23 | private fb: FormBuilder, 24 | private cd: ChangeDetectorRef 25 | ) { 26 | // use FormBuilder to create a form group 27 | this.authForm = this.fb.group({ 28 | 'email': ['', Validators.required], 29 | 'password': ['', Validators.required] 30 | }); 31 | } 32 | 33 | ngOnInit() { 34 | this.route.url.subscribe(data => { 35 | // Get the last piece of the URL (it's either 'login' or 'register') 36 | this.authType = data[data.length - 1].path; 37 | // Set a title for the page accordingly 38 | this.title = (this.authType === 'login') ? 'Sign in' : 'Sign up'; 39 | // add form control for username if this is the register page 40 | if (this.authType === 'register') { 41 | this.authForm.addControl('username', new FormControl()); 42 | } 43 | this.cd.markForCheck(); 44 | }); 45 | } 46 | 47 | submitForm() { 48 | this.isSubmitting = true; 49 | this.errors = {errors: {}}; 50 | 51 | const credentials = this.authForm.value; 52 | this.userService 53 | .attemptAuth(this.authType, credentials) 54 | .subscribe( 55 | data => this.router.navigateByUrl('/'), 56 | err => { 57 | this.errors = err; 58 | this.isSubmitting = false; 59 | this.cd.markForCheck(); 60 | } 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AuthComponent } from './auth.component'; 4 | import { NoAuthGuard } from './no-auth-guard.service'; 5 | import { SharedModule } from '../shared'; 6 | import { AuthRoutingModule } from './auth-routing.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | SharedModule, 11 | AuthRoutingModule 12 | ], 13 | declarations: [ 14 | AuthComponent 15 | ], 16 | providers: [ 17 | NoAuthGuard 18 | ] 19 | }) 20 | export class AuthModule {} 21 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/auth/no-auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { UserService } from '../core'; 6 | import { map , take } from 'rxjs/operators'; 7 | 8 | @Injectable() 9 | export class NoAuthGuard implements CanActivate { 10 | constructor( 11 | private router: Router, 12 | private userService: UserService 13 | ) {} 14 | 15 | canActivate( 16 | route: ActivatedRouteSnapshot, 17 | state: RouterStateSnapshot 18 | ): Observable { 19 | 20 | return this.userService.isAuthenticated.pipe(take(1), map(isAuth => !isAuth)); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import { HttpTokenInterceptor } from './interceptors/http.token.interceptor'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | CommonModule 9 | ], 10 | providers: [ 11 | { provide: HTTP_INTERCEPTORS, useClass: HttpTokenInterceptor, multi: true } 12 | ], 13 | declarations: [] 14 | }) 15 | export class CoreModule { } 16 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core.module'; 2 | export * from './services'; 3 | export * from './models'; 4 | export * from './interceptors'; 5 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/interceptors/http.token.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { JwtService } from '../services'; 6 | 7 | @Injectable() 8 | export class HttpTokenInterceptor implements HttpInterceptor { 9 | constructor(private jwtService: JwtService) {} 10 | 11 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 12 | const headersConfig = { 13 | 'Content-Type': 'application/json', 14 | 'Accept': 'application/json' 15 | }; 16 | 17 | const token = this.jwtService.getToken(); 18 | 19 | if (token) { 20 | headersConfig['Authorization'] = `Token ${token}`; 21 | } 22 | 23 | const request = req.clone({ setHeaders: headersConfig }); 24 | return next.handle(request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http.token.interceptor'; 2 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/article-list-config.model.ts: -------------------------------------------------------------------------------- 1 | export interface ArticleListConfig { 2 | type: string; 3 | 4 | filters: { 5 | tag?: string, 6 | author?: string, 7 | favorited?: string, 8 | limit?: number, 9 | offset?: number 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/article.model.ts: -------------------------------------------------------------------------------- 1 | import { Profile } from './profile.model'; 2 | 3 | export interface Article { 4 | slug: string; 5 | title: string; 6 | description: string; 7 | body: string; 8 | tagList: string[]; 9 | createdAt: string; 10 | updatedAt: string; 11 | favorited: boolean; 12 | favoritesCount: number; 13 | author: Profile; 14 | } 15 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/comment.model.ts: -------------------------------------------------------------------------------- 1 | import { Profile } from './profile.model'; 2 | 3 | export interface Comment { 4 | id: number; 5 | body: string; 6 | createdAt: string; 7 | author: Profile; 8 | } 9 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/errors.model.ts: -------------------------------------------------------------------------------- 1 | export interface Errors { 2 | errors: {[key: string]: string}; 3 | } 4 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './article.model'; 2 | export * from './article-list-config.model'; 3 | export * from './comment.model'; 4 | export * from './errors.model'; 5 | export * from './profile.model'; 6 | export * from './user.model'; 7 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/profile.model.ts: -------------------------------------------------------------------------------- 1 | export interface Profile { 2 | username: string; 3 | bio: string; 4 | image: string; 5 | following: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | email: string; 3 | token: string; 4 | username: string; 5 | bio: string; 6 | image: string; 7 | } 8 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { environment } from '../../../environments/environment'; 3 | import { HttpHeaders, HttpClient, HttpParams } from '@angular/common/http'; 4 | import { Observable , throwError } from 'rxjs'; 5 | 6 | import { catchError } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ApiService { 12 | constructor( 13 | private http: HttpClient 14 | ) {} 15 | 16 | private formatErrors(error: any) { 17 | return throwError(error.error); 18 | } 19 | 20 | get(path: string, params: HttpParams = new HttpParams()): Observable { 21 | return this.http.get(`${environment.api_url}${path}`, { params }) 22 | .pipe(catchError(this.formatErrors)); 23 | } 24 | 25 | put(path: string, body: Object = {}): Observable { 26 | return this.http.put( 27 | `${environment.api_url}${path}`, 28 | JSON.stringify(body) 29 | ).pipe(catchError(this.formatErrors)); 30 | } 31 | 32 | post(path: string, body: Object = {}): Observable { 33 | return this.http.post( 34 | `${environment.api_url}${path}`, 35 | JSON.stringify(body) 36 | ).pipe(catchError(this.formatErrors)); 37 | } 38 | 39 | delete(path): Observable { 40 | return this.http.delete( 41 | `${environment.api_url}${path}` 42 | ).pipe(catchError(this.formatErrors)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/articles.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { ApiService } from './api.service'; 6 | import { Article, ArticleListConfig } from '../models'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ArticlesService { 13 | constructor ( 14 | private apiService: ApiService 15 | ) {} 16 | 17 | query(config: ArticleListConfig): Observable<{articles: Article[], articlesCount: number}> { 18 | // Convert any filters over to Angular's URLSearchParams 19 | const params = {}; 20 | 21 | Object.keys(config.filters) 22 | .forEach((key) => { 23 | params[key] = config.filters[key]; 24 | }); 25 | 26 | return this.apiService 27 | .get( 28 | '/articles' + ((config.type === 'feed') ? '/feed' : ''), 29 | new HttpParams({ fromObject: params }) 30 | ); 31 | } 32 | 33 | get(slug): Observable
{ 34 | return this.apiService.get('/articles/' + slug) 35 | .pipe(map(data => data.article)); 36 | } 37 | 38 | destroy(slug) { 39 | return this.apiService.delete('/articles/' + slug); 40 | } 41 | 42 | save(article): Observable
{ 43 | // If we're updating an existing article 44 | if (article.slug) { 45 | return this.apiService.put('/articles/' + article.slug, {article: article}) 46 | .pipe(map(data => data.article)); 47 | 48 | // Otherwise, create a new article 49 | } else { 50 | return this.apiService.post('/articles/', {article: article}) 51 | .pipe(map(data => data.article)); 52 | } 53 | } 54 | 55 | favorite(slug): Observable
{ 56 | return this.apiService.post('/articles/' + slug + '/favorite'); 57 | } 58 | 59 | unfavorite(slug): Observable
{ 60 | return this.apiService.delete('/articles/' + slug + '/favorite'); 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { UserService } from './user.service'; 6 | import { take } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AuthGuard implements CanActivate { 12 | constructor( 13 | private router: Router, 14 | private userService: UserService 15 | ) {} 16 | 17 | canActivate( 18 | route: ActivatedRouteSnapshot, 19 | state: RouterStateSnapshot 20 | ): Observable { 21 | 22 | return this.userService.isAuthenticated.pipe(take(1)); 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/comments.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ApiService } from './api.service'; 5 | import { Comment } from '../models'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class CommentsService { 13 | constructor ( 14 | private apiService: ApiService 15 | ) {} 16 | 17 | add(slug, payload): Observable { 18 | return this.apiService 19 | .post( 20 | `/articles/${slug}/comments`, 21 | { comment: { body: payload } } 22 | ).pipe(map(data => data.comment)); 23 | } 24 | 25 | getAll(slug): Observable { 26 | return this.apiService.get(`/articles/${slug}/comments`) 27 | .pipe(map(data => data.comments)); 28 | } 29 | 30 | destroy(commentId, articleSlug) { 31 | return this.apiService 32 | .delete(`/articles/${articleSlug}/comments/${commentId}`); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api.service'; 2 | export * from './articles.service'; 3 | export * from './auth-guard.service'; 4 | export * from './comments.service'; 5 | export * from './jwt.service'; 6 | export * from './profiles.service'; 7 | export * from './tags.service'; 8 | export * from './user.service'; 9 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/jwt.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class JwtService { 8 | 9 | getToken(): String { 10 | return window.localStorage['jwtToken']; 11 | } 12 | 13 | saveToken(token: String) { 14 | window.localStorage['jwtToken'] = token; 15 | } 16 | 17 | destroyToken() { 18 | window.localStorage.removeItem('jwtToken'); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/profiles.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ApiService } from './api.service'; 5 | import { Profile } from '../models'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ProfilesService { 12 | constructor ( 13 | private apiService: ApiService 14 | ) {} 15 | 16 | get(username: string): Observable { 17 | return this.apiService.get('/profiles/' + username) 18 | .pipe(map((data: {profile: Profile}) => data.profile)); 19 | } 20 | 21 | follow(username: string): Observable { 22 | return this.apiService.post('/profiles/' + username + '/follow'); 23 | } 24 | 25 | unfollow(username: string): Observable { 26 | return this.apiService.delete('/profiles/' + username + '/follow'); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/core/services/tags.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ApiService } from './api.service'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class TagsService { 11 | constructor ( 12 | private apiService: ApiService 13 | ) {} 14 | 15 | getAll(): Observable<[string]> { 16 | return this.apiService.get('/tags') 17 | .pipe(map(data => data.tags)); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/editor/editable-article-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Article, ArticlesService, UserService } from '../core'; 6 | import { catchError , map } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class EditableArticleResolver implements Resolve
{ 12 | constructor( 13 | private articlesService: ArticlesService, 14 | private router: Router, 15 | private userService: UserService 16 | ) { } 17 | 18 | resolve( 19 | route: ActivatedRouteSnapshot, 20 | state: RouterStateSnapshot 21 | ): Observable { 22 | 23 | return this.articlesService.get(route.params['slug']) 24 | .pipe( 25 | map( 26 | article => { 27 | if (this.userService.getCurrentUser().username === article.author.username) { 28 | return article; 29 | } else { 30 | this.router.navigateByUrl('/'); 31 | } 32 | } 33 | ), 34 | catchError((err) => this.router.navigateByUrl('/')) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/editor/editor-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { EditorComponent } from './editor.component'; 4 | import { EditableArticleResolver } from './editable-article-resolver.service'; 5 | import { AuthGuard } from '../core'; 6 | import { SharedModule } from '../shared'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: EditorComponent, 12 | canActivate: [AuthGuard] 13 | }, 14 | { 15 | path: ':slug', 16 | component: EditorComponent, 17 | canActivate: [AuthGuard], 18 | resolve: { 19 | article: EditableArticleResolver 20 | } 21 | } 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forChild(routes)], 26 | exports: [RouterModule] 27 | }) 28 | export class EditorRoutingModule {} 29 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/editor/editor.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 | 16 |
17 | 18 |
19 | 23 |
24 | 25 |
26 | 31 |
32 | 33 |
34 | 39 | 40 |
41 | 43 | 44 | {{ tag }} 45 | 46 |
47 |
48 | 49 | 52 | 53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/editor/editor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | 4 | import { EditorComponent } from './editor.component'; 5 | 6 | import { SharedModule } from '../shared'; 7 | import { EditorRoutingModule } from './editor-routing.module'; 8 | 9 | @NgModule({ 10 | imports: [SharedModule, EditorRoutingModule], 11 | declarations: [EditorComponent], 12 | providers: [] 13 | }) 14 | export class EditorModule {} 15 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/home/home-auth-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { UserService } from '../core'; 6 | import { take } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class HomeAuthResolver implements Resolve { 12 | constructor( 13 | private router: Router, 14 | private userService: UserService 15 | ) {} 16 | 17 | resolve( 18 | route: ActivatedRouteSnapshot, 19 | state: RouterStateSnapshot 20 | ): Observable { 21 | 22 | return this.userService.isAuthenticated.pipe(take(1)); 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HomeComponent } from './home.component'; 4 | import { HomeAuthResolver } from './home-auth-resolver.service'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: HomeComponent, 10 | resolve: { 11 | isAuthenticated: HomeAuthResolver 12 | } 13 | } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class HomeRoutingModule {} 21 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/home/home.component.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | cursor:pointer; 3 | } 4 | 5 | .tag-pill{ 6 | cursor:pointer; 7 | } 8 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | 36 |
37 | 38 | 39 |
40 | 41 |
42 | 61 |
62 | 63 |
64 |
65 |
66 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { ArticleListConfig, TagsService, UserService } from '../core'; 5 | 6 | @Component({ 7 | selector: 'app-home-page', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.css'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class HomeComponent implements OnInit { 13 | constructor( 14 | private router: Router, 15 | private tagsService: TagsService, 16 | private userService: UserService, 17 | private cd: ChangeDetectorRef 18 | ) {} 19 | 20 | isAuthenticated: boolean; 21 | listConfig: ArticleListConfig = { 22 | type: 'all', 23 | filters: {} 24 | }; 25 | tags: Array = []; 26 | tagsLoaded = false; 27 | 28 | ngOnInit() { 29 | this.userService.isAuthenticated.subscribe( 30 | (authenticated) => { 31 | this.isAuthenticated = authenticated; 32 | 33 | // set the article list accordingly 34 | if (authenticated) { 35 | this.setListTo('feed'); 36 | } else { 37 | this.setListTo('all'); 38 | } 39 | this.cd.markForCheck(); 40 | } 41 | ); 42 | 43 | this.tagsService.getAll() 44 | .subscribe(tags => { 45 | this.tags = tags; 46 | this.tagsLoaded = true; 47 | this.cd.markForCheck(); 48 | }); 49 | } 50 | 51 | trackByFn(index, item) { 52 | return index; 53 | } 54 | 55 | setListTo(type: string = '', filters: Object = {}) { 56 | // If feed is requested but user is not authenticated, redirect to login 57 | if (type === 'feed' && !this.isAuthenticated) { 58 | this.router.navigateByUrl('/login'); 59 | return; 60 | } 61 | 62 | // Otherwise, set the list object 63 | this.listConfig = {type: type, filters: filters}; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | import { SharedModule } from '../shared'; 5 | import { HomeRoutingModule } from './home-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | HomeRoutingModule 11 | ], 12 | declarations: [ 13 | HomeComponent 14 | ], 15 | providers: [ 16 | ] 17 | }) 18 | export class HomeModule {} 19 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile-articles.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile-articles.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | 4 | import { ArticleListConfig, Profile } from '../core'; 5 | 6 | @Component({ 7 | selector: 'app-profile-articles', 8 | templateUrl: './profile-articles.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ProfileArticlesComponent implements OnInit { 12 | constructor( 13 | private route: ActivatedRoute, 14 | private router: Router, 15 | private cd: ChangeDetectorRef 16 | ) {} 17 | 18 | profile: Profile; 19 | articlesConfig: ArticleListConfig = { 20 | type: 'all', 21 | filters: {} 22 | }; 23 | 24 | ngOnInit() { 25 | this.route.parent.data.subscribe( 26 | (data: {profile: Profile}) => { 27 | this.profile = data.profile; 28 | this.articlesConfig = { 29 | type: 'all', 30 | filters: {} 31 | }; // Only method I found to refresh article load on swap 32 | this.articlesConfig.filters.author = this.profile.username; 33 | this.cd.markForCheck(); 34 | } 35 | ); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile-favorites.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile-favorites.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | 4 | import { ArticleListConfig, Profile } from '../core'; 5 | 6 | @Component({ 7 | selector: 'app-profile-favorites', 8 | templateUrl: './profile-favorites.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ProfileFavoritesComponent implements OnInit { 12 | constructor( 13 | private route: ActivatedRoute, 14 | private cd: ChangeDetectorRef 15 | ) {} 16 | 17 | profile: Profile; 18 | favoritesConfig: ArticleListConfig = { 19 | type: 'all', 20 | filters: {} 21 | }; 22 | 23 | ngOnInit() { 24 | this.route.parent.data.subscribe( 25 | (data: {profile: Profile}) => { 26 | this.profile = data.profile; 27 | this.favoritesConfig = {...this.favoritesConfig}; 28 | this.favoritesConfig.filters.favorited = this.profile.username; 29 | this.cd.markForCheck(); 30 | } 31 | ); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Profile, ProfilesService } from '../core'; 6 | import { catchError } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ProfileResolver implements Resolve { 12 | constructor( 13 | private profilesService: ProfilesService, 14 | private router: Router 15 | ) {} 16 | 17 | resolve( 18 | route: ActivatedRouteSnapshot, 19 | state: RouterStateSnapshot 20 | ): Observable { 21 | 22 | return this.profilesService.get(route.params['username']) 23 | .pipe(catchError((err) => this.router.navigateByUrl('/'))); 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { ProfileArticlesComponent } from './profile-articles.component'; 4 | import { ProfileFavoritesComponent } from './profile-favorites.component'; 5 | import { ProfileResolver } from './profile-resolver.service'; 6 | import { ProfileComponent } from './profile.component'; 7 | 8 | 9 | const routes: Routes = [ 10 | { 11 | path: ':username', 12 | component: ProfileComponent, 13 | resolve: { 14 | profile: ProfileResolver 15 | }, 16 | children: [ 17 | { 18 | path: '', 19 | component: ProfileArticlesComponent 20 | }, 21 | { 22 | path: 'favorites', 23 | component: ProfileFavoritesComponent 24 | } 25 | ] 26 | } 27 | ]; 28 | 29 | @NgModule({ 30 | imports: [RouterModule.forChild(routes)], 31 | exports: [RouterModule] 32 | }) 33 | export class ProfileRoutingModule {} 34 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 26 | 27 |
28 |
29 | 30 |
31 |
32 | 50 |
51 | 52 | 53 |
54 | 55 |
56 |
57 | 58 |
59 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import { User, UserService, Profile } from '../core'; 5 | import { concatMap , tap } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'app-profile-page', 9 | templateUrl: './profile.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class ProfileComponent implements OnInit { 13 | constructor( 14 | private route: ActivatedRoute, 15 | private userService: UserService, 16 | private cd: ChangeDetectorRef 17 | ) { } 18 | 19 | profile: Profile; 20 | currentUser: User; 21 | isUser: boolean; 22 | 23 | ngOnInit() { 24 | this.route.data.pipe( 25 | concatMap((data: { profile: Profile }) => { 26 | this.profile = data.profile; 27 | // Load the current user's data. 28 | return this.userService.currentUser.pipe(tap( 29 | (userData: User) => { 30 | this.currentUser = userData; 31 | this.isUser = (this.currentUser.username === this.profile.username); 32 | } 33 | )); 34 | }) 35 | ).subscribe((() => { 36 | this.cd.markForCheck(); 37 | })); 38 | } 39 | 40 | onToggleFollowing(following: boolean) { 41 | this.profile.following = following; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/profile/profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { ProfileArticlesComponent } from './profile-articles.component'; 4 | import { ProfileComponent } from './profile.component'; 5 | import { ProfileFavoritesComponent } from './profile-favorites.component'; 6 | import { SharedModule } from '../shared'; 7 | import { ProfileRoutingModule } from './profile-routing.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | SharedModule, 12 | ProfileRoutingModule 13 | ], 14 | declarations: [ 15 | ProfileArticlesComponent, 16 | ProfileComponent, 17 | ProfileFavoritesComponent 18 | ], 19 | providers: [ 20 | ] 21 | }) 22 | export class ProfileModule {} 23 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/settings/settings-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AuthGuard } from '../core'; 4 | import { SettingsComponent } from './settings.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: SettingsComponent, 10 | canActivate: [AuthGuard] 11 | } 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class SettingsRoutingModule {} 19 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | 5 | import { User, UserService } from '../core'; 6 | 7 | @Component({ 8 | selector: 'app-settings-page', 9 | templateUrl: './settings.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class SettingsComponent implements OnInit { 13 | user: User = {} as User; 14 | settingsForm: FormGroup; 15 | errors: Object = {}; 16 | isSubmitting = false; 17 | 18 | constructor( 19 | private router: Router, 20 | private userService: UserService, 21 | private fb: FormBuilder, 22 | private cd: ChangeDetectorRef 23 | ) { 24 | // create form group using the form builder 25 | this.settingsForm = this.fb.group({ 26 | image: '', 27 | username: '', 28 | bio: '', 29 | email: '', 30 | password: '' 31 | }); 32 | // Optional: subscribe to changes on the form 33 | // this.settingsForm.valueChanges.subscribe(values => this.updateUser(values)); 34 | } 35 | 36 | ngOnInit() { 37 | // Make a fresh copy of the current user's object to place in editable form fields 38 | Object.assign(this.user, this.userService.getCurrentUser()); 39 | // Fill the form 40 | this.settingsForm.patchValue(this.user); 41 | } 42 | 43 | logout() { 44 | this.userService.purgeAuth(); 45 | this.router.navigateByUrl('/'); 46 | } 47 | 48 | submitForm() { 49 | this.isSubmitting = true; 50 | 51 | // update the model 52 | this.updateUser(this.settingsForm.value); 53 | 54 | this.userService 55 | .update(this.user) 56 | .subscribe( 57 | updatedUser => this.router.navigateByUrl('/profile/' + updatedUser.username), 58 | err => { 59 | this.errors = err; 60 | this.isSubmitting = false; 61 | this.cd.markForCheck(); 62 | } 63 | ); 64 | } 65 | 66 | updateUser(values: Object) { 67 | Object.assign(this.user, values); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | import { SharedModule } from '../shared'; 5 | import { SettingsRoutingModule } from './settings-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | SettingsRoutingModule 11 | ], 12 | declarations: [ 13 | SettingsComponent 14 | ] 15 | }) 16 | export class SettingsModule {} 17 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-list.component.css: -------------------------------------------------------------------------------- 1 | .page-link { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-list.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
8 | Loading articles... 9 |
10 | 11 |
13 | No articles are here... yet. 14 |
15 | 16 | 30 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { Article, ArticleListConfig, ArticlesService } from '../../core'; 4 | @Component({ 5 | selector: 'app-article-list', 6 | styleUrls: ['article-list.component.css'], 7 | templateUrl: './article-list.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class ArticleListComponent { 11 | constructor ( 12 | private articlesService: ArticlesService, 13 | private cd: ChangeDetectorRef 14 | ) {} 15 | 16 | @Input() limit: number; 17 | @Input() 18 | set config(config: ArticleListConfig) { 19 | if (config) { 20 | this.query = config; 21 | this.currentPage = 1; 22 | this.runQuery(); 23 | } 24 | } 25 | 26 | query: ArticleListConfig; 27 | results: Article[]; 28 | loading = false; 29 | currentPage = 1; 30 | totalPages: Array = [1]; 31 | 32 | setPageTo(pageNumber) { 33 | this.currentPage = pageNumber; 34 | this.runQuery(); 35 | } 36 | 37 | trackByFn(index, item) { 38 | return index; 39 | } 40 | 41 | runQuery() { 42 | this.loading = true; 43 | this.results = []; 44 | 45 | // Create limit and offset filter (if necessary) 46 | if (this.limit) { 47 | this.query.filters.limit = this.limit; 48 | this.query.filters.offset = (this.limit * (this.currentPage - 1)); 49 | } 50 | 51 | this.articlesService.query(this.query) 52 | .subscribe(data => { 53 | this.loading = false; 54 | this.results = data.articles; 55 | 56 | // Used from http://www.jstips.co/en/create-range-0...n-easily-using-one-line/ 57 | this.totalPages = Array.from(new Array(Math.ceil(data.articlesCount / this.limit)), (val, index) => index + 1); 58 | this.cd.markForCheck(); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-meta.component.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-meta.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { Article } from '../../core'; 4 | 5 | @Component({ 6 | selector: 'app-article-meta', 7 | templateUrl: './article-meta.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class ArticleMetaComponent { 11 | @Input() article: Article; 12 | } 13 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-preview.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | {{article.favoritesCount}} 8 | 9 | 10 | 11 | 12 |

{{ article.title }}

13 |

{{ article.description }}

14 | Read more... 15 |
    16 |
  • 18 | {{ tag }} 19 |
  • 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/article-preview.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { Article } from '../../core'; 4 | 5 | @Component({ 6 | selector: 'app-article-preview', 7 | templateUrl: './article-preview.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class ArticlePreviewComponent { 11 | @Input() article: Article; 12 | 13 | trackByFn(index, item) { 14 | return index; 15 | } 16 | 17 | onToggleFavorite(favorited: boolean) { 18 | this.article['favorited'] = favorited; 19 | 20 | if (favorited) { 21 | this.article['favoritesCount']++; 22 | } else { 23 | this.article['favoritesCount']--; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/article-helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './article-list.component'; 2 | export * from './article-meta.component'; 3 | export * from './article-preview.component'; 4 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/buttons/favorite-button.component.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/buttons/favorite-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { Article, ArticlesService, UserService } from '../../core'; 5 | import { of } from 'rxjs'; 6 | import { concatMap , tap } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-favorite-button', 10 | templateUrl: './favorite-button.component.html', 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class FavoriteButtonComponent { 14 | constructor( 15 | private articlesService: ArticlesService, 16 | private router: Router, 17 | private userService: UserService, 18 | private cd: ChangeDetectorRef 19 | ) {} 20 | 21 | @Input() article: Article; 22 | @Output() toggle = new EventEmitter(); 23 | isSubmitting = false; 24 | 25 | toggleFavorite() { 26 | this.isSubmitting = true; 27 | 28 | this.userService.isAuthenticated.pipe(concatMap( 29 | (authenticated) => { 30 | // Not authenticated? Push to login screen 31 | if (!authenticated) { 32 | this.router.navigateByUrl('/login'); 33 | return of(null); 34 | } 35 | 36 | // Favorite the article if it isn't favorited yet 37 | if (!this.article.favorited) { 38 | return this.articlesService.favorite(this.article.slug) 39 | .pipe(tap( 40 | data => { 41 | this.isSubmitting = false; 42 | this.toggle.emit(true); 43 | }, 44 | err => this.isSubmitting = false 45 | )); 46 | 47 | // Otherwise, unfavorite the article 48 | } else { 49 | return this.articlesService.unfavorite(this.article.slug) 50 | .pipe(tap( 51 | data => { 52 | this.isSubmitting = false; 53 | this.toggle.emit(false); 54 | }, 55 | err => this.isSubmitting = false 56 | )); 57 | } 58 | 59 | } 60 | )).subscribe(() => { 61 | this.cd.markForCheck(); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/buttons/follow-button.component.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './favorite-button.component'; 2 | export * from './follow-button.component'; 3 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './article-helpers'; 2 | export * from './buttons'; 3 | export * from './layout'; 4 | export * from './list-errors.component'; 5 | export * from './shared.module'; 6 | export * from './show-authed.directive'; 7 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/layout/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | conduit 4 | 5 | © {{ today | date: 'yyyy' }}. 6 | An interactive learning project from Thinkster. 7 | Code licensed under MIT. 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/layout/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-layout-footer', 5 | templateUrl: './footer.component.html', 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | export class FooterComponent { 9 | today: number = Date.now(); 10 | } 11 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/layout/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | 3 | import { User, UserService } from '../../core'; 4 | 5 | @Component({ 6 | selector: 'app-layout-header', 7 | templateUrl: './header.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class HeaderComponent implements OnInit { 11 | constructor( 12 | private userService: UserService, 13 | private cd: ChangeDetectorRef 14 | ) {} 15 | 16 | currentUser: User; 17 | 18 | ngOnInit() { 19 | this.userService.currentUser.subscribe( 20 | (userData) => { 21 | this.currentUser = userData; 22 | this.cd.markForCheck(); 23 | } 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer.component'; 2 | export * from './header.component'; 3 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/list-errors.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | {{ error }} 4 |
  • 5 |
6 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/list-errors.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { Errors } from '../core'; 4 | 5 | @Component({ 6 | selector: 'app-list-errors', 7 | templateUrl: './list-errors.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class ListErrorsComponent { 11 | formattedErrors: Array = []; 12 | 13 | @Input() 14 | set errors(errorList: Errors) { 15 | this.formattedErrors = Object.keys(errorList.errors || {}) 16 | .map(key => `${key} ${errorList.errors[key]}`); 17 | } 18 | 19 | get errorList() { return this.formattedErrors; } 20 | 21 | trackByFn(index, item) { 22 | return index; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | import { ArticleListComponent, ArticleMetaComponent, ArticlePreviewComponent } from './article-helpers'; 8 | import { FavoriteButtonComponent, FollowButtonComponent } from './buttons'; 9 | import { ListErrorsComponent } from './list-errors.component'; 10 | import { ShowAuthedDirective } from './show-authed.directive'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | ReactiveFormsModule, 17 | HttpClientModule, 18 | RouterModule 19 | ], 20 | declarations: [ 21 | ArticleListComponent, 22 | ArticleMetaComponent, 23 | ArticlePreviewComponent, 24 | FavoriteButtonComponent, 25 | FollowButtonComponent, 26 | ListErrorsComponent, 27 | ShowAuthedDirective 28 | ], 29 | exports: [ 30 | ArticleListComponent, 31 | ArticleMetaComponent, 32 | ArticlePreviewComponent, 33 | CommonModule, 34 | FavoriteButtonComponent, 35 | FollowButtonComponent, 36 | FormsModule, 37 | ReactiveFormsModule, 38 | HttpClientModule, 39 | ListErrorsComponent, 40 | RouterModule, 41 | ShowAuthedDirective 42 | ] 43 | }) 44 | export class SharedModule {} 45 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/app/shared/show-authed.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Input, 4 | OnInit, 5 | TemplateRef, 6 | ViewContainerRef 7 | } from '@angular/core'; 8 | 9 | import { UserService } from '../core'; 10 | 11 | @Directive({ selector: '[appShowAuthed]' }) 12 | export class ShowAuthedDirective implements OnInit { 13 | constructor( 14 | private templateRef: TemplateRef, 15 | private userService: UserService, 16 | private viewContainer: ViewContainerRef 17 | ) {} 18 | 19 | condition: boolean; 20 | 21 | ngOnInit() { 22 | this.userService.isAuthenticated.subscribe( 23 | (isAuthenticated) => { 24 | if (isAuthenticated && this.condition || !isAuthenticated && !this.condition) { 25 | this.viewContainer.createEmbeddedView(this.templateRef); 26 | } else { 27 | this.viewContainer.clear(); 28 | } 29 | } 30 | ); 31 | } 32 | 33 | @Input() set appShowAuthed(condition: boolean) { 34 | this.condition = condition; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/.gitkeep -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/.npmignore -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | api_url: 'https://api.realworld.io/api' 4 | }; 5 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | api_url: 'https://api.realworld.io/api' 9 | }; 10 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/talk-dev-to-me-twitch/ed06f3b0695b4e2b9e17cb0dda17b6d88ecbcd46/spa-blue-green-deployments/app/src/favicon.ico -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Conduit 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Loading... 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | const bootstrapPromise = platformBrowserDynamic().bootstrapModule(AppModule); 12 | 13 | // Logging bootstrap information 14 | bootstrapPromise.then(success => console.log(`Bootstrap success`)) 15 | .catch(err => console.error(err)); 16 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ang2-conduit", 3 | "short_name": "ang2-conduit", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for the Reflect API. */ 22 | // import 'core-js/es6/reflect'; 23 | 24 | /** Evergreen browsers require these. **/ 25 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 26 | // import 'core-js/es7/reflect'; 27 | 28 | /*************************************************************************************************** 29 | * Zone JS is required by default for Angular itself. 30 | */ 31 | import 'zone.js'; // Included with Angular CLI. 32 | 33 | /*************************************************************************************************** 34 | * APPLICATION IMPORTS 35 | */ 36 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting(), { 26 | teardown: { destroyAfterEach: false } 27 | } 28 | ); 29 | // Then we find all the tests. 30 | const context = require.context('./', true, /\.spec\.ts$/); 31 | // And load the modules. 32 | context.keys().map(context); 33 | // Finally, start Karma to run the tests. 34 | __karma__.start(); 35 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "main.ts", 10 | "polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "downlevelIteration": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "ES2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # go.sum should be committed 15 | !go.sum 16 | 17 | # CDK asset staging directory 18 | .cdk.staging 19 | cdk.out 20 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ``` 4 | npm install -g aws-cdk 5 | cdk deploy 6 | ``` 7 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "go mod download && go run cdk.go", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "go.mod", 11 | "go.sum", 12 | "**/*test.go" 13 | ] 14 | }, 15 | "context": { 16 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 17 | "@aws-cdk/core:stackRelativeExports": true, 18 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 19 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 20 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-iam:minimizePolicies": true, 24 | "@aws-cdk/core:target-partitions": [ 25 | "aws", 26 | "aws-cn" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/cdk/go.mod: -------------------------------------------------------------------------------- 1 | module cdk 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-cdk-go/awscdk/v2 v2.18.0 7 | github.com/aws/constructs-go/constructs/v10 v10.0.9 8 | github.com/aws/jsii-runtime-go v1.55.1 9 | ) 10 | -------------------------------------------------------------------------------- /spa-blue-green-deployments/cdk/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 2 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 3 | github.com/aws/aws-cdk-go/awscdk/v2 v2.18.0 h1:GHb8GVCFCZTwf2daSv5TbGO9xP2GJlU1WXs7DoEt07c= 4 | github.com/aws/aws-cdk-go/awscdk/v2 v2.18.0/go.mod h1:GAUjiaBL3+MR0J8UyJGdVnWrHG9k4+io+K+kTLHydYg= 5 | github.com/aws/constructs-go/constructs/v10 v10.0.9 h1:YGk+deTAD3rgyANybjOtaoVrjC7HZZtALeC872avWFQ= 6 | github.com/aws/constructs-go/constructs/v10 v10.0.9/go.mod h1:RC6w8bOwxLmPX7Jfo9dkEZ9iVfgH4QnaVnfWvaNOHy0= 7 | github.com/aws/jsii-runtime-go v1.37.0/go.mod h1:6tZnlstx8bAB3vnLFF9n8bbkI//LDblAek9zFyMXV3E= 8 | github.com/aws/jsii-runtime-go v1.55.1 h1:naQzHp7NMVpQ6lElj5tn5HcnBvVjiP+g6XmNzs22Bvs= 9 | github.com/aws/jsii-runtime-go v1.55.1/go.mod h1:9htokR2a9XpRcbNf3fwqPaTs0CpC3KJCOUDMMJvEgZQ= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 17 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 21 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | --------------------------------------------------------------------------------