├── .npmrc ├── .prettierignore ├── unicorn_web ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── tests │ ├── alphabetical-sequencer.js │ ├── events │ │ └── eventbridge │ │ │ └── put_event_publication_evaluation_completed.json │ └── integration │ │ ├── transformations │ │ └── ddb_contract.jq │ │ ├── requestApproval.test.ts │ │ └── search.test.ts ├── jest.config.js ├── eslint.config.mjs ├── tsconfig.json ├── data │ ├── load_data.sh │ ├── approved_property.json │ └── property_data.json ├── src │ ├── schema │ │ └── unicorn_approvals │ │ │ └── publicationevaluationcompleted │ │ │ ├── PublicationEvaluationCompleted.ts │ │ │ ├── AWSEvent.ts │ │ │ └── marshaller │ │ │ └── Marshaller.ts │ ├── search_service │ │ └── powertools.ts │ └── publication_manager_service │ │ ├── powertools.ts │ │ └── publicationEvaluationEventHandler.ts ├── README.md ├── infrastructure │ ├── web-service │ │ └── samconfig.toml │ └── subscriptions │ │ └── unicorn-approvals-subscriptions.yaml ├── package.json ├── Makefile └── .gitignore ├── unicorn_approvals ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── tests │ ├── events │ │ ├── lambda │ │ │ ├── wait_for_contract_approval_function.json │ │ │ ├── content_integrity_validator_function_success.json │ │ │ ├── contract_status_changed.json │ │ │ └── contract_status_checker.json │ │ ├── eventbridge │ │ │ ├── contract_status_changed_event_contract_1_draft.json │ │ │ ├── contract_status_changed_event_contract_2_draft.json │ │ │ ├── contract_status_changed_event_contract_1_approved.json │ │ │ ├── contract_status_changed_event_contract_2_approved.json │ │ │ ├── publication_evaluation_completed_event.json │ │ │ ├── publication_approval_requested_event_inappropriate_description.json │ │ │ ├── publication_approval_requested_event_all_good.json │ │ │ ├── publication_approval_requested_event_pause_workflow.json │ │ │ ├── publication_approval_requested_event_non_existing_contract.json │ │ │ ├── put_event_property_approval_requested.json │ │ │ ├── publication_approval_requested_event_inappropriate_images.json │ │ │ └── publication_approval_requested_event.json │ │ └── dbb_stream_events │ │ │ ├── contract_status_changed_draft.json │ │ │ ├── status_approved_with_no_workflow.json │ │ │ ├── sfn_check_exists.json │ │ │ ├── status_approved_waiting_for_approval.json │ │ │ └── sfn_wait_approval.json │ ├── alphabetical-sequencer.js │ ├── unit │ │ └── contractStatusChangedEventHandler.test.ts │ └── integration │ │ └── contract_status_changed_event.test.ts ├── jest.config.js ├── eslint.config.mjs ├── tsconfig.json ├── src │ ├── schema │ │ └── unicorn_contracts │ │ │ └── contractstatuschanged │ │ │ ├── ContractStatusChanged.ts │ │ │ ├── AWSEvent.ts │ │ │ └── marshaller │ │ │ └── Marshaller.ts │ └── approvals_service │ │ ├── powertools.ts │ │ └── contractStatusChangedEventHandler.ts ├── README.md ├── package.json ├── infrastructure │ ├── approvals-service │ │ ├── samconfig.toml │ │ └── property_approval.asl.yaml │ ├── subscriptions │ │ ├── unicorn-contracts-subscriptions.yaml │ │ └── unicorn-web-subscriptions.yaml │ └── schema-registry │ │ └── PublicationEvaluationCompleted-schema.yaml ├── state_machine │ └── property_approval.asl.yaml └── .gitignore ├── unicorn_contracts ├── .npmignore ├── .prettierignore ├── infrastructure │ ├── subscriptions │ │ └── .gitkeep │ ├── contracts-service │ │ └── samconfig.toml │ └── schema-registry │ │ └── ContractStatusChanged-schema.yaml ├── .prettierrc.js ├── tests │ ├── alphabetical-sequencer.js │ ├── transformations │ │ └── ddb_contract.jq │ ├── integration │ │ ├── transformations │ │ │ └── ddb_contract.jq │ │ ├── create_contract.test.ts │ │ └── update_contract.test.ts │ ├── data │ │ └── contract_data.json │ └── unit │ │ └── events │ │ └── test_sqs_create_contract_valid_1.json ├── jest.config.js ├── eslint.config.mjs ├── tsconfig.json ├── src │ └── contracts_service │ │ ├── powertools.ts │ │ └── Contract.ts ├── package.json ├── README.md ├── Makefile └── .gitignore ├── pnpm-workspace.yaml ├── docs ├── architecture.png └── workshop_logo.png ├── .github ├── CODEOWNERS ├── workflows │ ├── auto_assign.yml │ ├── record_pr.yml │ ├── on_label_added.yml │ ├── on_merged_pr.yml │ ├── label_pr_on_title.yml │ ├── codeql-analysis.yml │ ├── on_opened_pr.yml │ ├── build_test.yml │ └── reusable_export_pr_details.yml ├── scripts │ ├── save_pr_details.js │ ├── download_pr_artifact.js │ ├── constants.js │ ├── enforce_acknowledgment.js │ ├── label_missing_related_issue.js │ ├── label_missing_acknowledgement_section.js │ ├── label_pr_based_on_title.js │ ├── label_related_issue.js │ └── comment_on_large_pr.js ├── semantic.yml ├── dependabot.yml ├── auto_assign.yml ├── PULL_REQUEST_TEMPLATE.md ├── boring-cyborg.yml └── ISSUE_TEMPLATE │ ├── maintenance.yml │ └── bug_report.yml ├── CODE_OF_CONDUCT.md ├── package.json ├── eslint.config.mjs ├── LICENSE ├── unicorn_shared ├── Makefile ├── uni-prop-namespaces.yaml └── uni-prop-images.yaml ├── .gitea └── actions │ └── configure │ └── action.yaml ├── CONTRIBUTING.md ├── README.md └── .gitignore /.npmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/template.yaml -------------------------------------------------------------------------------- /unicorn_web/.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /unicorn_approvals/.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /unicorn_contracts/.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /unicorn_web/.prettierignore: -------------------------------------------------------------------------------- 1 | **/template.yaml -------------------------------------------------------------------------------- /unicorn_approvals/.prettierignore: -------------------------------------------------------------------------------- 1 | **/template.yaml -------------------------------------------------------------------------------- /unicorn_contracts/.prettierignore: -------------------------------------------------------------------------------- 1 | **/template.yaml -------------------------------------------------------------------------------- /unicorn_contracts/infrastructure/subscriptions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - unicorn_shared 3 | - unicorn_contracts 4 | - unicorn_approvals 5 | - unicorn_web 6 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/HEAD/docs/architecture.png -------------------------------------------------------------------------------- /docs/workshop_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/HEAD/docs/workshop_logo.png -------------------------------------------------------------------------------- /unicorn_web/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /unicorn_approvals/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /unicorn_contracts/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/lambda/wait_for_contract_approval_function.json: -------------------------------------------------------------------------------- 1 | { 2 | "TaskToken": "xxx", 3 | "Input": { 4 | "property_id": "usa/anytown/main-street/123" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 2 | 3 | * @aws-samples/aws-serverless-developer-experience-workshop-ts -------------------------------------------------------------------------------- /.github/workflows/auto_assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | 6 | jobs: 7 | add-reviews: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v2.0.0 -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/lambda/content_integrity_validator_function_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "imageModerations": [ 3 | { 4 | "ModerationLabels": [] 5 | } 6 | ], 7 | "contentSentiment": { 8 | "Sentiment": "POSITIVE" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /unicorn_web/tests/alphabetical-sequencer.js: -------------------------------------------------------------------------------- 1 | const Sequencer = require('@jest/test-sequencer').default; 2 | 3 | class AlphabeticalSequencer extends Sequencer { 4 | sort(tests) { 5 | return tests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1)); 6 | } 7 | } 8 | 9 | module.exports = AlphabeticalSequencer; -------------------------------------------------------------------------------- /unicorn_approvals/tests/alphabetical-sequencer.js: -------------------------------------------------------------------------------- 1 | const Sequencer = require('@jest/test-sequencer').default; 2 | 3 | class AlphabeticalSequencer extends Sequencer { 4 | sort(tests) { 5 | return tests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1)); 6 | } 7 | } 8 | 9 | module.exports = AlphabeticalSequencer; -------------------------------------------------------------------------------- /unicorn_contracts/tests/alphabetical-sequencer.js: -------------------------------------------------------------------------------- 1 | const Sequencer = require('@jest/test-sequencer').default; 2 | 3 | class AlphabeticalSequencer extends Sequencer { 4 | sort(tests) { 5 | return tests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1)); 6 | } 7 | } 8 | 9 | module.exports = AlphabeticalSequencer; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "prepare": "husky" 4 | }, 5 | "devDependencies": { 6 | "@eslint/js": "^9.39.1", 7 | "eslint": "^9.39.1", 8 | "eslint-config-prettier": "^10.1.8", 9 | "globals": "^16.5.0", 10 | "husky": "^9.1.7", 11 | "prettier": "^3.6.2", 12 | "typescript-eslint": "^8.46.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "unicorn-approvals", 4 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}", 5 | "DetailType": "PublicationEvaluationCompleted", 6 | "EventBusName": "UnicornWebBus-local" 7 | } 8 | ] -------------------------------------------------------------------------------- /.github/scripts/save_pr_details.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({context, core}) => { 2 | const fs = require('fs'); 3 | const filename = "pr.txt"; 4 | 5 | try { 6 | fs.writeFileSync(`./${filename}`, JSON.stringify(context.payload)); 7 | 8 | return `PR successfully saved ${filename}` 9 | } catch (err) { 10 | core.setFailed("Failed to save PR details"); 11 | console.error(err); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/contract_status_changed_event_contract_1_draft.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn-contracts", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{ \"contract_last_modified_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"DRAFT\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/contract_status_changed_event_contract_2_draft.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn-contracts", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{ \"contract_last_modified_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"DRAFT\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/contract_status_changed_event_contract_1_approved.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn-contracts", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{ \"contract_last_modified_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"APPROVED\" }" 7 | } 8 | ] -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/contract_status_changed_event_contract_2_approved.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn-contracts", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{ \"contract_last_modified_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"APPROVED\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # conventional commit types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json 2 | types: 3 | - feat 4 | - fix 5 | - docs 6 | - style 7 | - refactor 8 | - perf 9 | - test 10 | - build 11 | - ci 12 | - chore 13 | - revert 14 | - improv 15 | 16 | # Always validate the PR title 17 | # and ignore the commits to lower the entry bar for contribution 18 | # while titles make up the Release notes to ease maintenance overhead 19 | titleOnly: true 20 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/transformations/ddb_contract.jq: -------------------------------------------------------------------------------- 1 | .Item | { 2 | property_id: .property_id.S, 3 | contract_id: .contract_id.S, 4 | seller_name: .seller_name.S, 5 | address: { 6 | country: .address.M.country.S, 7 | number: .address.M.number.N, 8 | city: .address.M.city.S, 9 | street: .address.M.street.S, 10 | }, 11 | contract_status: .contract_status.S, 12 | contract_created: .contract_created.S, 13 | contract_last_modified_on: .contract_last_modified_on.S 14 | } | del(..|nulls) 15 | -------------------------------------------------------------------------------- /unicorn_web/tests/integration/transformations/ddb_contract.jq: -------------------------------------------------------------------------------- 1 | .Item | { 2 | property_id: .property_id.S, 3 | contract_id: .contract_id.S, 4 | seller_name: .seller_name.S, 5 | address: { 6 | country: .address.M.country.S, 7 | number: .address.M.number.N, 8 | city: .address.M.city.S, 9 | street: .address.M.street.S, 10 | }, 11 | contract_status: .contract_status.S, 12 | contract_created: .contract_created.S, 13 | contract_last_modified_on: .contract_last_modified_on.S 14 | } | del(..|nulls) 15 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/lambda/contract_status_changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "account": "123456789012", 4 | "region": "us-east-1", 5 | "detail-type": "ContractStatusChanged", 6 | "source": "unicorn-contracts", 7 | "time": "2022-08-14T22:06:31Z", 8 | "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", 9 | "resources": [], 10 | "detail": { 11 | "contract_last_modified_on": "10/08/2022 19:56:30", 12 | "contract_id": "222", 13 | "property_id": "usa/anytown/main-street/123", 14 | "contract_status": "DRAFT" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/transformations/ddb_contract.jq: -------------------------------------------------------------------------------- 1 | .Item | { 2 | property_id: .property_id.S, 3 | contract_id: .contract_id.S, 4 | seller_name: .seller_name.S, 5 | address: { 6 | country: .address.M.country.S, 7 | number: .address.M.number.N, 8 | city: .address.M.city.S, 9 | street: .address.M.street.S, 10 | }, 11 | contract_status: .contract_status.S, 12 | contract_created: .contract_created.S, 13 | contract_last_modified_on: .contract_last_modified_on.S 14 | } | del(..|nulls) 15 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import prettierConfig from 'eslint-config-prettier' 4 | 5 | export default tseslint.config( 6 | { 7 | ignores: ["**/node_modules", "**/.aws-sam", "**/template.yaml", "**/cdk.out", "**/.github", "**/jest.config.js", "**/.prettierrc.js"], 8 | }, 9 | eslint.configs.recommended, 10 | tseslint.configs.stylistic, 11 | prettierConfig, 12 | { 13 | rules: { 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": ["error"] 16 | } 17 | } 18 | ); -------------------------------------------------------------------------------- /unicorn_contracts/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | transform: { 4 | "^.+\\.ts?$": ["ts-jest", { 5 | tsconfig: { 6 | skipLibCheck: true 7 | } 8 | }], 9 | }, 10 | moduleFileExtensions: ["js", "ts"], 11 | collectCoverageFrom: ["**/src/**/*.ts", "!**/node_modules/**"], 12 | testMatch: ["**/tests/unit/*.test.ts", "**/tests/integration/*.test.ts"], 13 | testPathIgnorePatterns: ["/node_modules/"], 14 | testEnvironment: "node", 15 | testSequencer: "./tests/alphabetical-sequencer.js", 16 | coverageProvider: "v8" 17 | }; -------------------------------------------------------------------------------- /unicorn_contracts/tests/data/contract_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "property_id": "usa/anytown/main-street/111", 4 | "address": { 5 | "city": "Anytown", 6 | "country": "USA", 7 | "number": 111, 8 | "street": "Main Street" 9 | }, 10 | "contract_created": "2023-11-19T02:24:11.480Z", 11 | "contract_id": "21265809-bc6e-4d01-8ab8-2c22939f16d7", 12 | "contract_last_modified_on": "2023-11-19T02:24:11.480Z", 13 | "contract_status": "DRAFT", 14 | "seller_name": "John Doe" 15 | } 16 | ] -------------------------------------------------------------------------------- /unicorn_web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | transform: { 4 | "^.+\\.ts?$": ["ts-jest", { 5 | tsconfig: { 6 | skipLibCheck: true 7 | } 8 | }], 9 | }, 10 | moduleFileExtensions: ["js", "ts"], 11 | collectCoverageFrom: ["**/src/**/*.ts", "!**/node_modules/**"], 12 | testMatch: ["**/tests/unit/*.test.ts", "**/tests/integration/*.test.ts"], 13 | testPathIgnorePatterns: ["/node_modules/"], 14 | testEnvironment: "node", 15 | testSequencer: "./tests/alphabetical-sequencer.js", 16 | coverageProvider: "v8", 17 | }; 18 | -------------------------------------------------------------------------------- /unicorn_approvals/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | transform: { 4 | "^.+\\.ts?$": ["ts-jest", { 5 | tsconfig: { 6 | skipLibCheck: true 7 | } 8 | }], 9 | }, 10 | moduleFileExtensions: ["js", "ts"], 11 | collectCoverageFrom: ["**/src/**/*.ts", "!**/node_modules/**"], 12 | testMatch: ["**/tests/unit/*.test.ts", "**/tests/integration/*.test.ts"], 13 | testPathIgnorePatterns: ["/node_modules/"], 14 | testEnvironment: "node", 15 | testSequencer: "./tests/alphabetical-sequencer.js", 16 | coverageProvider: "v8", 17 | }; 18 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_evaluation_completed_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "f849f683-76e1-1c84-669d-544a9828dfef", 4 | "detail-type": "PublicationEvaluationCompleted", 5 | "source": "unicorn-approvals", 6 | "account": "123456789012", 7 | "time": "2022-08-16T06:33:05Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "property_id": "usa/anytown/main-street/111", 12 | "evaluation_result": "APPROVED|DECLINED", 13 | "result_reason": "UNSAFE_IMAGE_DETECTED|BAD_SENTIMENT_DETECTED|..." 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/record_pr.yml: -------------------------------------------------------------------------------- 1 | name: Record PR details 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, closed] 6 | 7 | jobs: 8 | record_pr: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: "Extract PR details" 14 | uses: actions/github-script@v7 15 | with: 16 | script: | 17 | const script = require('.github/scripts/save_pr_details.js') 18 | await script({github, context, core}) 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: pr 22 | path: pr.txt 23 | -------------------------------------------------------------------------------- /unicorn_web/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import prettierConfig from 'eslint-config-prettier' 4 | 5 | export default tseslint.config( 6 | { 7 | ignores: ["**/node_modules", "**/.aws-sam", "**/template.yaml", "**/cdk.out", "**/.github", "**/jest.config.js", "**/.prettierrc.js", "**/alphabetical-sequencer.js"], 8 | }, 9 | eslint.configs.recommended, 10 | tseslint.configs.stylistic, 11 | prettierConfig, 12 | { 13 | rules: { 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": ["error"] 16 | } 17 | } 18 | ); -------------------------------------------------------------------------------- /unicorn_approvals/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import prettierConfig from 'eslint-config-prettier' 4 | 5 | export default tseslint.config( 6 | { 7 | ignores: ["**/node_modules", "**/.aws-sam", "**/template.yaml", "**/cdk.out", "**/.github", "**/jest.config.js", "**/.prettierrc.js", "**/alphabetical-sequencer.js"], 8 | }, 9 | eslint.configs.recommended, 10 | tseslint.configs.stylistic, 11 | prettierConfig, 12 | { 13 | rules: { 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": ["error"] 16 | } 17 | } 18 | ); -------------------------------------------------------------------------------- /unicorn_contracts/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import prettierConfig from 'eslint-config-prettier' 4 | 5 | export default tseslint.config( 6 | { 7 | ignores: ["**/node_modules", "**/.aws-sam", "**/template.yaml", "**/cdk.out", "**/.github", "**/jest.config.js", "**/.prettierrc.js", "**/alphabetical-sequencer.js"], 8 | }, 9 | eslint.configs.recommended, 10 | tseslint.configs.stylistic, 11 | prettierConfig, 12 | { 13 | rules: { 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": ["error"] 16 | } 17 | } 18 | ); -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directories: 10 | - "unicorn_contracts" # Location of package manifests 11 | - "unicorn_approvals" 12 | - "unicorn_web" 13 | schedule: 14 | interval: "monthly" 15 | -------------------------------------------------------------------------------- /unicorn_web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "strict": true, 5 | "types": ["node", "jest"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "src/*": ["src/*"], 9 | "tests/*": ["tests/*"] 10 | }, 11 | "resolveJsonModule": true, 12 | "preserveConstEnums": true, 13 | "noEmit": true, 14 | "sourceMap": false, 15 | "module":"commonjs", 16 | "moduleResolution":"node", 17 | "esModuleInterop": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "experimentalDecorators": true, 21 | "isolatedModules": true 22 | }, 23 | "exclude": ["node_modules"] 24 | } -------------------------------------------------------------------------------- /unicorn_approvals/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "strict": true, 5 | "types": ["node", "jest"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "src/*": ["src/*"], 9 | "tests/*": ["tests/*"] 10 | }, 11 | "resolveJsonModule": true, 12 | "preserveConstEnums": true, 13 | "noEmit": true, 14 | "sourceMap": false, 15 | "module":"commonjs", 16 | "moduleResolution":"node", 17 | "esModuleInterop": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "experimentalDecorators": true, 21 | "isolatedModules": true 22 | }, 23 | "exclude": ["node_modules"] 24 | } -------------------------------------------------------------------------------- /unicorn_contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "strict": true, 5 | "types": ["node", "jest"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "src/*": ["src/*"], 9 | "tests/*": ["tests/*"] 10 | }, 11 | "resolveJsonModule": true, 12 | "preserveConstEnums": true, 13 | "noEmit": true, 14 | "sourceMap": false, 15 | "module":"commonjs", 16 | "moduleResolution":"node", 17 | "esModuleInterop": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "experimentalDecorators": true, 21 | "isolatedModules": true 22 | }, 23 | "exclude": ["node_modules"] 24 | } -------------------------------------------------------------------------------- /unicorn_web/data/load_data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | ROOT_DIR="$(cd -- "$(dirname "$0")/../" >/dev/null 2>&1 ; pwd -P )" 5 | STACK_NAME="$(yq -ot '.default.global.parameters.stack_name' $ROOT_DIR/samconfig.toml)" 6 | 7 | JSON_FILE="$(cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/property_data.json" 8 | echo "JSON_FILE: '${JSON_FILE}'" 9 | 10 | DDB_TBL_NAME="$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query 'Stacks[0].Outputs[?ends_with(OutputKey, `PropertiesTableName`)].OutputValue' --output text)" 11 | echo "DDB_TABLE_NAME: '${DDB_TBL_NAME}'" 12 | 13 | echo "LOADING ITEMS TO DYNAMODB:" 14 | aws ddb put ${DDB_TBL_NAME} file://${JSON_FILE} 15 | echo "DONE!" 16 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: true 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - sliedig 10 | 11 | # A number of reviewers added to the pull request 12 | # Set 0 to add all the reviewers (default: 0) 13 | numberOfReviewers: 1 14 | 15 | # A list of assignees, overrides reviewers if set 16 | # assignees: 17 | # - sliedig 18 | 19 | # A number of assignees to add to the pull request 20 | # Set to 0 to add all of the assignees. 21 | # Uses numberOfReviewers if unset. 22 | numberOfAssignees: 0 23 | 24 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 25 | # skipKeywords: 26 | # - wip 27 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/lambda/contract_status_checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Input": { 3 | "property_id": "usa/anytown/main-street/123", 4 | "country": "USA", 5 | "city": "Anytown", 6 | "street": "Main Street", 7 | "number": 123, 8 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 9 | "contract": "sale", 10 | "listprice": 200.0, 11 | "currency": "SPL", 12 | "images": [ 13 | "usa/anytown/main-street-123-0d61b4e3" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /unicorn_web/src/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | export class PublicationEvaluationCompleted { 4 | 'evaluationResult': string; 5 | 'propertyId': string; 6 | 7 | private static discriminator: string | undefined = undefined; 8 | 9 | private static attributeTypeMap: { 10 | name: string; 11 | baseName: string; 12 | type: string; 13 | }[] = [ 14 | { 15 | name: 'evaluationResult', 16 | baseName: 'evaluation_result', 17 | type: 'string', 18 | }, 19 | { 20 | name: 'propertyId', 21 | baseName: 'property_id', 22 | type: 'string', 23 | }, 24 | ]; 25 | 26 | public static getAttributeTypeMap() { 27 | return PublicationEvaluationCompleted.attributeTypeMap; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /unicorn_web/data/approved_property.json: -------------------------------------------------------------------------------- 1 | { 2 | "PK": "PROPERTY#au#anytown", 3 | "SK": "main-street#1337", 4 | "country": "AU", 5 | "city": "Anytown", 6 | "street": "Main Street", 7 | "number": 1337, 8 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 9 | "contract": "sale", 10 | "listprice": 200, 11 | "currency": "SPL", 12 | "images": [ 13 | "property_images/prop1_exterior1.jpg", 14 | "property_images/prop1_interior1.jpg", 15 | "property_images/prop1_interior2.jpg", 16 | "property_images/prop1_interior3.jpg" 17 | ], 18 | "status": "APPROVED" 19 | } -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_approval_requested_event_inappropriate_description.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn-web", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This is a property for goblins. The property has the worst quality and is atrocious when it comes to design. The property is not clean whatsoever, and will make any property owner have buyers' remorse as soon the property is bought. Keep away from this property as much as possible!\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue number:** 2 | 3 | ## Summary 4 | 5 | ### Changes 6 | 7 | > Please provide a summary of what's being changed 8 | 9 | ### User experience 10 | 11 | > Please share what the user experience looks like before and after this change 12 | 13 | ## Checklist 14 | 15 | Please leave checklist items unchecked if they do not apply to your change. 16 | 17 | * [ ] I have performed a self-review of this change 18 | * [ ] Changes have been tested 19 | * [ ] Changes are documented 20 | * [ ] PR title follows [conventional commit semantics](https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/blob/develop/.github/semantic.yml) 21 | 22 | ## Acknowledgment 23 | 24 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 25 | 26 | **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. 27 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_approval_requested_event_all_good.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn-web", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /.github/scripts/download_pr_artifact.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({github, context, core}) => { 2 | const fs = require('fs'); 3 | 4 | const workflowRunId = process.env.WORKFLOW_ID; 5 | core.info(`Listing artifacts for workflow run ${workflowRunId}`); 6 | 7 | const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 8 | owner: context.repo.owner, 9 | repo: context.repo.repo, 10 | run_id: workflowRunId, 11 | }); 12 | 13 | const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0]; 14 | 15 | core.info(`Downloading artifacts for workflow run ${workflowRunId}`); 16 | const artifact = await github.rest.actions.downloadArtifact({ 17 | owner: context.repo.owner, 18 | repo: context.repo.repo, 19 | artifact_id: matchArtifact.id, 20 | archive_format: 'zip', 21 | }); 22 | 23 | core.info("Saving artifact found", artifact); 24 | 25 | fs.writeFileSync('pr.zip', Buffer.from(artifact.data)); 26 | } 27 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_approval_requested_event_pause_workflow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn-web", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_approval_requested_event_non_existing_contract.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn-web", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/333\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":333},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/put_event_property_approval_requested.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "unicorn-web", 4 | "Detail": "{ \"property_id\": \"usa/anytown/main-street/111\", \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 111, \"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\", \"contract\": \"sale\", \"listprice\": 200, \"currency\": \"SPL\", \"images\": [ \"property_images/prop1_exterior1.jpg\", \"property_images/prop1_interior1.jpg\", \"property_images/prop1_interior2.jpg\", \"property_images/prop1_interior3.jpg\", \"property_images/prop1_interior4-bad.jpg\" ] }", 5 | "DetailType": "PublicationApprovalRequested", 6 | "EventBusName": "unicorn-approvals-eventbus-local" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_approval_requested_event_inappropriate_images.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn-web", 5 | "EventBusName": "unicorn-approvals-eventbus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\",\"property_images/prop1_interior4-bad.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_approvals/src/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.ts: -------------------------------------------------------------------------------- 1 | export class ContractStatusChanged { 2 | 'contractId': string; 3 | 'contractLastModifiedOn': string; 4 | 'contractStatus': string; 5 | 'propertyId': string; 6 | 7 | private static discriminator: string | undefined = undefined; 8 | 9 | private static attributeTypeMap: { 10 | name: string; 11 | baseName: string; 12 | type: string; 13 | }[] = [ 14 | { 15 | name: 'contractId', 16 | baseName: 'contract_id', 17 | type: 'string', 18 | }, 19 | { 20 | name: 'contractLastModifiedOn', 21 | baseName: 'contract_last_modified_on', 22 | type: 'string', 23 | }, 24 | { 25 | name: 'contractStatus', 26 | baseName: 'contract_status', 27 | type: 'string', 28 | }, 29 | { 30 | name: 'propertyId', 31 | baseName: 'property_id', 32 | type: 'string', 33 | }, 34 | ]; 35 | 36 | public static getAttributeTypeMap() { 37 | return ContractStatusChanged.attributeTypeMap; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /unicorn_web/src/search_service/powertools.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { Metrics } from '@aws-lambda-powertools/metrics'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | import type { LogLevel } from '@aws-lambda-powertools/logger/types'; 6 | import { Tracer } from '@aws-lambda-powertools/tracer'; 7 | 8 | const SERVICE_NAMESPACE = process.env.SERVICE_NAMESPACE ?? 'test_namespace'; 9 | 10 | const defaultValues = { 11 | region: process.env.AWS_REGION || 'N/A', 12 | executionEnv: process.env.AWS_EXECUTION_ENV || 'N/A', 13 | }; 14 | 15 | const logger = new Logger({ 16 | logLevel: (process.env.LOG_LEVEL || 'INFO') as LogLevel, 17 | persistentLogAttributes: { 18 | ...defaultValues, 19 | logger: { 20 | name: '@aws-lambda-powertools/logger', 21 | }, 22 | }, 23 | }); 24 | 25 | const metrics = new Metrics({ 26 | defaultDimensions: defaultValues, 27 | namespace: SERVICE_NAMESPACE, 28 | }); 29 | 30 | const tracer = new Tracer(); 31 | 32 | export { metrics, logger, tracer }; 33 | -------------------------------------------------------------------------------- /unicorn_approvals/src/approvals_service/powertools.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { Metrics } from '@aws-lambda-powertools/metrics'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | import type { LogLevel } from '@aws-lambda-powertools/logger/types'; 6 | import { Tracer } from '@aws-lambda-powertools/tracer'; 7 | 8 | const SERVICE_NAMESPACE = process.env.SERVICE_NAMESPACE ?? 'test_namespace'; 9 | 10 | const defaultValues = { 11 | region: process.env.AWS_REGION || 'N/A', 12 | executionEnv: process.env.AWS_EXECUTION_ENV || 'N/A', 13 | }; 14 | 15 | const logger = new Logger({ 16 | logLevel: (process.env.LOG_LEVEL || 'INFO') as LogLevel, 17 | persistentLogAttributes: { 18 | ...defaultValues, 19 | logger: { 20 | name: '@aws-lambda-powertools/logger', 21 | }, 22 | }, 23 | }); 24 | 25 | const metrics = new Metrics({ 26 | defaultDimensions: defaultValues, 27 | namespace: SERVICE_NAMESPACE, 28 | }); 29 | 30 | const tracer = new Tracer(); 31 | 32 | export { metrics, logger, tracer }; 33 | -------------------------------------------------------------------------------- /unicorn_contracts/src/contracts_service/powertools.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { Metrics } from '@aws-lambda-powertools/metrics'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | import type { LogLevel } from '@aws-lambda-powertools/logger/types'; 6 | import { Tracer } from '@aws-lambda-powertools/tracer'; 7 | 8 | const SERVICE_NAMESPACE = process.env.SERVICE_NAMESPACE ?? 'test_namespace'; 9 | 10 | const defaultValues = { 11 | region: process.env.AWS_REGION || 'N/A', 12 | executionEnv: process.env.AWS_EXECUTION_ENV || 'N/A', 13 | }; 14 | 15 | const logger = new Logger({ 16 | logLevel: (process.env.LOG_LEVEL || 'INFO') as LogLevel, 17 | persistentLogAttributes: { 18 | ...defaultValues, 19 | logger: { 20 | name: '@aws-lambda-powertools/logger', 21 | }, 22 | }, 23 | }); 24 | 25 | const metrics = new Metrics({ 26 | defaultDimensions: defaultValues, 27 | namespace: SERVICE_NAMESPACE, 28 | }); 29 | 30 | const tracer = new Tracer(); 31 | 32 | export { metrics, logger, tracer }; 33 | -------------------------------------------------------------------------------- /unicorn_web/src/publication_manager_service/powertools.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { Metrics } from '@aws-lambda-powertools/metrics'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | import type { LogLevel } from '@aws-lambda-powertools/logger/types'; 6 | import { Tracer } from '@aws-lambda-powertools/tracer'; 7 | 8 | const SERVICE_NAMESPACE = process.env.SERVICE_NAMESPACE ?? 'test_namespace'; 9 | 10 | const defaultValues = { 11 | region: process.env.AWS_REGION || 'N/A', 12 | executionEnv: process.env.AWS_EXECUTION_ENV || 'N/A', 13 | }; 14 | 15 | const logger = new Logger({ 16 | logLevel: (process.env.LOG_LEVEL || 'INFO') as LogLevel, 17 | persistentLogAttributes: { 18 | ...defaultValues, 19 | logger: { 20 | name: '@aws-lambda-powertools/logger', 21 | }, 22 | }, 23 | }); 24 | 25 | const metrics = new Metrics({ 26 | defaultDimensions: defaultValues, 27 | namespace: SERVICE_NAMESPACE, 28 | }); 29 | 30 | const tracer = new Tracer(); 31 | 32 | export { metrics, logger, tracer }; 33 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/dbb_stream_events/contract_status_changed_draft.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "70283d3d4dcfa99d38276979fd1e3f1f", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661269906.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "23/08/2022 15:51:44" }, 14 | "contract_id": { "S": "b9c03b51-5b8c-47c8-9d76-223679cde774" }, 15 | "contract_status": { "S": "DRAFT" }, 16 | "property_id": { "S": "usa/anytown/main-street/999" } 17 | }, 18 | "SequenceNumber": "100000000005391461882", 19 | "SizeBytes": 187, 20 | "StreamViewType": "NEW_AND_OLD_IMAGES" 21 | }, 22 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/scripts/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.freeze({ 2 | /** @type {string} */ 3 | // Values: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 4 | "PR_ACTION": process.env.PR_ACTION?.replace(/"/g, '') || "", 5 | 6 | /** @type {string} */ 7 | "PR_AUTHOR": process.env.PR_AUTHOR?.replace(/"/g, '') || "", 8 | 9 | /** @type {string} */ 10 | "PR_BODY": process.env.PR_BODY || "", 11 | 12 | /** @type {string} */ 13 | "PR_TITLE": process.env.PR_TITLE || "", 14 | 15 | /** @type {number} */ 16 | "PR_NUMBER": process.env.PR_NUMBER || 0, 17 | 18 | /** @type {string} */ 19 | "PR_IS_MERGED": process.env.PR_IS_MERGED || "false", 20 | 21 | /** @type {string} */ 22 | "LABEL_BLOCK": "do-not-merge", 23 | 24 | /** @type {string} */ 25 | "LABEL_BLOCK_REASON": "need-issue", 26 | 27 | /** @type {string} */ 28 | "LABEL_BLOCK_MISSING_LICENSE_AGREEMENT": "need-license-agreement-acknowledge", 29 | 30 | /** @type {string} */ 31 | "LABEL_PENDING_RELEASE": "pending-release", 32 | 33 | /** @type {string[]} */ 34 | "IGNORE_AUTHORS": ["dependabot[bot]", "markdownify[bot]"], 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/events/test_sqs_create_contract_valid_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "b28d6431-f347-4044-bc07-c22b18bf91e4", 5 | "receiptHandle": "AQEBB+zJwS8zlIoHEkx2CGpli6qUttEYoqoWkwfo6Ke6N3Xky7xPHMBJsTzonsr/OEfZIqax4eZeokh+ySkxTq7xdT4ZRxSP9QLfCR3ceWNO8IS4YYQpclPfhTj9NzcrH5U6caTvB63+BLNLwrTo/0y2xBQYnNKBgdJ5Jot4/2iWcLtIgtIeJ9WnnTNf7/8ITaE9OEHws+svh06OxaC6NO4o8orLhrdX2Bh4hSsrtlVxP1lypN2Uw0Cz/PONVTUK6XmRWbQTj/G/nkDaDNsnT7FRfqyy0YPGoE2NdQiFWp0sB4nVhH0/LSK/nzD1fYTBf6LjxdbDLakO2GUVfkjI8ZOlluAqg0crBCa6z6I6TcPA4VOE5aE0ImhP/DiagaJNDD+nquzgqfT0fh8vBDz/h6z5wTBhLShJ0sm/iFyN6ey8Iuo=", 6 | "md5OfBody": "b886edd2bff032c4d10fed7606d17e8f", 7 | "body": "{\n\"address\": {\n\"country\": \"USA\",\n\"city\": \"Anytown\",\n\"street\": \"Main Street\",\n\"number\": 444\n},\n\"seller_name\": \"John Doe\",\n\"property_id\": \"usa/anytown/main-street/444\"\n}", 8 | "md5OfMessageAttributes": "b51fb21666798a04bb45833ff6dc08ad", 9 | "messageAttributes": { 10 | "HttpMethod": { 11 | "stringValue": "POST", 12 | "dataType": "String" 13 | } 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.github/boring-cyborg.yml: -------------------------------------------------------------------------------- 1 | ##### Labeler ########################################################################################################## 2 | labelPRBasedOnFilePath: 3 | service/contracts: 4 | - Unicorn.Contracts 5 | service/properties: 6 | - Unicorn.Approvals 7 | service/web: 8 | - Unicorn.Web 9 | 10 | internal: 11 | - .github/* 12 | - .github/**/* 13 | - .chglog/* 14 | - .flake8 15 | - .gitignore 16 | - .pre-commit-config.yaml 17 | - Makefile 18 | - CONTRIBUTING.md 19 | - CODE_OF_CONDUCT.md 20 | - LICENSE 21 | 22 | ##### Greetings ######################################################################################################## 23 | firstPRWelcomeComment: > 24 | Thanks a lot for your first contribution! Please check out our contributing guidelines and don't hesitate to ask whatever you need. 25 | 26 | # Comment to be posted to congratulate user on their first merged PR 27 | firstPRMergeComment: > 28 | Awesome work, congrats on your first merged pull request and thank you for helping improve everyone's experience! 29 | 30 | # Comment to be posted to on first time issues 31 | firstIssueWelcomeComment: > 32 | Thanks for opening your first issue here! We'll come back to you as soon as we can. -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/eventbridge/publication_approval_requested_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "f849f683-76e1-1c84-669d-544a9828dfef", 4 | "detail-type": "PublicationApprovalRequested", 5 | "source": "unicorn-web", 6 | "account": "123456789012", 7 | "time": "2022-08-16T06:33:05Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "property_id": "usa/anytown/main-street/111", 12 | "address": { 13 | "country": "USA", 14 | "city": "Anytown", 15 | "street": "Main Street", 16 | "number": 111 17 | }, 18 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 19 | "contract": "sale", 20 | "listprice": 200, 21 | "currency": "SPL", 22 | "images": [ 23 | "property_images/prop1_exterior1.jpg", 24 | "property_images/prop1_interior1.jpg", 25 | "property_images/prop1_interior2.jpg", 26 | "property_images/prop1_interior3.jpg", 27 | "property_images/prop1_interior4-bad.jpg" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Maintenance 2 | description: Suggest an activity to help address tech debt, governance, and anything internal 3 | title: "Maintenance: TITLE" 4 | labels: ["internal", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to help us improve operational excellence. 10 | 11 | *Future readers*: Please react with 👍 and your use case to help us understand customer demand. 12 | - type: textarea 13 | id: activity 14 | attributes: 15 | label: Summary 16 | description: Please provide an overview in one or two paragraphs 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: importance 21 | attributes: 22 | label: Why is this needed? 23 | description: Please help us understand the value so we can prioritize it accordingly 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: suggestion 28 | attributes: 29 | label: Solution 30 | description: If available, please share what a good solution would look like 31 | validations: 32 | required: false 33 | - type: markdown 34 | attributes: 35 | value: | 36 | --- 37 | **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. 38 | -------------------------------------------------------------------------------- /.github/workflows/on_label_added.yml: -------------------------------------------------------------------------------- 1 | name: On Label added 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | uses: ./.github/workflows/reusable_export_pr_details.yml 13 | with: 14 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 15 | workflow_origin: ${{ github.event.repository.full_name }} 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | split-large-pr: 20 | needs: get_pr_details 21 | runs-on: ubuntu-latest 22 | permissions: 23 | issues: write 24 | pull-requests: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | # Maintenance: Persist state per PR as an artifact to avoid spam on label add 28 | - name: "Suggest split large Pull Request" 29 | uses: actions/github-script@v7 30 | env: 31 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 32 | PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} 33 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | script: | 37 | const script = require('.github/scripts/comment_on_large_pr.js'); 38 | await script({github, context, core}); 39 | -------------------------------------------------------------------------------- /.github/workflows/on_merged_pr.yml: -------------------------------------------------------------------------------- 1 | name: On PR merge 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' 12 | uses: ./.github/workflows/reusable_export_pr_details.yml 13 | with: 14 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 15 | workflow_origin: ${{ github.event.repository.full_name }} 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | release_label_on_merge: 19 | needs: get_pr_details 20 | runs-on: ubuntu-latest 21 | if: needs.get_pr_details.outputs.prIsMerged == 'true' 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: "Label PR related issue for release" 25 | uses: actions/github-script@v7 26 | env: 27 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 28 | PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} 29 | PR_IS_MERGED: ${{ needs.get_pr_details.outputs.prIsMerged }} 30 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | script: | 34 | const script = require('.github/scripts/label_related_issue.js') 35 | await script({github, context, core}) 36 | -------------------------------------------------------------------------------- /.github/scripts/enforce_acknowledgment.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_ACTION, 3 | PR_AUTHOR, 4 | PR_BODY, 5 | PR_NUMBER, 6 | IGNORE_AUTHORS, 7 | LABEL_BLOCK, 8 | LABEL_BLOCK_REASON 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_ACTION != "opened") { 17 | return core.notice("Only newly open PRs are labelled to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; 21 | const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); 22 | if (isMatch == null) { 23 | core.info(`No related issue found, maybe the author didn't use the template but there is one.`) 24 | 25 | let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; 26 | await github.rest.issues.createComment({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | body: msg, 30 | issue_number: PR_NUMBER, 31 | }); 32 | 33 | return await github.rest.issues.addLabels({ 34 | issue_number: PR_NUMBER, 35 | owner: context.repo.owner, 36 | repo: context.repo.repo, 37 | labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/dbb_stream_events/status_approved_with_no_workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "8178a46079764b693e00ef96c1f6cfa6", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661270661.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "23/08/2022 16:04:19" }, 14 | "contract_id": { "S": "b9c03b51-5b8c-47c8-9d76-223679cde774" }, 15 | "contract_status": { "S": "APPROVED" }, 16 | "property_id": { "S": "usa/anytown/main-street/999" } 17 | }, 18 | "OldImage": { 19 | "contract_last_modified_on": { "S": "23/08/2022 15:51:44" }, 20 | "contract_id": { "S": "b9c03b51-5b8c-47c8-9d76-223679cde774" }, 21 | "contract_status": { "S": "DRAFT" }, 22 | "property_id": { "S": "usa/anytown/main-street/999" } 23 | }, 24 | "SequenceNumber": "200000000005392276079", 25 | "SizeBytes": 339, 26 | "StreamViewType": "NEW_AND_OLD_IMAGES" 27 | }, 28 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/scripts/label_missing_related_issue.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_ACTION, 3 | PR_AUTHOR, 4 | PR_BODY, 5 | PR_NUMBER, 6 | IGNORE_AUTHORS, 7 | LABEL_BLOCK, 8 | LABEL_BLOCK_REASON 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_ACTION != "opened") { 17 | return core.notice("Only newly open PRs are labelled to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; 21 | const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); 22 | if (isMatch == null) { 23 | core.info(`No related issue found, maybe the author didn't use the template but there is one.`) 24 | 25 | let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; 26 | await github.rest.issues.createComment({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | body: msg, 30 | issue_number: PR_NUMBER, 31 | }); 32 | 33 | return await github.rest.issues.addLabels({ 34 | issue_number: PR_NUMBER, 35 | owner: context.repo.owner, 36 | repo: context.repo.repo, 37 | labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/label_pr_on_title.yml: -------------------------------------------------------------------------------- 1 | name: Label PR based on title 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | # Guardrails to only ever run if PR recording workflow was indeed 12 | # run in a PR event and ran successfully 13 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 14 | uses: ./.github/workflows/reusable_export_pr_details.yml 15 | with: 16 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 17 | workflow_origin: ${{ github.event.repository.full_name }} 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | label_pr: 21 | needs: get_pr_details 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | - name: "Label PR based on title" 27 | uses: actions/github-script@v7 28 | env: 29 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 30 | PR_TITLE: ${{ needs.get_pr_details.outputs.prTitle }} 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | # This safely runs in our base repo, not on fork 34 | # thus allowing us to provide a write access token to label based on PR title 35 | # and label PR based on semantic title accordingly 36 | script: | 37 | const script = require('.github/scripts/label_pr_based_on_title.js') 38 | await script({github, context, core}) 39 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/create_contract.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { 4 | findOutputValue, 5 | clearDatabase, 6 | getCloudWatchLogsValues, 7 | sleep, 8 | } from './helper'; 9 | 10 | describe('Testing creating contracts', () => { 11 | let apiUrl: string; 12 | 13 | beforeAll(async () => { 14 | // Clear DB 15 | await clearDatabase(); 16 | // Find API Endpoint 17 | apiUrl = await findOutputValue('ApiUrl'); 18 | }); 19 | 20 | afterAll(async () => { 21 | // Clear DB 22 | await clearDatabase(); 23 | }); 24 | 25 | it('Should create a item in DynamoDB and fire a eventbridge event', async () => { 26 | const response = await fetch(`${apiUrl}contracts`, { 27 | method: 'POST', 28 | headers: { 'content-type': 'application/json' }, 29 | body: '{ "address": { "country": "USA", "city": "Anytown", "street": "Main Street", "number": 111 }, "seller_name": "John Doe", "property_id": "usa/anytown/main-street/111" }', 30 | }); 31 | expect(response.status).toBe(200); 32 | const json = await response.json(); 33 | expect(json).toEqual({ message: 'OK' }); 34 | await sleep(15000); 35 | const event = await getCloudWatchLogsValues( 36 | 'usa/anytown/main-street/111' 37 | ).next(); 38 | expect(event.value['detail-type']).toEqual('ContractStatusChanged'); 39 | expect(event.value['detail'].property_id).toEqual( 40 | 'usa/anytown/main-street/111' 41 | ); 42 | expect(event.value['detail'].contract_status).toEqual('DRAFT'); 43 | }, 30000); 44 | }); 45 | -------------------------------------------------------------------------------- /unicorn_web/tests/integration/requestApproval.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { 4 | initialiseDatabase, 5 | findOutputValue, 6 | clearDatabase, 7 | getCloudWatchLogsValues, 8 | sleep, 9 | } from './helper'; 10 | 11 | describe('Testing approval requests', () => { 12 | let apiUrl: string; 13 | 14 | beforeAll(async () => { 15 | // Clear DB 16 | await clearDatabase(); 17 | // Load data 18 | await initialiseDatabase(); 19 | // Find API Endpoint 20 | apiUrl = await findOutputValue('uni-prop-local-web', 'UnicornWebApiUrl'); 21 | }, 10000); 22 | 23 | afterAll(async () => { 24 | // Clear DB 25 | await clearDatabase(); 26 | }); 27 | 28 | it('Should a confirm the approval request and fire a eventbridge event', async () => { 29 | const response = await fetch(`${apiUrl}request_approval`, { 30 | method: 'POST', 31 | headers: { 'content-type': 'application/json' }, 32 | body: '{"property_id":"USA/Anytown/main-street/111"}', 33 | }); 34 | expect(response.status).toBe(200); 35 | const json = await response.json(); 36 | expect(json).toEqual({ message: 'OK' }); 37 | await sleep(5000); 38 | const event = await getCloudWatchLogsValues( 39 | 'USA/Anytown/main-street/111' 40 | ).next(); 41 | expect(event.value['detail-type']).toEqual('PublicationApprovalRequested'); 42 | expect(event.value['detail'].property_id).toEqual( 43 | 'USA/Anytown/main-street/111' 44 | ); 45 | expect(event.value['detail'].status).toEqual('PENDING'); 46 | }, 20000); 47 | }); 48 | -------------------------------------------------------------------------------- /unicorn_approvals/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Approvals 2 | 3 | ![Properties Approval Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-approvals.png) 4 | 5 | ## Architecture overview 6 | 7 | **Unicorn Approvals** uses an AWS Step Functions state machine to approve property listings for Unicorn Web. The workflow checks for contract information, description sentiment and safe images, and verifies the contract is approved before approving the listing. It publishes the result via the `PublicationEvaluationCompleted` event. 8 | 9 | A Unicorn Properties agent initiates the workflow by requesting to approve a listing, generating a `PublicationApprovalRequested` event with property information. To decouple from the Contracts Service, the Approvals service maintains a local copy of contract status by consuming the ContractStatusChanged event. 10 | 11 | The workflow checks the contract state. If the contract is in the WaitForContractApproval state, it updates the contract status for the property with its task token, triggering a DynamoDB stream event. The Property Approval Sync function handles these events and passes the task token back to the state machine based on the contract state. 12 | 13 | If the workflow completes successfully, it emits a PublicationEvaluationCompleted event with an **approved** or **declined** evaluation result, which Unicorn Web listens to update its publication flag. 14 | 15 | **Note:** Upon deleting the CloudFormation stack for this service, check if the `ApprovalStateMachine` StepFunction doesn't have any executions in `RUNNING` state. If there are, cancel those execution prior to deleting the CloudFormation stack. -------------------------------------------------------------------------------- /unicorn_web/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Web 2 | 3 | ![Properties Web Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-properties-web.png) 4 | 5 | ## Architecture Overview 6 | 7 | Unicorn Web lets customers search for and view property listings. The Web API also allows Unicorn Properties agents to request approval for specific properties that they want to publish so they may be returned in customer searches results. These requests are sent to the Unicorn Approvals service for validation. 8 | 9 | Lambda functions handle API Gateway requests to: 10 | 11 | - Search approved property listings: The **Search function** retrieves property listings marked as APPROVED from the DynamoDB table using multiple search patterns. 12 | 13 | - Request property listing approval: The **Request Approval function** sends an EventBridge event requesting approval for a property listing specified in the payload. 14 | 15 | - Process approved listings: The **Publication Evaluation Event Handler function** processes `PublicationEvaluationCompleted` events from the Unicorn Approvals service and writes the evaluation result to the DynamoDB table. 16 | 17 | ### Testing the APIs 18 | 19 | ```bash 20 | export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-web --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text` 21 | 22 | curl --location --request POST "${API}request_approval" \ 23 | --header 'Content-Type: application/json' \ 24 | --data-raw '{"PropertyId": "usa/anytown/main-street/111"}' 25 | 26 | 27 | curl -X POST ${API_URL}request_approval \ 28 | -H 'Content-Type: application/json' \ 29 | -d '{"PropertyId":"usa/anytown/main-street/111"}' | jq 30 | ``` 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "develop", main ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "develop" ] 9 | schedule: 10 | - cron: '42 8 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'javascript' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 37 | # If this step fails, then you should remove it and run the build manually (see below) 38 | # - name: Autobuild 39 | # uses: github/codeql-action/autobuild@v3 40 | 41 | # ℹ️ Command-line programs to run using the OS shell. 42 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 43 | 44 | # If the Autobuild fails above, remove it and uncomment the following three lines. 45 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 46 | 47 | # - run: | 48 | # echo "Run, Build Application using script" 49 | # ./location_of_script_within_repo/buildscript.sh 50 | 51 | - name: Perform CodeQL Analysis 52 | uses: github/codeql-action/analyze@v3 53 | -------------------------------------------------------------------------------- /unicorn_shared/Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | 3 | ENVIRONMENTS = local dev prod 4 | 5 | .PHONY: deploy-namespaces deploy-images delete-namespaces delete-images 6 | 7 | deploy-namespaces: ## Deploys global Unicorn Properties namespaces for all Stages 8 | aws cloudformation create-stack --stack-name uni-prop-namespaces --template-body file://uni-prop-namespaces.yaml --capabilities CAPABILITY_AUTO_EXPAND 9 | 10 | deploy-images: ## Deploys shared images stack for local dev prod stages 11 | @for env in $(ENVIRONMENTS); do \ 12 | stage=$$env; \ 13 | if ! aws cloudformation describe-stacks --stack-name "uni-prop-$$env-images" >/dev/null 2>&1; then \ 14 | echo "Creating shared images stack for $$env environment"; \ 15 | aws cloudformation create-stack \ 16 | --stack-name "uni-prop-$$env-images" \ 17 | --template-body file://uni-prop-images.yaml \ 18 | --parameters ParameterKey=Stage,ParameterValue=$$stage \ 19 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND || echo "Stack creation failed!"; \ 20 | fi; \ 21 | done 22 | 23 | delete-namespaces: ## Depletes Unicorn Properties namespaces 24 | aws cloudformation delete-stack --stack-name uni-prop-namespaces 25 | 26 | delete-images: ## Deletes all shared images stacks 27 | @for env in $(ENVIRONMENTS); do \ 28 | stage=$$env; \ 29 | if aws cloudformation describe-stacks --stack-name "uni-prop-$$env-images" >/dev/null 2>&1; then \ 30 | echo "Deleting shared images stack for $$env environment"; \ 31 | aws cloudformation delete-stack \ 32 | --stack-name "uni-prop-$$env-images"; \ 33 | fi; \ 34 | done 35 | 36 | list-parameters: ## Lists all parameters in the Unicorn Properties namespace 37 | aws ssm get-parameters-by-path --path "/uni-prop" --recursive --with-decryption --query 'Parameters[*].[Name,Value,Type]' --output table -------------------------------------------------------------------------------- /.github/scripts/label_missing_acknowledgement_section.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_ACTION, 3 | PR_AUTHOR, 4 | PR_BODY, 5 | PR_NUMBER, 6 | IGNORE_AUTHORS, 7 | LABEL_BLOCK, 8 | LABEL_BLOCK_MISSING_LICENSE_AGREEMENT 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_ACTION != "opened") { 17 | return core.notice("Only newly open PRs are labelled to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ACK_SECTION_REGEX = /By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice./; 21 | 22 | const isMatch = RELATED_ACK_SECTION_REGEX.exec(PR_BODY); 23 | if (isMatch == null) { 24 | core.info(`No acknowledgement section found, maybe the author didn't use the template but there is one.`) 25 | 26 | let msg = "No acknowledgement section found. Please make sure you used the template to open a PR and didn't remove the acknowledgment section. Check the template here: https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/blob/develop/.github/PULL_REQUEST_TEMPLATE.md#acknowledgment"; 27 | await github.rest.issues.createComment({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | body: msg, 31 | issue_number: PR_NUMBER, 32 | }); 33 | 34 | return await github.rest.issues.addLabels({ 35 | issue_number: PR_NUMBER, 36 | owner: context.repo.owner, 37 | repo: context.repo.repo, 38 | labels: [LABEL_BLOCK, LABEL_BLOCK_MISSING_LICENSE_AGREEMENT] 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/scripts/label_pr_based_on_title.js: -------------------------------------------------------------------------------- 1 | const { PR_NUMBER, PR_TITLE } = require("./constants") 2 | 3 | module.exports = async ({github, context, core}) => { 4 | const FEAT_REGEX = /feat(\((.+)\))?(:.+)/ 5 | const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/ 6 | const DOCS_REGEX = /(docs|doc)(\((.+)\))?(:.+)/ 7 | const CHORE_REGEX = /(chore)(\((.+)\))?(:.+)/ 8 | const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/ 9 | const REFACTOR_REGEX = /(refactor)(\((.+)\))?(:.+)/ 10 | 11 | const labels = { 12 | "feature": FEAT_REGEX, 13 | "bug": BUG_REGEX, 14 | "documentation": DOCS_REGEX, 15 | "internal": CHORE_REGEX, 16 | "enhancement": REFACTOR_REGEX, 17 | "deprecated": DEPRECATED_REGEX, 18 | } 19 | 20 | // Maintenance: We should keep track of modified PRs in case their titles change 21 | let miss = 0; 22 | try { 23 | for (const label in labels) { 24 | const matcher = new RegExp(labels[label]) 25 | const matches = matcher.exec(PR_TITLE) 26 | if (matches != null) { 27 | core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`) 28 | 29 | await github.rest.issues.addLabels({ 30 | issue_number: PR_NUMBER, 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | labels: [label] 34 | }) 35 | 36 | return; 37 | } else { 38 | core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`) 39 | miss += 1 40 | } 41 | } 42 | } finally { 43 | if (miss == Object.keys(labels).length) { 44 | core.notice(`PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...`) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /unicorn_web/infrastructure/web-service/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-web" 5 | s3_prefix = "uni-prop-local-web" 6 | resolve_s3 = true 7 | resolve_image_repositories = true 8 | 9 | [default.build.parameters] 10 | cached = true 11 | parallel = true 12 | 13 | [default.deploy.parameters] 14 | disable_rollback = true 15 | confirm_changeset = false 16 | fail_on_empty_changeset = false 17 | capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] 18 | parameter_overrides = ["Stage=local"] 19 | 20 | [default.validate.parameters] 21 | lint = true 22 | 23 | [default.sync.parameters] 24 | watch = true 25 | 26 | [default.local_start_api.parameters] 27 | warm_containers = "EAGER" 28 | 29 | [default.local_start_lambda.parameters] 30 | warm_containers = "EAGER" 31 | 32 | # ############################################## # 33 | 34 | [dev] 35 | [dev.global] 36 | [dev.global.parameters] 37 | stack_name = "uni-prop-dev-web" 38 | s3_prefix = "uni-prop-dev-web" 39 | resolve_s3 = true 40 | resolve_image_repositories = true 41 | 42 | [dev.deploy] 43 | [dev.deploy.parameters] 44 | template = ".aws-sam/packaged-dev-web-service.yaml" 45 | confirm_changeset = false 46 | fail_on_empty_changeset = false 47 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 48 | parameter_overrides = "Stage=dev" 49 | 50 | # ############################################## # 51 | 52 | [prod] 53 | [prod.global] 54 | [prod.global.parameters] 55 | stack_name = "uni-prop-prod-web" 56 | s3_prefix = "uni-prop-prod-web" 57 | resolve_s3 = true 58 | resolve_image_repositories = true 59 | 60 | [prod.deploy] 61 | [prod.deploy.parameters] 62 | template = ".aws-sam/packaged-prod-web-service.yaml" 63 | confirm_changeset = false 64 | fail_on_empty_changeset = false 65 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 66 | parameter_overrides = "Stage=prod" -------------------------------------------------------------------------------- /unicorn_approvals/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "properties", 3 | "version": "1.0.0", 4 | "description": "Properties Module for Serverless Developer Experience Reference Architecture - Node", 5 | "repository": "https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/tree/develop", 6 | "license": "MIT", 7 | "author": "Amazon.com, Inc.", 8 | "main": "app.js", 9 | "scripts": { 10 | "build": "sam build", 11 | "compile": "tsc", 12 | "deploy": "sam build && sam deploy --no-confirm-changeset", 13 | "format": "prettier --write '**/*.ts'", 14 | "lint": "eslint --ext .ts --quiet --fix", 15 | "test": "NODE_OPTIONS='--experimental-vm-modules' jest --runInBand", 16 | "unit": "jest --config=jest.config.test.unit.ts" 17 | }, 18 | "dependencies": { 19 | "@aws-lambda-powertools/commons": "^2.28.1", 20 | "@aws-lambda-powertools/logger": "^2.28.1", 21 | "@aws-lambda-powertools/metrics": "^2.28.1", 22 | "@aws-lambda-powertools/tracer": "^2.28.1", 23 | "@aws-sdk/client-dynamodb": "^3.931.0", 24 | "@aws-sdk/client-sfn": "^3.931.0", 25 | "@aws-sdk/util-dynamodb": "^3.931.0", 26 | "aws-lambda": "^1.0.7" 27 | }, 28 | "devDependencies": { 29 | "@aws-sdk/client-cloudformation": "^3.931.0", 30 | "@aws-sdk/client-cloudwatch-logs": "^3.931.0", 31 | "@aws-sdk/client-eventbridge": "^3.931.0", 32 | "@aws-sdk/lib-dynamodb": "^3.931.0", 33 | "@jest/test-sequencer": "^30.2.0", 34 | "@types/aws-lambda": "^8.10.158", 35 | "@types/jest": "^30.0.0", 36 | "@types/node": "^22.19.1", 37 | "aws-sdk-client-mock": "^4.1.0", 38 | "esbuild": "^0.27.0", 39 | "esbuild-jest": "^0.5.0", 40 | "eslint": "^9.39.1", 41 | "eslint-config-prettier": "^10.1.8", 42 | "globals": "^16.5.0", 43 | "jest": "^30.2.0", 44 | "prettier": "^3.6.2", 45 | "ts-jest": "^29.4.5", 46 | "ts-node": "^10.9.2", 47 | "typescript": "^5.9.3", 48 | "typescript-eslint": "^8.46.4" 49 | } 50 | } -------------------------------------------------------------------------------- /.github/scripts/label_related_issue.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_AUTHOR, 3 | PR_BODY, 4 | PR_NUMBER, 5 | IGNORE_AUTHORS, 6 | LABEL_PENDING_RELEASE, 7 | HANDLE_MAINTAINERS_TEAM, 8 | PR_IS_MERGED, 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_IS_MERGED == "false") { 17 | return core.notice("Only merged PRs to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; 21 | 22 | const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); 23 | 24 | try { 25 | if (!isMatch) { 26 | core.setFailed(`Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}`); 27 | return await github.rest.issues.createComment({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`, 31 | issue_number: PR_NUMBER, 32 | }); 33 | } 34 | } catch (error) { 35 | core.setFailed(`Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}`); 36 | throw new Error(error); 37 | } 38 | 39 | const { groups: {issue} } = isMatch 40 | 41 | try { 42 | core.info(`Auto-labeling related issue ${issue} for release`) 43 | return await github.rest.issues.addLabels({ 44 | issue_number: issue, 45 | owner: context.repo.owner, 46 | repo: context.repo.repo, 47 | labels: [LABEL_PENDING_RELEASE] 48 | }) 49 | } catch (error) { 50 | core.setFailed(`Is this issue number (${issue}) valid? Perhaps a discussion?`); 51 | throw new Error(error); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /unicorn_approvals/infrastructure/approvals-service/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-approvals" 5 | s3_prefix = "uni-prop-local-approvals" 6 | resolve_s3 = true 7 | resolve_image_repositories = true 8 | 9 | [default.build.parameters] 10 | cached = true 11 | parallel = true 12 | 13 | [default.deploy.parameters] 14 | disable_rollback = true 15 | confirm_changeset = false 16 | fail_on_empty_changeset = false 17 | capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] 18 | parameter_overrides = ["Stage=local"] 19 | 20 | [default.validate.parameters] 21 | lint = true 22 | 23 | [default.sync.parameters] 24 | watch = true 25 | 26 | [default.local_start_api.parameters] 27 | warm_containers = "EAGER" 28 | 29 | [default.local_start_lambda.parameters] 30 | warm_containers = "EAGER" 31 | 32 | # ############################################## # 33 | 34 | [dev] 35 | [dev.global] 36 | [dev.global.parameters] 37 | stack_name = "uni-prop-dev-approvals" 38 | s3_prefix = "uni-prop-dev-approvals" 39 | resolve_s3 = true 40 | resolve_image_repositories = true 41 | 42 | [dev.deploy] 43 | [dev.deploy.parameters] 44 | template = ".aws-sam/packaged-dev-approvals-service.yaml" 45 | confirm_changeset = false 46 | fail_on_empty_changeset = false 47 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 48 | parameter_overrides = "Stage=dev" 49 | 50 | # ############################################## # 51 | 52 | [prod] 53 | [prod.global] 54 | [prod.global.parameters] 55 | stack_name = "uni-prop-prod-approvals" 56 | s3_prefix = "uni-prop-prod-approvals" 57 | resolve_s3 = true 58 | resolve_image_repositories = true 59 | 60 | [prod.deploy] 61 | [prod.deploy.parameters] 62 | template = ".aws-sam/packaged-prod-approvals-service.yaml" 63 | confirm_changeset = false 64 | fail_on_empty_changeset = false 65 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 66 | parameter_overrides = "Stage=prod" -------------------------------------------------------------------------------- /unicorn_contracts/infrastructure/contracts-service/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-contracts" 5 | s3_prefix = "uni-prop-local-contracts" 6 | resolve_s3 = true 7 | resolve_image_repositories = true 8 | 9 | [default.build.parameters] 10 | cached = true 11 | parallel = true 12 | 13 | [default.deploy.parameters] 14 | disable_rollback = true 15 | confirm_changeset = false 16 | fail_on_empty_changeset = false 17 | capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] 18 | parameter_overrides = ["Stage=local"] 19 | 20 | [default.validate.parameters] 21 | lint = true 22 | 23 | [default.sync.parameters] 24 | watch = true 25 | 26 | [default.local_start_api.parameters] 27 | warm_containers = "EAGER" 28 | 29 | [default.local_start_lambda.parameters] 30 | warm_containers = "EAGER" 31 | 32 | # ############################################## # 33 | 34 | [dev] 35 | [dev.global] 36 | [dev.global.parameters] 37 | stack_name = "uni-prop-dev-contracts" 38 | s3_prefix = "uni-prop-dev-contracts" 39 | resolve_s3 = true 40 | resolve_image_repositories = true 41 | 42 | [dev.deploy] 43 | [dev.deploy.parameters] 44 | template = ".aws-sam/packaged-dev-contracts-service.yaml" 45 | confirm_changeset = false 46 | fail_on_empty_changeset = false 47 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 48 | parameter_overrides = "Stage=dev" 49 | 50 | # ############################################## # 51 | 52 | [prod] 53 | [prod.global] 54 | [prod.global.parameters] 55 | stack_name = "uni-prop-prod-contracts" 56 | s3_prefix = "uni-prop-prod-contracts" 57 | resolve_s3 = true 58 | resolve_image_repositories = true 59 | 60 | [prod.deploy] 61 | [prod.deploy.parameters] 62 | template = ".aws-sam/packaged-prod-contracts-service.yaml" 63 | confirm_changeset = false 64 | fail_on_empty_changeset = false 65 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 66 | parameter_overrides = "Stage=prod" -------------------------------------------------------------------------------- /.gitea/actions/configure/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Configure Build Environment' 2 | description: 'A reusable action that configures the build environment' 3 | 4 | runs: 5 | using: 'composite' 6 | steps: 7 | - run: echo "**** Setup Python ****" 8 | - name: Setup Python 9 | uses: actions/setup-python@v6 10 | with: 11 | python-version: ${{ env.PYTHON_VERSION }} 12 | - run: python --version 13 | 14 | - run: echo "**** Install Python based tooling ****" 15 | - name: Install Python based tooling 16 | run: pip install cfn-lint cfn-lint-serverless 17 | 18 | - run: echo "**** Setup NodeJS ****" 19 | - name: Setup NodeJS 20 | uses: actions/setup-node@v5 21 | with: 22 | node-version: ${{ env.NODE_VERSION }} 23 | - run: 'echo "NodeJS version: $(node --version)"' 24 | 25 | - run: echo "**** Setup pnpm ****" 26 | - uses: pnpm/action-setup@v4 27 | with: 28 | version: 10 29 | - run: 'echo "pnpm version: $(pnpm --version)"' 30 | 31 | - run: echo "**** Install NodeJS based tooling ****" 32 | - name: Install NodeJS based tooling 33 | run: npm install -g esbuild 34 | 35 | - run: echo "**** Install application dependencies ****" 36 | - name: Install application dependencies 37 | run: pnpm install --no-frozen-lockfile 38 | working-directory: ${{ gitea.workspace }}/${{ env.SERVICE_FOLDER }} 39 | 40 | - run: echo "**** Install AWS CLI ****" 41 | - name: Install AWS CLI 42 | run: | 43 | curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 44 | unzip -q awscliv2.zip 45 | sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update > /dev/null 2>&1 46 | aws --version 47 | 48 | - run: echo "**** Install SAM CLI ****" 49 | - name: Install SAM CLI 50 | uses: aws-actions/setup-sam@v2 51 | with: 52 | use-installer: true 53 | - run: sam --version -------------------------------------------------------------------------------- /unicorn_web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "version": "1.0.0", 4 | "description": "Contracts Module for Serverless Developer Experience Reference Architecture - Node", 5 | "repository": "https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/tree/develop", 6 | "license": "MIT", 7 | "author": "Amazon.com, Inc.", 8 | "main": "app.js", 9 | "scripts": { 10 | "build": "sam build", 11 | "compile": "tsc", 12 | "deploy": "sam build && sam deploy --no-confirm-changeset", 13 | "format": "prettier --write '**/*.ts'", 14 | "lint": "eslint --ext .ts --quiet --fix", 15 | "test": "NODE_OPTIONS='--experimental-vm-modules' jest --runInBand", 16 | "unit": "jest --config=jest.config.test.unit.ts" 17 | }, 18 | "dependencies": { 19 | "@aws-lambda-powertools/commons": "^2.28.1", 20 | "@aws-lambda-powertools/logger": "^2.28.1", 21 | "@aws-lambda-powertools/metrics": "^2.28.1", 22 | "@aws-lambda-powertools/tracer": "^2.28.1", 23 | "@aws-sdk/client-cloudformation": "^3.931.0", 24 | "@aws-sdk/client-cloudwatch-logs": "^3.931.0", 25 | "@aws-sdk/client-dynamodb": "^3.931.0", 26 | "@aws-sdk/client-eventbridge": "^3.931.0", 27 | "@aws-sdk/lib-dynamodb": "^3.931.0", 28 | "@aws-sdk/util-dynamodb": "^3.931.0", 29 | "aws-lambda": "^1.0.7" 30 | }, 31 | "devDependencies": { 32 | "@jest/test-sequencer": "^30.2.0", 33 | "@types/aws-lambda": "^8.10.158", 34 | "@types/cfn-response": "^1.0.8", 35 | "@types/jest": "^30.0.0", 36 | "@types/node": "^22.19.1", 37 | "aws-sdk-client-mock": "^4.1.0", 38 | "esbuild": "^0.27.0", 39 | "esbuild-jest": "^0.5.0", 40 | "eslint": "^9.39.1", 41 | "eslint-config-prettier": "^10.1.8", 42 | "globals": "^16.5.0", 43 | "jest": "^30.2.0", 44 | "prettier": "^3.6.2", 45 | "ts-jest": "^29.4.5", 46 | "ts-node": "^10.9.2", 47 | "typescript": "^5.9.3", 48 | "typescript-eslint": "^8.46.4" 49 | } 50 | } -------------------------------------------------------------------------------- /unicorn_contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "version": "1.0.0", 4 | "description": "Contracts Module for Serverless Developer Experience Reference Architecture - Node", 5 | "repository": "https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/tree/develop", 6 | "license": "MIT", 7 | "author": "Amazon.com, Inc.", 8 | "main": "app.js", 9 | "scripts": { 10 | "build": "sam build", 11 | "compile": "tsc", 12 | "deploy": "sam build && sam deploy --no-confirm-changeset", 13 | "format": "prettier --write '**/*.ts'", 14 | "lint": "eslint --ext .ts --quiet --fix", 15 | "test": "NODE_OPTIONS='--experimental-vm-modules' jest --runInBand", 16 | "unit": "jest --config=jest.config.test.unit.ts" 17 | }, 18 | "dependencies": { 19 | "@aws-lambda-powertools/commons": "^2.28.1", 20 | "@aws-lambda-powertools/logger": "^2.28.1", 21 | "@aws-lambda-powertools/metrics": "^2.28.1", 22 | "@aws-lambda-powertools/tracer": "^2.28.1", 23 | "@aws-sdk/client-cloudformation": "^3.931.0", 24 | "@aws-sdk/client-cloudwatch-logs": "^3.931.0", 25 | "@aws-sdk/client-dynamodb": "^3.931.0", 26 | "@aws-sdk/client-eventbridge": "^3.931.0", 27 | "@aws-sdk/client-sqs": "^3.931.0", 28 | "@aws-sdk/lib-dynamodb": "^3.931.0", 29 | "@aws-sdk/util-dynamodb": "^3.931.0", 30 | "aws-lambda": "^1.0.7" 31 | }, 32 | "devDependencies": { 33 | "@jest/test-sequencer": "^30.2.0", 34 | "@types/aws-lambda": "^8.10.158", 35 | "@types/jest": "^30.0.0", 36 | "@types/node": "^22.19.1", 37 | "aws-sdk-client-mock": "^4.1.0", 38 | "esbuild": "^0.27.0", 39 | "esbuild-jest": "^0.5.0", 40 | "eslint": "^9.39.1", 41 | "eslint-config-prettier": "^10.1.8", 42 | "globals": "^16.5.0", 43 | "jest": "^30.2.0", 44 | "prettier": "^3.6.2", 45 | "ts-jest": "^29.4.5", 46 | "ts-node": "^10.9.2", 47 | "typescript": "^5.9.3", 48 | "typescript-eslint": "^8.46.4" 49 | } 50 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a reproducible bug to help us improve 3 | title: "Bug: TITLE" 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for submitting a bug report. Please add as much information as possible to help us reproduce, and remove any potential sensitive data. 10 | - type: textarea 11 | id: expected_behaviour 12 | attributes: 13 | label: Expected Behaviour 14 | description: Please share details on the behaviour you expected 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: current_behaviour 19 | attributes: 20 | label: Current Behaviour 21 | description: Please share details on the current issue 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: code_snippet 26 | attributes: 27 | label: Code snippet 28 | description: Please share a code snippet to help us reproduce the issue 29 | render: csharp 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: solution 34 | attributes: 35 | label: Possible Solution 36 | description: If known, please suggest a potential resolution 37 | validations: 38 | required: false 39 | - type: textarea 40 | id: steps 41 | attributes: 42 | label: Steps to Reproduce 43 | description: Please share how we might be able to reproduce this issue 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: logs 48 | attributes: 49 | label: Debugging logs 50 | description: If available, please share debugging logs 51 | render: csharp 52 | validations: 53 | required: false 54 | - type: markdown 55 | attributes: 56 | value: | 57 | -- 58 | **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. 59 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/update_contract.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { 4 | initialiseDatabase, 5 | findOutputValue, 6 | clearDatabase, 7 | getCloudWatchLogsValues, 8 | sleep, 9 | } from './helper'; 10 | 11 | describe('Testing updating contracts', () => { 12 | let apiUrl: string; 13 | 14 | beforeAll(async () => { 15 | // Clear DB 16 | await clearDatabase(); 17 | // Load data 18 | await initialiseDatabase(); 19 | // Find API Endpoint 20 | apiUrl = await findOutputValue('ApiUrl'); 21 | // Create DRAFT contract 22 | await fetch(`${apiUrl}contracts`, { 23 | method: 'POST', 24 | headers: { 'content-type': 'application/json' }, 25 | body: '{ "address": { "country": "USA", "city": "Anytown", "street": "Main Street", "number": 111 }, "seller_name": "John Doe", "property_id": "usa/anytown/main-street/111" }', 26 | }); 27 | }, 40000); 28 | 29 | afterAll(async () => { 30 | // Clear DB 31 | await clearDatabase(); 32 | }); 33 | 34 | it('Should update the item in DynamoDB and fire a eventbridge event when an existing contract is updated', async () => { 35 | const response = await fetch(`${apiUrl}contracts`, { 36 | method: 'PUT', 37 | headers: { 'content-type': 'application/json' }, 38 | body: '{"property_id":"usa/anytown/main-street/111"}', 39 | }); 40 | expect(response.status).toBe(200); 41 | const json = await response.json(); 42 | expect(json).toEqual({ message: 'OK' }); 43 | await sleep(15000); 44 | const event = await getCloudWatchLogsValues( 45 | 'usa/anytown/main-street/111' 46 | ).next(); 47 | expect(event.value['detail-type']).toEqual('ContractStatusChanged'); 48 | expect(event.value['detail'].property_id).toEqual( 49 | 'usa/anytown/main-street/111' 50 | ); 51 | expect(event.value['detail'].contract_status).toEqual('APPROVED'); 52 | }, 30000); 53 | }); 54 | -------------------------------------------------------------------------------- /unicorn_approvals/src/schema/unicorn_contracts/contractstatuschanged/AWSEvent.ts: -------------------------------------------------------------------------------- 1 | export class AWSEvent { 2 | 'detail': T; 3 | 'detail_type': string; 4 | 'resources': string[]; 5 | 'id': string; 6 | 'source': string; 7 | 'time': Date; 8 | 'region': string; 9 | 'version': string; 10 | 'account': string; 11 | 12 | static discriminator: string | undefined = undefined; 13 | 14 | static detail = 'detail'; 15 | 16 | static genericType = 'T'; 17 | 18 | static attributeTypeMap: { 19 | name: string; 20 | baseName: string; 21 | type: string; 22 | }[] = [ 23 | { 24 | name: AWSEvent.detail, 25 | baseName: AWSEvent.detail, 26 | type: AWSEvent.genericType, 27 | }, 28 | { 29 | name: 'detail_type', 30 | baseName: 'detail-type', 31 | type: 'string', 32 | }, 33 | { 34 | name: 'resources', 35 | baseName: 'resources', 36 | type: 'Array', 37 | }, 38 | { 39 | name: 'id', 40 | baseName: 'id', 41 | type: 'string', 42 | }, 43 | { 44 | name: 'source', 45 | baseName: 'source', 46 | type: 'string', 47 | }, 48 | { 49 | name: 'time', 50 | baseName: 'time', 51 | type: 'Date', 52 | }, 53 | { 54 | name: 'region', 55 | baseName: 'region', 56 | type: 'string', 57 | }, 58 | { 59 | name: 'version', 60 | baseName: 'version', 61 | type: 'string', 62 | }, 63 | { 64 | name: 'account', 65 | baseName: 'account', 66 | type: 'string', 67 | }, 68 | ]; 69 | 70 | public static getAttributeTypeMap() { 71 | return AWSEvent.attributeTypeMap; 72 | } 73 | 74 | public static updateAttributeTypeMapDetail(type: string) { 75 | const index = AWSEvent.attributeTypeMap.indexOf({ 76 | name: AWSEvent.detail, 77 | baseName: AWSEvent.detail, 78 | type: AWSEvent.genericType, 79 | }); 80 | this.attributeTypeMap[index] = { 81 | name: AWSEvent.detail, 82 | baseName: AWSEvent.detail, 83 | type, 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/on_opened_pr.yml: -------------------------------------------------------------------------------- 1 | name: On new PR 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | uses: ./.github/workflows/reusable_export_pr_details.yml 13 | with: 14 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 15 | workflow_origin: ${{ github.event.repository.full_name }} 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | check_related_issue: 19 | needs: get_pr_details 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: "Ensure related issue is present" 24 | uses: actions/github-script@v7 25 | env: 26 | PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} 27 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 28 | PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} 29 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | script: | 33 | const script = require('.github/scripts/label_missing_related_issue.js') 34 | await script({github, context, core}) 35 | check_acknowledge_section: 36 | needs: get_pr_details 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: "Ensure acknowledgement section is present" 41 | uses: actions/github-script@v7 42 | env: 43 | PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} 44 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 45 | PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} 46 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 47 | with: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | script: | 50 | const script = require('.github/scripts/label_missing_acknowledgement_section.js') 51 | await script({github, context, core}) 52 | -------------------------------------------------------------------------------- /unicorn_shared/uni-prop-namespaces.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | Shared namespace configuration for the Unicorn Properties microservices architecture. 5 | This template establishes centralized namespace parameters stored in AWS Systems Manager 6 | Parameter Store, enabling consistent service identification and cross-service communication 7 | across the Contracts, Approvals, and Web services. Deploy this template once per environment 8 | before deploying individual microservices to ensure proper namespace resolution. 9 | 10 | Resources: 11 | UnicornContractsNamespaceParam: 12 | Type: AWS::SSM::Parameter 13 | Properties: 14 | Type: String 15 | Name: !Sub /uni-prop/UnicornContractsNamespace 16 | Value: "unicorn-contracts" 17 | 18 | UnicornApprovalsNamespaceParam: 19 | Type: AWS::SSM::Parameter 20 | Properties: 21 | Type: String 22 | Name: !Sub /uni-prop/UnicornApprovalsNamespace 23 | Value: "unicorn-approvals" 24 | 25 | UnicornWebNamespaceParam: 26 | Type: AWS::SSM::Parameter 27 | Properties: 28 | Type: String 29 | Name: !Sub /uni-prop/UnicornWebNamespace 30 | Value: "unicorn-web" 31 | 32 | Outputs: 33 | UnicornContractsNamespace: 34 | Description: Unicorn Contracts namespace parameter 35 | Value: !Ref UnicornContractsNamespaceParam 36 | 37 | UnicornApprovalsNamespace: 38 | Description: Unicorn Properties namespace parameter 39 | Value: !Ref UnicornApprovalsNamespaceParam 40 | 41 | UnicornWebNamespace: 42 | Description: Unicorn Web namespace parameter 43 | Value: !Ref UnicornWebNamespaceParam 44 | 45 | UnicornContractsNamespaceVale: 46 | Description: Unicorn Contracts namespace parameter value 47 | Value: !GetAtt UnicornContractsNamespaceParam.Value 48 | 49 | UnicornApprovalsNamespaceValue: 50 | Description: Unicorn Properties namespace parameter value 51 | Value: !GetAtt UnicornApprovalsNamespaceParam.Value 52 | 53 | UnicornWebNamespaceValue: 54 | Description: Unicorn Web namespace parameter value 55 | Value: !GetAtt UnicornWebNamespaceParam.Value 56 | -------------------------------------------------------------------------------- /unicorn_approvals/infrastructure/subscriptions/unicorn-contracts-subscriptions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: > 5 | Establishes cross-service event flow by creating an EventBridge rule that facilitates 6 | bus-to-bus communication (subscription rule), routing ContractStatusChanged events from the Unicorn Contracts 7 | event bus to the Unicorn Approvals event bus for downstream processing. 8 | 9 | Parameters: 10 | Stage: 11 | Type: String 12 | Default: local 13 | AllowedValues: 14 | - local 15 | - dev 16 | - prod 17 | 18 | Resources: 19 | #### UNICORN CONTRACTS EVENT SUBSCRIPTIONS 20 | 21 | # EventBridge rule that subscribes to ContractStatusChanged events from Contracts service 22 | # and forwards them to Approvals event bus for processing approval workflows. 23 | ContractStatusChangedSubscriptionRule: 24 | Type: AWS::Events::Rule 25 | Properties: 26 | Name: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}-ContractStatusChanged" 27 | Description: > 28 | Subscription rule for ContractStatusChanged event targeting the Unicorn 29 | Approvals event bus for processing approval workflows. 30 | EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ContractsEventBusArn}}" 31 | EventPattern: 32 | source: 33 | - "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}" 34 | detail-type: 35 | - ContractStatusChanged 36 | Targets: 37 | - Id: SendEventTo 38 | Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ApprovalsEventBusArn}}" 39 | RoleArn: 40 | Fn::ImportValue: 41 | Fn::Sub: "uni-prop-${Stage}-approvals-domain-EventBridgeRoleArn" 42 | Tags: 43 | - Key: rule-owner-service-namespace 44 | Value: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}" 45 | 46 | Outputs: 47 | ContractStatusChangedSubscription: 48 | Description: Rule ARN for Contract service event subscription 49 | Value: 50 | Fn::GetAtt: [ContractStatusChangedSubscriptionRule, Arn] 51 | -------------------------------------------------------------------------------- /unicorn_web/src/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | export class AWSEvent { 4 | 'detail': T; 5 | 'detail_type': string; 6 | 'resources': string[]; 7 | 'id': string; 8 | 'source': string; 9 | 'time': Date; 10 | 'region': string; 11 | 'version': string; 12 | 'account': string; 13 | 14 | static discriminator: string | undefined = undefined; 15 | 16 | static detail = 'detail'; 17 | 18 | static genericType = 'T'; 19 | 20 | static attributeTypeMap: { 21 | name: string; 22 | baseName: string; 23 | type: string; 24 | }[] = [ 25 | { 26 | name: AWSEvent.detail, 27 | baseName: AWSEvent.detail, 28 | type: AWSEvent.genericType, 29 | }, 30 | { 31 | name: 'detail_type', 32 | baseName: 'detail-type', 33 | type: 'string', 34 | }, 35 | { 36 | name: 'resources', 37 | baseName: 'resources', 38 | type: 'Array', 39 | }, 40 | { 41 | name: 'id', 42 | baseName: 'id', 43 | type: 'string', 44 | }, 45 | { 46 | name: 'source', 47 | baseName: 'source', 48 | type: 'string', 49 | }, 50 | { 51 | name: 'time', 52 | baseName: 'time', 53 | type: 'Date', 54 | }, 55 | { 56 | name: 'region', 57 | baseName: 'region', 58 | type: 'string', 59 | }, 60 | { 61 | name: 'version', 62 | baseName: 'version', 63 | type: 'string', 64 | }, 65 | { 66 | name: 'account', 67 | baseName: 'account', 68 | type: 'string', 69 | }, 70 | ]; 71 | 72 | public static getAttributeTypeMap() { 73 | return AWSEvent.attributeTypeMap; 74 | } 75 | 76 | public static updateAttributeTypeMapDetail(type: string) { 77 | const index = AWSEvent.attributeTypeMap.indexOf({ 78 | name: AWSEvent.detail, 79 | baseName: AWSEvent.detail, 80 | type: AWSEvent.genericType, 81 | }); 82 | this.attributeTypeMap[index] = { 83 | name: AWSEvent.detail, 84 | baseName: AWSEvent.detail, 85 | type, 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/unit/contractStatusChangedEventHandler.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { Context, EventBridgeEvent } from 'aws-lambda'; 4 | import { randomUUID } from 'crypto'; 5 | import { lambdaHandler } from '../../src/approvals_service/contractStatusChangedEventHandler'; 6 | import { mockClient } from 'aws-sdk-client-mock'; 7 | import { 8 | DynamoDBClient, 9 | UpdateItemCommandInput, 10 | } from '@aws-sdk/client-dynamodb'; 11 | 12 | describe('Unit tests for contract creation', function () { 13 | const ddbMock = mockClient(DynamoDBClient); 14 | 15 | beforeEach(() => { 16 | ddbMock.reset(); 17 | }); 18 | 19 | test('verifies successful response', async () => { 20 | let cmd: any; 21 | 22 | const dateToCheck = new Date(); 23 | 24 | async function verifyInput(input: any) { 25 | cmd = input as UpdateItemCommandInput; 26 | expect(cmd['Key']['property_id'].S).toEqual('property1'); 27 | expect(cmd['ExpressionAttributeValues'][':c'].S).toEqual('contract1'); 28 | expect(cmd['ExpressionAttributeValues'][':t'].S).toEqual('APPROVED'); 29 | expect(cmd['ExpressionAttributeValues'][':m'].S).toEqual( 30 | dateToCheck.toISOString() 31 | ); 32 | return { 33 | $metadata: { 34 | httpStatusCode: 200, 35 | }, 36 | }; 37 | } 38 | 39 | ddbMock.callsFake(verifyInput); 40 | 41 | const expectedId = randomUUID(); 42 | const context: Context = { 43 | awsRequestId: expectedId, 44 | } as any; 45 | const event: EventBridgeEvent = { 46 | id: expectedId, 47 | account: 'nullAccount', 48 | version: '0', 49 | time: 'nulltime', 50 | region: 'ap-southeast-2', 51 | source: 'unicorn-approvals', 52 | resources: [''], 53 | detail: { 54 | contract_id: 'contract1', 55 | property_id: 'property1', 56 | contract_status: 'APPROVED', 57 | contract_last_modified_on: dateToCheck.toISOString(), 58 | }, 59 | 'detail-type': 'ContractStatusChanged', 60 | }; 61 | 62 | await lambdaHandler(event, context); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /.github/scripts/comment_on_large_pr.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_NUMBER, 3 | PR_ACTION, 4 | PR_AUTHOR, 5 | IGNORE_AUTHORS, 6 | } = require("./constants") 7 | 8 | 9 | /** 10 | * Notify PR author to split XXL PR in smaller chunks 11 | * 12 | * @param {object} core - core functions instance from @actions/core 13 | * @param {object} gh_client - Pre-authenticated REST client (Octokit) 14 | * @param {string} owner - GitHub Organization 15 | * @param {string} repository - GitHub repository 16 | */ 17 | const notifyAuthor = async ({ 18 | core, 19 | gh_client, 20 | owner, 21 | repository, 22 | }) => { 23 | core.info(`Commenting on PR ${PR_NUMBER}`) 24 | 25 | let msg = `### ⚠️Large PR detected⚠️ 26 | 27 | Please consider breaking into smaller PRs to avoid significant review delays. Ignore if this PR has naturally grown to this size after reviews. 28 | `; 29 | 30 | try { 31 | await gh_client.rest.issues.createComment({ 32 | owner: owner, 33 | repo: repository, 34 | body: msg, 35 | issue_number: PR_NUMBER, 36 | }); 37 | } catch (error) { 38 | core.setFailed("Failed to notify PR author to split large PR"); 39 | console.error(err); 40 | } 41 | } 42 | 43 | module.exports = async ({github, context, core}) => { 44 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 45 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 46 | } 47 | 48 | if (PR_ACTION != "labeled") { 49 | return core.notice("Only run on PRs labeling actions; skipping") 50 | } 51 | 52 | 53 | /** @type {string[]} */ 54 | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ 55 | owner: context.repo.owner, 56 | repo: context.repo.repo, 57 | issue_number: PR_NUMBER, 58 | }) 59 | 60 | // Schema: https://docs.github.com/en/rest/issues/labels#list-labels-for-an-issue 61 | for (const label of labels) { 62 | core.info(`Label: ${label}`) 63 | if (label.name == "size/XXL") { 64 | await notifyAuthor({ 65 | core: core, 66 | gh_client: github, 67 | owner: context.repo.owner, 68 | repository: context.repo.repo, 69 | }) 70 | break; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /unicorn_approvals/infrastructure/subscriptions/unicorn-web-subscriptions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: > 5 | Establishes cross-service event flow by creating an EventBridge rule that facilitates 6 | bus-to-bus communication (subscription rule), routing PublicationApprovalRequested events from the Unicorn Web 7 | event bus to the Unicorn Approvals event bus for downstream processing. 8 | 9 | Parameters: 10 | Stage: 11 | Type: String 12 | Default: local 13 | AllowedValues: 14 | - local 15 | - dev 16 | - prod 17 | 18 | Resources: 19 | #### UNICORN WEB EVENT SUBSCRIPTIONS 20 | 21 | # EventBridge rule that subscribes to PublicationApprovalRequested events from the Unicorn Web service 22 | # and forwards them to the Unicorn Approvals event bus for processing approval workflows 23 | PublicationApprovalRequestedSubscriptionRule: 24 | Type: AWS::Events::Rule 25 | Properties: 26 | Name: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}-PublicationApprovalRequested" 27 | Description: Subscription rule for the PublicationApprovalRequested events from the Unicorn 28 | Web service and forwards them to the Unicorn Approvals event bus, triggering the Approval workflow. 29 | EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/WebEventBusArn}}" 30 | EventPattern: 31 | source: 32 | - "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}" 33 | detail-type: 34 | - PublicationApprovalRequested 35 | Targets: 36 | - Id: SendEventTo 37 | Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ApprovalsEventBusArn}}" 38 | RoleArn: 39 | Fn::ImportValue: 40 | Fn::Sub: "uni-prop-${Stage}-approvals-domain-EventBridgeRoleArn" 41 | Tags: 42 | - Key: rule-owner-service-namespace 43 | Value: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}" 44 | 45 | Outputs: 46 | PublicationApprovalRequestedSubscription: 47 | Description: Rule ARN for Web service event subscription 48 | Value: 49 | Fn::GetAtt: [PublicationApprovalRequestedSubscriptionRule, Arn] 50 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/dbb_stream_events/sfn_check_exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "1dca9e1e9538380e4ef763e3d18bc2e4", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661312354.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "24/08/2022 03:37:28" }, 14 | "contract_id": { "S": "418b32e7-87c4-40d2-a551-4d7a56830905" }, 15 | "contract_status": { "S": "DRAFT" }, 16 | "property_id": { "S": "usa/anytown/main-street/999" }, 17 | "sfn_contract_exists_task_token": { 18 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAASxHOzYwUsA8mg9FgpS7TiMpXEm01Dro6ACdTxRxl9d8dCtnuHT1deXjaAkkzwh2ytaUpAjwWWcGgjA/pQUbzhdSBhE/oRx7ASRUNd4weldA53bOnM4TNL4y/k6nfPAsrnFiL++wyk6ERt/VSUsquBM6Mm1Kw+/9r6QqmmVkWoBfg/+KVA==sWAo4hixm0jircgkrfnKH5o4lPGXsHZz1AuALF0Lvy70waNMfwgmCz0PXejS+PcXEHLJ0eA9/IuBlhOfUQeHzv5hdMj9jh1n62k387CytHq5LPPTuqsLHizplCsapv3HYiHb8CZmeEHNVD0jBOZvUPx28v1ERDp4UkVAm1Kilxyt30ORmMZwZ7A9ucMRpMxjmK0uPUjP35yvbXU/z9R9sYAAK/e01mTnl1O5G5v7Fy/0qpGFYlMbONcl/+DWKwjysYLs33/CSuRaKQFsd5a9LTH5VrAM7lzvHNvbnb+ZeAIfMWz7clVJyo1f4kaqpHafQf+4B3TQOdDsb/ASpu2QYsw6o24PMUoaZQ67fST/tAEsAHm1h41L0YhWwpC/dWlHReUABQ3PKd+wT1VPs3wpW/PzwRddwzB6laFvN38YU5sTU7widqGY14OvN5W9vzb4iaFtdUmbfMWxinML0sRBSFUQHWmCYDnOliIR36sq4T6GkxuNVue7RWlhDbDLQu9SlM00nSxg/9dbcl4wAwxH" 19 | } 20 | }, 21 | "OldImage": { 22 | "contract_last_modified_on": { "S": "24/08/2022 03:37:28" }, 23 | "contract_id": { "S": "418b32e7-87c4-40d2-a551-4d7a56830905" }, 24 | "contract_status": { "S": "DRAFT" }, 25 | "property_id": { "S": "usa/anytown/main-street/999" } 26 | }, 27 | "SequenceNumber": "2360500000000006677219691", 28 | "SizeBytes": 1102, 29 | "StreamViewType": "NEW_AND_OLD_IMAGES" 30 | }, 31 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /unicorn_web/infrastructure/subscriptions/unicorn-approvals-subscriptions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: > 5 | Establishes cross-service event flow by creating an EventBridge rule that facilitates 6 | bus-to-bus communication (subscription rule), routing PublicationEvaluationCompleted events from the Unicorn Approvals 7 | event bus to the Unicorn Web event bus for downstream processing. 8 | 9 | Parameters: 10 | Stage: 11 | Type: String 12 | Default: local 13 | AllowedValues: 14 | - local 15 | - dev 16 | - prod 17 | 18 | Resources: 19 | #### UNICORN PROPERTIES EVENT SUBSCRIPTIONS 20 | 21 | # EventBridge rule that subscribes to PublicationEvaluationCompleted events from the Unicorn Approvals service 22 | # and forwards them to the Unicorn Web event bus for processing publication approval results 23 | PublicationEvaluationCompletedSubscriptionRule: 24 | Type: AWS::Events::Rule 25 | Properties: 26 | Name: "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}-PublicationEvaluationCompleted" 27 | Description: > 28 | Subscription rule for the PublicationEvaluationCompleted events from the Unicorn 29 | Approvals service and forwards them to the Unicorn Web event bus for processing 30 | publication approval results 31 | EventBusName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ApprovalsEventBusArn}}" 32 | EventPattern: 33 | source: 34 | - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}" 35 | detail-type: 36 | - PublicationEvaluationCompleted 37 | Targets: 38 | - Id: SendEventTo 39 | Arn: !Sub "{{resolve:ssm:/uni-prop/${Stage}/WebEventBusArn}}" 40 | RoleArn: 41 | Fn::ImportValue: 42 | Fn::Sub: "uni-prop-${Stage}-web-domain-EventBridgeRoleArn" 43 | Tags: 44 | - Key: rule-owner-service-namespace 45 | Value: "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}" 46 | 47 | Outputs: 48 | PublicationEvaluationCompletedSubscription: 49 | Description: Rule ARN for Property service event subscription 50 | Value: 51 | Fn::GetAtt: [ PublicationEvaluationCompletedSubscriptionRule, Arn ] 52 | -------------------------------------------------------------------------------- /unicorn_contracts/src/contracts_service/Contract.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | /** 5 | * Defines the structure of a contract in the database. 6 | * 7 | * @property address - The address of the contract. 8 | * @property property_id - The ID of the property associated with the contract. 9 | * @property contract_id - The ID of the contract. 10 | * @property seller_name - The name of the seller. 11 | * @property contract_status - The status of the contract. 12 | * @property contract_created - The date the contract was created. 13 | * @property contract_last_modified_on - The date the contract was last modified. 14 | */ 15 | export interface ContractDBType { 16 | address?: string; 17 | property_id: string; 18 | contract_id?: string; 19 | seller_name?: string; 20 | contract_status: ContractStatusEnum; 21 | contract_created?: string; 22 | contract_last_modified_on?: string; 23 | } 24 | 25 | /** 26 | * Enumerates the possible status values for a contract. 27 | * 28 | * @enum {string} 29 | * @property APPROVED - The contract has been approved. 30 | * @property CANCELLED - The contract has been cancelled. 31 | * @property DRAFT - The contract is in draft status. 32 | * @property CLOSED - The contract has been closed. 33 | * @property EXPIRED - The contract has expired. 34 | */ 35 | export enum ContractStatusEnum { 36 | APPROVED = 'APPROVED', 37 | CANCELLED = 'CANCELLED', 38 | DRAFT = 'DRAFT', 39 | CLOSED = 'CLOSED', 40 | EXPIRED = 'EXPIRED', 41 | } 42 | 43 | /** 44 | * Defines an interface for a contract error that extends the base Error interface. 45 | * 46 | * @interface ContractError 47 | * @extends Error 48 | * @property propertyId - The ID of the property associated with the error. 49 | * @property object - The object associated with the error (optional). 50 | */ 51 | export interface ContractError extends Error { 52 | propertyId: string; 53 | object?: any; 54 | } 55 | 56 | /** 57 | * Defines an interface for a contract response. 58 | * 59 | * @interface ContractResponse 60 | * @property propertyId - The ID of the property associated with the response. 61 | * @property metadata - Additional metadata associated with the response. 62 | */ 63 | export interface ContractResponse { 64 | propertyId: string; 65 | metadata: any; 66 | } 67 | -------------------------------------------------------------------------------- /unicorn_contracts/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Contracts 2 | 3 | ![Contracts Service Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-contracts.png) 4 | 5 | ## Architecture overview 6 | 7 | The **Unicorn Contracts** service manages contractual relationships between customers and Unicorn Properties agency. The service handles standard terms and conditions, property service rates, fees, and additional services. 8 | 9 | Each property can have only one active contract. Properties use their address as a unique identifier instead of a GUID, which correlates across services. 10 | 11 | For example: `usa/anytown/main-street/111`. 12 | 13 | The contract workflow operates as follows: 14 | 15 | 1. Agents submit contract creation/update commands through the Contracts API 16 | 1. The API sends requests to Amazon SQS 17 | 1. A Contracts function processes the queue messages and updates Amazon DynamoDB 18 | 1. DynamoDB Streams captures contract changes 19 | 1. Amazon EventBridge Pipes transforms the DynamoDB records into ContractStatusChanged events 20 | 1. Unicorn Approvals consumes these events to track contract changes without direct database dependencies 21 | 22 | An example of `ContractStatusChanged` event: 23 | 24 | ```json 25 | { 26 | "version": "0", 27 | "account": "123456789012", 28 | "region": "us-east-1", 29 | "detail-type": "ContractStatusChanged", 30 | "source": "unicorn-contracts", 31 | "time": "2022-08-14T22:06:31Z", 32 | "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", 33 | "resources": [], 34 | "detail": { 35 | "contract_updated_on": "10/08/2022 19:56:30", 36 | "ContractId": "617dda8c-e79b-406a-bc5b-3a4712f5e4d7", 37 | "PropertyId": "usa/anytown/main-street/111", 38 | "ContractStatus": "DRAFT" 39 | } 40 | } 41 | ``` 42 | 43 | ### Testing the APIs 44 | 45 | ```bash 46 | export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-contract --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text` 47 | 48 | curl --location --request POST "${API}contract" \ 49 | --header 'Content-Type: application/json' \ 50 | --data-raw '{ 51 | "address": { 52 | "country": "USA", 53 | "city": "Anytown", 54 | "street": "Main Street", 55 | "number": 111 56 | }, 57 | "seller_name": "John Doe", 58 | "property_id": "usa/anytown/main-street/111" 59 | }' 60 | 61 | 62 | curl --location --request PUT "${API}contract" \ 63 | --header 'Content-Type: application/json' \ 64 | --data-raw '{"property_id": "usa/anytown/main-street/111"}' | jq 65 | ``` 66 | -------------------------------------------------------------------------------- /.github/workflows/build_test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test Workflow 2 | on: 3 | push: 4 | branches: [develop] 5 | paths: 6 | - '.github/workflows/*' 7 | - 'unicorn_shared/**' 8 | - 'unicorn_contracts/**' 9 | - 'unicorn_approvals/**' 10 | - 'unicorn_web/**' 11 | env: 12 | AWS_REGION : "ap-southeast-2" 13 | 14 | permissions: 15 | id-token: write 16 | contents: read 17 | 18 | jobs: 19 | # shared-infrastructure: 20 | # runs-on: ubuntu-latest 21 | # steps: 22 | # - uses: actions/checkout@v4 23 | 24 | # - name: Configure AWS credentials 25 | # uses: aws-actions/configure-aws-credentials@v4 26 | # with: 27 | # role-to-assume: arn:aws:iam::819998446679:role/GithubActions-ServerlessDeveloperExperience 28 | # aws-region: ${{ env.AWS_REGION }} 29 | 30 | # - name: Deploy Shared Images 31 | # working-directory: unicorn_shared 32 | # run: make deploy-images 33 | 34 | # - name: Deploy Shared Namespaces 35 | # working-directory: unicorn_shared 36 | # run: aws cloudformation update-stack --stack-name uni-prop-namespaces --template-body file://uni-prop-namespaces.yaml --capabilities CAPABILITY_AUTO_EXPAND 37 | 38 | unicorn-service: 39 | # needs: shared-infrastructure 40 | runs-on: ubuntu-latest 41 | continue-on-error: true 42 | 43 | strategy: 44 | #max-parallel: 1 45 | matrix: 46 | folder: [unicorn_contracts, unicorn_web, unicorn_approvals] 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Setup NodeJS 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: 18.x 55 | 56 | - uses: pnpm/action-setup@v3 57 | with: 58 | version: 8 59 | package_json_file: ${{ matrix.folder }}/package.json 60 | 61 | - name: Install dependencies 62 | run: pnpm i 63 | working-directory: ${{ matrix.folder }} 64 | 65 | - name: Run unit tests 66 | run: make unit-test 67 | working-directory: ${{ matrix.folder }} 68 | 69 | # - name: Configure AWS credentials 70 | # uses: aws-actions/configure-aws-credentials@v4 71 | # with: 72 | # role-to-assume: arn:aws:iam::819998446679:role/GithubActions-ServerlessDeveloperExperience 73 | # aws-region: ${{ env.AWS_REGION }} 74 | 75 | - name: Configure AWS SAM 76 | uses: aws-actions/setup-sam@v2 77 | with: 78 | use-installer: true 79 | 80 | - name: Build the SAM template 81 | run: sam build 82 | working-directory: ${{ matrix.folder }} 83 | 84 | # - name: Deploy the SAM template 85 | # run: sam deploy --no-confirm-changeset 86 | # working-directory: ${{ matrix.folder }} 87 | 88 | # - name: Run integration tests 89 | # run: make integration-test 90 | # working-directory: ${{ matrix.folder }} 91 | -------------------------------------------------------------------------------- /unicorn_web/data/property_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "PK": "PROPERTY#usa#anytown", 4 | "SK": "main-street#111", 5 | "country": "USA", 6 | "city": "Anytown", 7 | "street": "Main Street", 8 | "number": 111, 9 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 10 | "contract": "sale", 11 | "listprice": 200, 12 | "currency": "SPL", 13 | "images": [ 14 | "property_images/prop1_exterior1.jpg", 15 | "property_images/prop1_interior1.jpg", 16 | "property_images/prop1_interior2.jpg", 17 | "property_images/prop1_interior3.jpg" 18 | ], 19 | "status": "PENDING" 20 | }, 21 | { 22 | "PK": "PROPERTY#usa#main-town", 23 | "SK": "my-street#222", 24 | "country": "USA", 25 | "city": "Main Town", 26 | "street": "My Street", 27 | "number": 222, 28 | "description": "This picturesque Main Town property is ideally suited for the DIY enthusiast unicorns. The antique art-nouveau pillars at the entrance offer a reminder of historic times. This property comes uniquely suited with no electricity, offering a tranquil environment with no digital disruptions. The rooftop has only minor fire damages from fireworks incidents during the 1970s.", 29 | "contract": "sale", 30 | "listprice": 150, 31 | "currency": "SPL", 32 | "images": [ 33 | "property_images/prop2_exterior1.jpg", 34 | "property_images/prop2_exterior2.jpg", 35 | "property_images/prop2_interior1.jpg", 36 | "property_images/prop2_interior2.jpg" 37 | ], 38 | "status": "PENDING" 39 | }, 40 | { 41 | "PK": "PROPERTY#usa#anytown", 42 | "SK": "main-street#333", 43 | "country": "USA", 44 | "city": "Anytown", 45 | "street": "Main Street", 46 | "number": 333, 47 | "description": "State of the art mansion featuring the most luxurious facilities, not only of Anytown but the whole Anytown region. The garden is equipped with three separate landing pads; one for the owner, one for guests and one for employees of the owner. The estate has its own satellite dish receiver and is equipped with double fiber connections for high-speed internet. The house has both an indoor and an outdoor swimming pool with automated pool cleaning facilities.", 48 | "contract": "sale", 49 | "listprice": 250, 50 | "currency": "SPL", 51 | "images": [ 52 | "property_images/prop3_exterior1.jpg", 53 | "property_images/prop3_interior1.jpg", 54 | "property_images/prop3_interior2.jpg", 55 | "property_images/prop3_interior3.jpg" 56 | ], 57 | "status": "PENDING" 58 | } 59 | ] -------------------------------------------------------------------------------- /unicorn_approvals/tests/integration/contract_status_changed_event.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'; 4 | import { 5 | sleep, 6 | findOutputValue, 7 | clearDatabase, 8 | initializeDatabase, 9 | } from './helper'; 10 | import { 11 | EventBridgeClient, 12 | PutEventsCommand, 13 | } from '@aws-sdk/client-eventbridge'; 14 | 15 | import ContractStatusChangedDraftEvent from '../events/eventbridge/contract_status_changed_event_contract_1_draft.json'; 16 | import ContractApprovedEvent from '../events/eventbridge/contract_status_changed_event_contract_2_approved.json'; 17 | 18 | describe('Testing draft contract event handling', () => { 19 | beforeAll(async () => { 20 | await clearDatabase(); 21 | await initializeDatabase(); 22 | }, 30000); 23 | 24 | afterAll(async () => { 25 | await clearDatabase(); 26 | }, 30000); 27 | 28 | it('Should create a draft contract', async () => { 29 | // Arrange 30 | const ddb = new DynamoDBClient({ 31 | region: process.env.AWS_DEFAULT_REGION, 32 | }); 33 | const evb = new EventBridgeClient({ 34 | region: process.env.AWS_DEFAULT_REGION, 35 | }); 36 | const contractStatusTableName = await findOutputValue( 37 | 'uni-prop-local-approvals', 38 | 'ContractStatusTableName' 39 | ); 40 | 41 | // Act 42 | await evb.send( 43 | new PutEventsCommand({ Entries: ContractStatusChangedDraftEvent }) 44 | ); 45 | await sleep(10000); 46 | // Assert 47 | const getItemCommand = new GetItemCommand({ 48 | TableName: contractStatusTableName, 49 | Key: { property_id: { S: 'usa/anytown/main-street/111' } }, 50 | }); 51 | const ddbResp = await ddb.send(getItemCommand); 52 | expect(ddbResp?.Item).toBeTruthy(); 53 | if (!ddbResp?.Item) throw Error('Contract not found'); 54 | expect(ddbResp.Item.contract_status?.S).toBe('DRAFT'); 55 | expect(ddbResp.Item.sfn_wait_approved_task_token).toBe(undefined); 56 | }, 30000); 57 | 58 | it('Should update an existing contract status to APPROVED', async () => { 59 | // Arrange 60 | const ddb = new DynamoDBClient({ 61 | region: process.env.AWS_DEFAULT_REGION, 62 | }); 63 | const evb = new EventBridgeClient({ 64 | region: process.env.AWS_DEFAULT_REGION, 65 | }); 66 | const contractStatusTableName = await findOutputValue( 67 | 'uni-prop-local-approvals', 68 | 'ContractStatusTableName' 69 | ); 70 | 71 | // Act 72 | await evb.send(new PutEventsCommand({ Entries: ContractApprovedEvent })); 73 | await sleep(5000); 74 | 75 | // Assert 76 | const getItemCommand = new GetItemCommand({ 77 | TableName: contractStatusTableName, 78 | Key: { property_id: { S: 'usa/anytown/main-street/222' } }, 79 | }); 80 | const ddbResp = await ddb.send(getItemCommand); 81 | expect(ddbResp?.Item).toBeTruthy(); 82 | if (!ddbResp.Item) throw new Error('Contract not found'); 83 | expect(ddbResp.Item.contract_status?.S).toBe('APPROVED'); 84 | }, 30000); 85 | }); 86 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/dbb_stream_events/status_approved_waiting_for_approval.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "ce16edd66e3812941e0d721e877c752a", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661391844, 11 | "Keys": { 12 | "property_id": { 13 | "S": "usa/anytown/main-street/111" 14 | } 15 | }, 16 | "NewImage": { 17 | "contract_last_modified_on": { 18 | "S": "25/08/2022 01:44:02" 19 | }, 20 | "contract_id": { 21 | "S": "ef3f01a5-79ec-411a-b59a-7dbb19208ead" 22 | }, 23 | "contract_status": { 24 | "S": "APPROVED" 25 | }, 26 | "property_id": { 27 | "S": "usa/anytown/main-street/111" 28 | } 29 | }, 30 | "OldImage": { 31 | "contract_last_modified_on": { 32 | "S": "24/08/2022 15:53:26" 33 | }, 34 | "sfn_wait_approved_task_token": { 35 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAapnwdMR3Z7RAg3IavSq2hbHt+CZPIQYaakFO6Em9Ik00VsGcaznxotIUGB2t7kihvuu/ffeoF+Z4yg4dggTxtzVwvRJwUDQr33/s/LhJyvEfNS57PXCv/CYFssJZ+28FRCAYbGekKaopYhaUlvq0taLGMaEfIbRBeLUmLHInDkPPDbwTA==N0LRrCP3bIFW1MPkMQC3kd35p8yflvpThHCeviqe3qeyKBX03+ziocuyvNHVpktMuECnHL3MN9a6BfpSM1KItYI+qdIC74ls83ALSjjOs8G8pOz4OudcliLYAvmZPRQXvFaw6aSMLfJJ7xRpEFSBwj1zDzbadMLtfudG78pmZX1m75/idMU5gz0UPkC87bVQN6Kyjl7obxAeUO4aoqGJeNz6WbJQtrsUhiQWVEH/AwjUAj9Q0DwRqRPqeWyrv4MIMKea/Xu9vhXbcS+zPWPq8onN9fyAqoMNh64K6wSGWxtAbaaByKxNpQu7o9ho/Iu/ME0KOAqyUK6vcnXOpIwIoMAZiG34KF4UnQsD0gIokQcGLbehMGRixvEJMDZIloLxkuH0jvpIvD5xokGxpHwiVMISi2XRJ92nnGmWTCLWqsqJlsg4We6snJp0Akw2w1Nt41lgY8kWkjxHNWEHDIMjzx1zeWiVa9b9aDDcckb71ouknJCN4gbxVs+yP30M97qnCEmMrc25Yq7cEXLhu5Dh" 36 | }, 37 | "contract_id": { 38 | "S": "ef3f01a5-79ec-411a-b59a-7dbb19208ead" 39 | }, 40 | "contract_status": { 41 | "S": "DRAFT" 42 | }, 43 | "property_id": { 44 | "S": "usa/anytown/main-street/111" 45 | }, 46 | "sfn_contract_exists_task_token": { 47 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAfL0WOHwLPeUsqktBZSKrsN4dJqIHsLoClU1UUkV+fstVYTXYkMNsmLNyfmrWPVOpQrtK0JKSw+SmuA0qq5qIx/bwz10JfWY7EUB8BHSW6AeYPdzOwHUDqSLs+OzCBl4HhqbeLWCadXOrXRylY5N6P0QOmjLUN/08v+8Ioz2DvKqa3pUbQ==InWjQiaSHWHtJgpe+1IxIipKGyk0vg+LqvlIKRiJENbE1Hz2zZK3aROztTCnHISR2rYBjTDihcn3S6QTqRrlhLvlkxBrxIIzz8UB4v4PaPLklUFPV8IEF0JpT4uhpRST2QV7RuHBZWgQOcMRpM+0yvHZ5Pq4YNzJHKpkvEW34Kv4wsduWbYG3Wl6Yej5TvAHNpoqi3PS+Z2/cih/EDQmTiznmwLPptnA0wc1RrhShT7Izm2hR/lINW9LcY9346oyxEK2riWm9v9kxMB8vg926nqdIKwwDNCsMrpQGgOsIfW+ECQNPTefuPWIwZ4ycvzWeEP6mdhZ+pusD4+XUDIZ5Jjok74YIxqHE2nSck+kLyqpXxcxBc6CRX+SUSCUuWyWEpjYx43qAfTRgLhm1r+DlI5sg1ARHe0oVUoeuvV7jX2b9AHarMEiPUxvTGqn/lRM9c8x55TskXxF9EE7AV0hCzCC+DiT3nj70LZ/7iSJZpyORH3B3KEEJQA/7BtE4LYPVWmIhbVjXr0mf67zVl6Y" 48 | } 49 | }, 50 | "SequenceNumber": "6547000000000005631781392", 51 | "SizeBytes": 1869, 52 | "StreamViewType": "NEW_AND_OLD_IMAGES" 53 | }, 54 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /unicorn_approvals/tests/events/dbb_stream_events/sfn_wait_approval.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "eb388b2e39dc9da93600f611374f588b", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661345617.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "24/08/2022 08:09:11" }, 14 | "contract_id": { "S": "3a574672-a87f-4150-8286-506cfeea4913" }, 15 | "contract_status": { "S": "DRAFT" }, 16 | "sfn_verify_approved_task_token": { 17 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAATHiOzW3P9kS+UUCeOLc19jKIJ1NMlo4NoiGqkf5yt+P6Wv6DNHN71Sxsq0MSHeOfmjJEKHgVOgs55fI/cgvjAeesz/iWJvOrv7va2bHQuRMgSFT1J8HfMsA8O4TRTyv/yt14N+w0rSBuJLgJjOil23JGXjoh/989KvkDwwIIRVJJ6uHEw==/hCsUZVkaUn3WfnF4amPTo/9lWL3cG3HiDDFQgMH3owU0HbDV3vd3IlxMK0z73Y9D9oojAfwO1GrxPfPq/THtQvX15dlIOvEyPyiKwNo0y2kxuwoJDrwWpY/4NNbNEwOGIagivrQG2GURD89jLtf7FQUrryDIMadvx3bpfXIzRj5hRjArTR7sEhOWfiV1JPT5ReJvkxi++W3I9zAY+cdh4DYtnfZd2wXP5Di1/VBgz+TOviQEeeZFeg8tEM3S/xDpiRdSWCSiKFeY2I6jFgFzywhrCrDMWkt08eB1aZhxQuvMDbt4+3vBeIYoYhozOPI0Q8JAfOhIJ3EewNRFXbnrhGPDoctnE2tMQ5y8n8xTrp/DGRSEZ3IDRyxI6w1j8oyCsbqaEAB0d8jpXOaM4yuxaijbzU8czw6JjA6c+N2DIl3eHsl5OUcat/+298HdoFUo9BiICbEowAZePNh2uu/8CkAqam3h68JydNW6nZA3fo6agSBL9IFe4xGFU79Lu8AMPhIehKWuWPVeRsnqaUx" 18 | }, 19 | "property_id": { "S": "usa/anytown/main-street/999" }, 20 | "sfn_contract_exists_task_token": { 21 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAVS9BUaG0YfI6XbiFBynBEemcfQshPfHZqpHkUSL5AI0H54qZ95l2VRQ17BIZuhBNSfJyNLryGJFYq3Ro80CJcn5NePgWmpZt6Tx0SxZOtTaqU9Ozksy3p56Kg0uMABO1vmm5WJj7mnaaFmGwCl10+40Mrnmhqh6Q49BqcTZ1Ud92+NrPA==Z7mKaWIDnW9z8IviYx1YsRNkwpmKHwECnyFLgODmLEnwv9HKJCdKZV27GdM/A+C7X2OYyCei4fp6VG1l4qMVwukmt6nTc7vdTBxCV32C+mGSmhOWiK/qxRTxRKukLmrf5ahlgE/JDj8vHhptOVyqXNPzh3cYDURW/WMNR6EP+k9s1vsPvgOOivwMYkVAN0+Xpu4HV9gdSw0OqDAHScZUjVfvXxPVSnUrrvEpeUUflAUafO2Z+mdzCl173qTzd0povZXdkTKyMmdlILI6sZhuahnkuDcGD6Pnoq7XL/i3bDDGR90LCqxWnTorkec0HBNVct4/RltQnvRY0zASG6N9LDpXOy0qLY9ypgbWU4vEnTxOP0j3QYJjA8wO2wih45T3JRKGZ4N6U22WOEUwhePCyvr6oDBDel0cOS7nZE1fP3gn7k42ssnxIBGGzIsXp39HbnDkp8b4a4MNHmZ6tPcySyx9A5bokRG3XAaZfHZgqcUTSyCRCcbOxANkPxSvJADVHC1by8mp/j6tw202kXBD" 22 | } 23 | }, 24 | "OldImage": { 25 | "contract_last_modified_on": { "S": "24/08/2022 08:09:11" }, 26 | "contract_id": { "S": "3a574672-a87f-4150-8286-506cfeea4913" }, 27 | "contract_status": { "S": "DRAFT" }, 28 | "property_id": { "S": "usa/anytown/main-street/999" }, 29 | "sfn_contract_exists_task_token": { 30 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAVS9BUaG0YfI6XbiFBynBEemcfQshPfHZqpHkUSL5AI0H54qZ95l2VRQ17BIZuhBNSfJyNLryGJFYq3Ro80CJcn5NePgWmpZt6Tx0SxZOtTaqU9Ozksy3p56Kg0uMABO1vmm5WJj7mnaaFmGwCl10+40Mrnmhqh6Q49BqcTZ1Ud92+NrPA==Z7mKaWIDnW9z8IviYx1YsRNkwpmKHwECnyFLgODmLEnwv9HKJCdKZV27GdM/A+C7X2OYyCei4fp6VG1l4qMVwukmt6nTc7vdTBxCV32C+mGSmhOWiK/qxRTxRKukLmrf5ahlgE/JDj8vHhptOVyqXNPzh3cYDURW/WMNR6EP+k9s1vsPvgOOivwMYkVAN0+Xpu4HV9gdSw0OqDAHScZUjVfvXxPVSnUrrvEpeUUflAUafO2Z+mdzCl173qTzd0povZXdkTKyMmdlILI6sZhuahnkuDcGD6Pnoq7XL/i3bDDGR90LCqxWnTorkec0HBNVct4/RltQnvRY0zASG6N9LDpXOy0qLY9ypgbWU4vEnTxOP0j3QYJjA8wO2wih45T3JRKGZ4N6U22WOEUwhePCyvr6oDBDel0cOS7nZE1fP3gn7k42ssnxIBGGzIsXp39HbnDkp8b4a4MNHmZ6tPcySyx9A5bokRG3XAaZfHZgqcUTSyCRCcbOxANkPxSvJADVHC1by8mp/j6tw202kXBD" 31 | } 32 | }, 33 | "SequenceNumber": "4068300000000009582749593", 34 | "SizeBytes": 2634, 35 | "StreamViewType": "NEW_AND_OLD_IMAGES" 36 | }, 37 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /unicorn_contracts/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Unicorn Contracts Service Infrastructure 2 | 3 | # Variables 4 | STAGE ?= local 5 | REGION ?= ap-southeast-2 6 | STACK_PREFIX = uni-prop-$(STAGE) 7 | 8 | .PHONY: help build deploy deploy-domain deploy-schema deploy-service test unit-test integration-test lint clean delete 9 | 10 | help: ## Show available commands 11 | @echo "Unicorn Contracts Service Infrastructure" 12 | @echo "" 13 | @echo "Usage: make [target] [STAGE=local|dev|prod] [REGION=ap-southeast-2]" 14 | @echo "" 15 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}' 16 | 17 | build: ## Build the application 18 | @echo "Building TypeScript application..." 19 | @npm install 20 | @sam build --template-file infrastructure/contracts-service/template.yaml 21 | 22 | deploy-domain: ## Deploy domain resources (EventBridge, Schema Registry) 23 | @echo "Deploying domain resources..." 24 | @sam deploy \ 25 | --template-file infrastructure/domain.yaml \ 26 | --stack-name $(STACK_PREFIX)-contracts-domain \ 27 | --parameter-overrides Stage=$(STAGE) \ 28 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ 29 | --region $(REGION) \ 30 | --resolve-s3 \ 31 | --no-confirm-changeset \ 32 | --no-fail-on-empty-changeset 33 | 34 | deploy-schema: ## Deploy event schema 35 | @echo "Deploying event schema..." 36 | @sam deploy \ 37 | --template-file infrastructure/schema-registry/ContractStatusChanged-schema.yaml \ 38 | --stack-name $(STACK_PREFIX)-contracts-schema-ContractStatusChanged \ 39 | --parameter-overrides Stage=$(STAGE) \ 40 | --region $(REGION) \ 41 | --resolve-s3 \ 42 | --no-confirm-changeset \ 43 | --no-fail-on-empty-changeset 44 | 45 | deploy-service: build ## Deploy service resources (Lambda, API Gateway, DynamoDB) 46 | @echo "Deploying service resources..." 47 | @sam deploy \ 48 | --template-file infrastructure/contracts-service/template.yaml \ 49 | --stack-name $(STACK_PREFIX)-contracts-contracts-service \ 50 | --parameter-overrides Stage=$(STAGE) \ 51 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ 52 | --region $(REGION) \ 53 | --resolve-s3 \ 54 | --no-confirm-changeset \ 55 | --no-fail-on-empty-changeset 56 | 57 | deploy: deploy-domain deploy-schema deploy-service ## Deploy all infrastructure and application 58 | 59 | test: ## Run tests 60 | @echo "Running tests..." 61 | @npm test 62 | 63 | unit-test: ## Run unit tests only 64 | @echo "Running unit tests..." 65 | @npm run test -- --testPathPattern=tests/unit 66 | 67 | integration-test: ## Run integration tests only 68 | @echo "Running integration tests..." 69 | @npm run test -- --testPathPattern=tests/integration 70 | 71 | lint: ## Lint CloudFormation templates 72 | @echo "Linting CloudFormation templates..." 73 | @cfn-lint infrastructure/contracts-service.yaml -a cfn_lint_serverless.rules 74 | @cfn-lint infrastructure/domain.yaml 75 | @cfn-lint infrastructure/schema-registry/ContractStatusChanged-schema.yaml 76 | 77 | clean: ## Clean build artifacts 78 | @echo "Cleaning build artifacts..." 79 | @rm -rf .aws-sam/ 80 | @rm -rf node_modules/ 81 | @rm -rf dist/ 82 | 83 | delete: ## Delete all stacks 84 | @echo "Deleting stacks in reverse order..." 85 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-contracts-contracts-service --region $(REGION) || true 86 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-contracts-schema-ContractStatusChanged --region $(REGION) || true 87 | @echo "Waiting for service and schema stacks to be deleted..." 88 | @aws cloudformation wait stack-delete-complete --stack-name $(STACK_PREFIX)-contracts-contracts-service --region $(REGION) || true 89 | @aws cloudformation wait stack-delete-complete --stack-name $(STACK_PREFIX)-contracts-schema-ContractStatusChanged --region $(REGION) || true 90 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-contracts-domain --region $(REGION) || true 91 | @echo "All stacks deleted" -------------------------------------------------------------------------------- /.github/workflows/reusable_export_pr_details.yml: -------------------------------------------------------------------------------- 1 | name: Export previously recorded PR 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | record_pr_workflow_id: 7 | description: "Record PR workflow execution ID to download PR details" 8 | required: true 9 | type: number 10 | workflow_origin: 11 | description: "Repository full name for runner integrity" 12 | required: true 13 | type: string 14 | secrets: 15 | token: 16 | description: "GitHub Actions temporary and scoped token" 17 | required: true 18 | # Map the workflow outputs to job outputs 19 | outputs: 20 | prNumber: 21 | description: "PR Number" 22 | value: ${{ jobs.export_pr_details.outputs.prNumber }} 23 | prTitle: 24 | description: "PR Title" 25 | value: ${{ jobs.export_pr_details.outputs.prTitle }} 26 | prBody: 27 | description: "PR Body as string" 28 | value: ${{ jobs.export_pr_details.outputs.prBody }} 29 | prAuthor: 30 | description: "PR author username" 31 | value: ${{ jobs.export_pr_details.outputs.prAuthor }} 32 | prAction: 33 | description: "PR event action" 34 | value: ${{ jobs.export_pr_details.outputs.prAction }} 35 | prIsMerged: 36 | description: "Whether PR is merged" 37 | value: ${{ jobs.export_pr_details.outputs.prIsMerged }} 38 | 39 | jobs: 40 | export_pr_details: 41 | 42 | if: inputs.workflow_origin == 'aws-samples/aws-serverless-developer-experience-workshop-typescript' 43 | runs-on: ubuntu-latest 44 | env: 45 | FILENAME: pr.txt 46 | # Map the job outputs to step outputs 47 | outputs: 48 | prNumber: ${{ steps.prNumber.outputs.prNumber }} 49 | prTitle: ${{ steps.prTitle.outputs.prTitle }} 50 | prBody: ${{ steps.prBody.outputs.prBody }} 51 | prAuthor: ${{ steps.prAuthor.outputs.prAuthor }} 52 | prAction: ${{ steps.prAction.outputs.prAction }} 53 | prIsMerged: ${{ steps.prIsMerged.outputs.prIsMerged }} 54 | steps: 55 | - name: Checkout repository # in case caller workflow doesn't checkout thus failing with file not found 56 | uses: actions/checkout@v4 57 | - name: "Download previously saved PR" 58 | uses: actions/github-script@v7 59 | env: 60 | WORKFLOW_ID: ${{ inputs.record_pr_workflow_id }} 61 | # For security, we only download artifacts tied to the successful PR recording workflow 62 | with: 63 | github-token: ${{ secrets.token }} 64 | script: | 65 | const script = require('.github/scripts/download_pr_artifact.js') 66 | await script({github, context, core}) 67 | # NodeJS standard library doesn't provide ZIP capabilities; use system `unzip` command instead 68 | - name: "Unzip PR artifact" 69 | run: unzip pr.zip 70 | # NOTE: We need separate steps for each mapped output and respective IDs 71 | # otherwise the parent caller won't see them regardless on how outputs are set. 72 | - name: "Export Pull Request Number" 73 | id: prNumber 74 | run: echo prNumber=$(jq -c '.number' ${FILENAME}) >> "$GITHUB_OUTPUT" 75 | - name: "Export Pull Request Title" 76 | id: prTitle 77 | run: echo prTitle=$(jq -c '.pull_request.title' ${FILENAME}) >> "$GITHUB_OUTPUT" 78 | - name: "Export Pull Request Body" 79 | id: prBody 80 | run: echo prBody=$(jq -c '.pull_request.body' ${FILENAME}) >> "$GITHUB_OUTPUT" 81 | - name: "Export Pull Request Author" 82 | id: prAuthor 83 | run: echo prAuthor=$(jq -c '.pull_request.user.login' ${FILENAME}) >> "$GITHUB_OUTPUT" 84 | - name: "Export Pull Request Action" 85 | id: prAction 86 | run: echo prAction=$(jq -c '.action' ${FILENAME}) >> "$GITHUB_OUTPUT" 87 | - name: "Export Pull Request Merged status" 88 | id: prIsMerged 89 | run: echo prIsMerged=$(jq -c '.pull_request.merged' ${FILENAME}) >> "$GITHUB_OUTPUT" 90 | -------------------------------------------------------------------------------- /unicorn_approvals/infrastructure/schema-registry/PublicationEvaluationCompleted-schema.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: > 5 | EventBridge schema definition for PublicationEvaluationCompleted events. 6 | 7 | Parameters: 8 | Stage: 9 | Type: String 10 | Default: local 11 | AllowedValues: 12 | - local 13 | - dev 14 | - prod 15 | 16 | Resources: 17 | 18 | # EventBridge schema definition for PublicationEvaluationCompleted events 19 | # This schema validates the structure of events published when property publication evaluations are completed 20 | # and enables type-safe event handling across consuming services. 21 | PublicationEvaluationCompleted: 22 | Type: AWS::EventSchemas::Schema 23 | Properties: 24 | Type: "OpenApi3" 25 | RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ApprovalsSchemaRegistryName}}" 26 | SchemaName: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}@PublicationEvaluationCompleted" 27 | Description: The EventBridge schema for when a property publication evaluation is completed. 28 | Content: | 29 | { 30 | "openapi": "3.0.0", 31 | "info": { 32 | "version": "1.0.0", 33 | "title": "PublicationEvaluationCompleted" 34 | }, 35 | "paths": {}, 36 | "components": { 37 | "schemas": { 38 | "AWSEvent": { 39 | "type": "object", 40 | "required": [ 41 | "detail-type", 42 | "resources", 43 | "detail", 44 | "id", 45 | "source", 46 | "time", 47 | "region", 48 | "version", 49 | "account" 50 | ], 51 | "x-amazon-events-detail-type": "PublicationEvaluationCompleted", 52 | "x-amazon-events-source": "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}", 53 | "properties": { 54 | "detail": { 55 | "$ref": "#/components/schemas/PublicationEvaluationCompleted" 56 | }, 57 | "account": { 58 | "type": "string" 59 | }, 60 | "detail-type": { 61 | "type": "string" 62 | }, 63 | "id": { 64 | "type": "string" 65 | }, 66 | "region": { 67 | "type": "string" 68 | }, 69 | "resources": { 70 | "type": "array", 71 | "items": { 72 | "type": "string" 73 | } 74 | }, 75 | "source": { 76 | "type": "string" 77 | }, 78 | "time": { 79 | "type": "string", 80 | "format": "date-time" 81 | }, 82 | "version": { 83 | "type": "string" 84 | } 85 | } 86 | }, 87 | "PublicationEvaluationCompleted": { 88 | "type": "object", 89 | "required": [ 90 | "property_id", 91 | "evaluation_result" 92 | ], 93 | "properties": { 94 | "property_id": { 95 | "type": "string" 96 | }, 97 | "evaluation_result": { 98 | "type": "string" 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /unicorn_web/tests/integration/search.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { clearDatabase, findOutputValue } from './helper'; 4 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 5 | import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; 6 | import ApprovedProperty from '../../data/approved_property.json'; 7 | 8 | describe('Testing approved property listing searches', () => { 9 | let apiUrl: string; 10 | 11 | beforeAll(async () => { 12 | // Clear db 13 | await clearDatabase(); 14 | // Create an approved property listing 15 | const tableName = await findOutputValue( 16 | 'uni-prop-local-web', 17 | 'PropertiesTableName' 18 | ); 19 | const docClient = DynamoDBDocumentClient.from( 20 | new DynamoDBClient({ region: process.env.AWS_DEFAULT_REGION }) 21 | ); 22 | await docClient.send( 23 | new PutCommand({ 24 | TableName: tableName, 25 | Item: ApprovedProperty, 26 | }) 27 | ); 28 | // Find API Endpoint 29 | apiUrl = await findOutputValue('uni-prop-local-web', 'UnicornWebApiUrl'); 30 | }, 10000); 31 | 32 | afterAll(async () => { 33 | await clearDatabase(); 34 | }); 35 | 36 | it('Should show the approved property listing in search by city', async () => { 37 | const response = await fetch(`${apiUrl}search/au/anytown`, { 38 | method: 'GET', 39 | }); 40 | const json = await response.json(); 41 | expect(json).toEqual([ 42 | { 43 | city: 'Anytown', 44 | contract: 'sale', 45 | country: 'AU', 46 | currency: 'SPL', 47 | listprice: 200, 48 | number: 1337, 49 | description: 50 | 'This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.', 51 | status: 'APPROVED', 52 | street: 'Main Street', 53 | }, 54 | ]); 55 | }); 56 | 57 | it('Should show the approved property listing in search by street', async () => { 58 | const response = await fetch(`${apiUrl}search/au/anytown/main-street`, { 59 | method: 'GET', 60 | }); 61 | const json = await response.json(); 62 | expect(json).toEqual([ 63 | { 64 | city: 'Anytown', 65 | contract: 'sale', 66 | country: 'AU', 67 | currency: 'SPL', 68 | listprice: 200, 69 | number: 1337, 70 | description: 71 | 'This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.', 72 | status: 'APPROVED', 73 | street: 'Main Street', 74 | }, 75 | ]); 76 | }); 77 | 78 | it('Should show the approved property listing in search by address', async () => { 79 | const response = await fetch( 80 | `${apiUrl}properties/au/anytown/main-street/1337`, 81 | { 82 | method: 'GET', 83 | } 84 | ); 85 | const json = await response.json(); 86 | expect(json).toEqual({ 87 | city: 'Anytown', 88 | contract: 'sale', 89 | country: 'AU', 90 | currency: 'SPL', 91 | listprice: 200, 92 | number: 1337, 93 | description: 94 | 'This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.', 95 | status: 'APPROVED', 96 | street: 'Main Street', 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /unicorn_contracts/infrastructure/schema-registry/ContractStatusChanged-schema.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: > 5 | EventBridge schema definition for ContractStatusChanged events. 6 | 7 | Parameters: 8 | Stage: 9 | Type: String 10 | Default: local 11 | AllowedValues: 12 | - local 13 | - dev 14 | - prod 15 | 16 | Resources: 17 | 18 | # This schema defines the structure for ContractStatusChanged events that are published 19 | # to EventBridge when contract status updates occur in the Unicorn Contracts Service. 20 | # Other services can use this schema to validate incoming events and ensure proper 21 | # event handling across the Unicorn Properties application. 22 | ContractStatusChangedEventSchema: 23 | Type: AWS::EventSchemas::Schema 24 | Properties: 25 | Type: "OpenApi3" 26 | RegistryName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ContractsSchemaRegistryName}}" 27 | SchemaName: "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}@ContractStatusChanged" 28 | Description: > 29 | EventBridge schema for ContractStatusChanged events published by the Unicorn Contracts Service 30 | when a contract is updated. 31 | Content: | 32 | { 33 | "openapi": "3.0.0", 34 | "info": { 35 | "version": "1.0.0", 36 | "title": "ContractStatusChanged" 37 | }, 38 | "paths": {}, 39 | "components": { 40 | "schemas": { 41 | "AWSEvent": { 42 | "type": "object", 43 | "required": [ 44 | "detail-type", 45 | "resources", 46 | "detail", 47 | "id", 48 | "source", 49 | "time", 50 | "region", 51 | "version", 52 | "account" 53 | ], 54 | "x-amazon-events-detail-type": "ContractStatusChanged", 55 | "x-amazon-events-source": "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}", 56 | "properties": { 57 | "detail": { 58 | "$ref": "#/components/schemas/ContractStatusChanged" 59 | }, 60 | "account": { 61 | "type": "string" 62 | }, 63 | "detail-type": { 64 | "type": "string" 65 | }, 66 | "id": { 67 | "type": "string" 68 | }, 69 | "region": { 70 | "type": "string" 71 | }, 72 | "resources": { 73 | "type": "array", 74 | "items": { 75 | "type": "object" 76 | } 77 | }, 78 | "source": { 79 | "type": "string" 80 | }, 81 | "time": { 82 | "type": "string", 83 | "format": "date-time" 84 | }, 85 | "version": { 86 | "type": "string" 87 | } 88 | } 89 | }, 90 | "ContractStatusChanged": { 91 | "type": "object", 92 | "required": [ 93 | "contract_last_modified_on", 94 | "contract_id", 95 | "contract_status", 96 | "property_id" 97 | ], 98 | "properties": { 99 | "contract_id": { 100 | "type": "string" 101 | }, 102 | "contract_last_modified_on": { 103 | "type": "string", 104 | "format": "date-time" 105 | }, 106 | "contract_status": { 107 | "type": "string" 108 | }, 109 | "property_id": { 110 | "type": "string" 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /unicorn_approvals/src/approvals_service/contractStatusChangedEventHandler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { EventBridgeEvent, Context } from 'aws-lambda'; 4 | import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; 5 | import { MetricUnit } from '@aws-lambda-powertools/metrics'; 6 | import { logger, metrics, tracer } from './powertools'; 7 | import { 8 | DynamoDBClient, 9 | UpdateItemCommand, 10 | UpdateItemCommandInput, 11 | UpdateItemCommandOutput, 12 | } from '@aws-sdk/client-dynamodb'; 13 | import { ContractStatusChanged } from '../schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged'; 14 | import { Marshaller } from '../schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller'; 15 | 16 | // Empty configuration for DynamoDB 17 | const ddbClient = new DynamoDBClient({}); 18 | const DDB_TABLE = process.env.CONTRACT_STATUS_TABLE ?? 'ContractStatusTable'; 19 | 20 | export interface ContractStatusError extends Error { 21 | contract_id: string; 22 | name: string; 23 | object: any; 24 | } 25 | 26 | class ContractStatusChangedFunction implements LambdaInterface { 27 | /** 28 | * Handle the contract status changed event from the EventBridge instance. 29 | * @param {Object} event - EventBridge Event Input Format 30 | * @returns {void} 31 | * 32 | */ 33 | @tracer.captureLambdaHandler() 34 | @metrics.logMetrics({ captureColdStartMetric: true }) 35 | @logger.injectLambdaContext({ logEvent: true }) 36 | public async handler( 37 | event: EventBridgeEvent, 38 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 39 | context: Context 40 | ): Promise { 41 | logger.info(`Contract status changed: ${JSON.stringify(event.detail)}`); 42 | try { 43 | // Construct the entry to insert into database. 44 | const statusEntry: ContractStatusChanged = Marshaller.unmarshal( 45 | event.detail, 46 | 'ContractStatusChanged' 47 | ); 48 | tracer.putAnnotation('ContractStatus', statusEntry.contractStatus); 49 | logger.info(`Unmarshalled entry: ${JSON.stringify(statusEntry)}`); 50 | 51 | // Call saveContractStatus with the entry 52 | await this.saveContractStatus(statusEntry); 53 | } catch (error: any) { 54 | tracer.addErrorAsMetadata(error as Error); 55 | logger.error(`Error during DDB UPDATE: ${JSON.stringify(error)}`); 56 | } 57 | metrics.addMetric('ContractStatusChanged', MetricUnit.Count, 1); 58 | } 59 | 60 | /** 61 | * Update the ContractStatus entry in the database 62 | * @param statusEntry 63 | */ 64 | @tracer.captureMethod() 65 | private async saveContractStatus(statusEntry: ContractStatusChanged) { 66 | logger.info( 67 | `Updating status: ${statusEntry.contractStatus} for ${statusEntry.propertyId}` 68 | ); 69 | const ddbUpdateCommandInput: UpdateItemCommandInput = { 70 | TableName: DDB_TABLE, 71 | Key: { property_id: { S: statusEntry.propertyId } }, 72 | UpdateExpression: 73 | 'set contract_id = :c, contract_status = :t, contract_last_modified_on = :m', 74 | ExpressionAttributeValues: { 75 | ':c': { S: statusEntry.contractId as string }, 76 | ':t': { S: statusEntry.contractStatus as string }, 77 | ':m': { S: statusEntry.contractLastModifiedOn }, 78 | }, 79 | }; 80 | logger.info(`Constructed command ${JSON.stringify(ddbUpdateCommandInput)}`); 81 | const ddbUpdateCommand = new UpdateItemCommand(ddbUpdateCommandInput); 82 | 83 | // Send the command 84 | const ddbUpdateCommandOutput: UpdateItemCommandOutput = 85 | await ddbClient.send(ddbUpdateCommand); 86 | logger.info( 87 | `Updated status: ${statusEntry.contractStatus} for ${statusEntry.propertyId}` 88 | ); 89 | if (ddbUpdateCommandOutput.$metadata.httpStatusCode != 200) { 90 | const error: ContractStatusError = { 91 | contract_id: statusEntry.contractId, 92 | name: 'ContractStatusDBUpdateError', 93 | message: 94 | 'Response error code: ' + 95 | ddbUpdateCommandOutput.$metadata.httpStatusCode, 96 | object: ddbUpdateCommandOutput.$metadata, 97 | }; 98 | throw error; 99 | } 100 | } 101 | } 102 | 103 | const myFunction = new ContractStatusChangedFunction(); 104 | export const lambdaHandler = async ( 105 | event: EventBridgeEvent, 106 | context: Context 107 | ): Promise => { 108 | return myFunction.handler(event, context); 109 | }; 110 | -------------------------------------------------------------------------------- /unicorn_shared/uni-prop-images.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | Base infrastructure that will set up the central event bus and S3 image upload bucket per stage. 5 | 6 | Metadata: 7 | cfn-lint: 8 | config: 9 | ignore_checks: 10 | - ES6000 11 | - WS1004 12 | 13 | Parameters: 14 | Stage: 15 | Type: String 16 | Default: local 17 | AllowedValues: 18 | - local 19 | - dev 20 | - prod 21 | 22 | Globals: 23 | Function: 24 | Timeout: 15 25 | Runtime: python3.12 26 | MemorySize: 512 27 | Tracing: Active 28 | Architectures: 29 | - arm64 30 | Tags: 31 | stage: !Ref Stage 32 | project: AWS Serverless Developer Experience 33 | service: Unicorn Base Infrastructure 34 | 35 | Resources: 36 | 37 | #### SSM PARAMETERS 38 | 39 | 40 | UnicornPropertiesImagesBucketParam: 41 | Type: AWS::SSM::Parameter 42 | Properties: 43 | Type: String 44 | Name: !Sub /uni-prop/${Stage}/ImagesBucket 45 | Value: !Ref UnicornPropertiesImagesBucket 46 | 47 | #### S3 PROPERTY IMAGES BUCKET 48 | UnicornPropertiesImagesBucket: 49 | Type: AWS::S3::Bucket 50 | Properties: 51 | BucketName: !Sub "uni-prop-${Stage}-images-${AWS::AccountId}-${AWS::Region}" 52 | 53 | #### IMAGE UPLOAD CUSTOM RESOURCE FUNCTION 54 | ImageUploadFunction: 55 | Type: AWS::Serverless::Function 56 | Properties: 57 | Handler: index.lambda_handler 58 | Runtime: python3.13 59 | Policies: 60 | - S3CrudPolicy: 61 | BucketName: !Ref UnicornPropertiesImagesBucket 62 | - Statement: 63 | - Sid: S3DeleteBucketPolicy 64 | Effect: Allow 65 | Action: 66 | - s3:DeleteBucket 67 | Resource: !GetAtt UnicornPropertiesImagesBucket.Arn 68 | InlineCode: | 69 | import os 70 | import zipfile 71 | from urllib.request import urlopen 72 | import boto3 73 | import cfnresponse 74 | 75 | zip_file_name = 'property_images.zip' 76 | url = f"https://aws-serverless-developer-experience-workshop-assets.s3.amazonaws.com/property_images/{zip_file_name}" 77 | temp_zip_download_location = f"/tmp/{zip_file_name}" 78 | 79 | s3 = boto3.resource('s3') 80 | 81 | def create(event, context): 82 | image_bucket_name = event['ResourceProperties']['DestinationBucket'] 83 | bucket = s3.Bucket(image_bucket_name) 84 | print(f"downloading zip file from: {url} to: {temp_zip_download_location}") 85 | r = urlopen(url).read() 86 | with open(temp_zip_download_location, 'wb') as t: 87 | t.write(r) 88 | print('zip file downloaded') 89 | 90 | print(f"unzipping file: {temp_zip_download_location}") 91 | with zipfile.ZipFile(temp_zip_download_location,'r') as zip_ref: 92 | zip_ref.extractall('/tmp') 93 | 94 | print('file unzipped') 95 | 96 | #### upload to s3 97 | for root,_,files in os.walk('/tmp/property_images'): 98 | for file in files: 99 | print(f"file: {os.path.join(root, file)}") 100 | print(f"s3 bucket: {image_bucket_name}") 101 | bucket.upload_file(os.path.join(root, file), f"property_images/{file}") 102 | def delete(event, context): 103 | image_bucket_name = event['ResourceProperties']['DestinationBucket'] 104 | img_bucket = s3.Bucket(image_bucket_name) 105 | img_bucket.objects.delete() 106 | img_bucket.delete() 107 | def lambda_handler(event, context): 108 | try: 109 | if event['RequestType'] in ['Create', 'Update']: 110 | create(event, context) 111 | elif event['RequestType'] in ['Delete']: 112 | delete(event, context) 113 | except Exception as e: 114 | print(e) 115 | cfnresponse.send(event, context, cfnresponse.SUCCESS, dict()) 116 | ImageUploadFunctionLogGroup: 117 | Type: AWS::Logs::LogGroup 118 | DeletionPolicy: Delete 119 | UpdateReplacePolicy: Delete 120 | Properties: 121 | LogGroupName: !Sub "/aws/lambda/${ImageUploadFunction}" 122 | 123 | ImageUpload: 124 | Type: Custom::ImageUpload 125 | Properties: 126 | ServiceToken: !GetAtt ImageUploadFunction.Arn 127 | DestinationBucket: !Ref UnicornPropertiesImagesBucket 128 | 129 | Outputs: 130 | ImageUploadBucketName: 131 | Value: !Ref UnicornPropertiesImagesBucket 132 | Description: "S3 bucket for property images" -------------------------------------------------------------------------------- /unicorn_approvals/state_machine/property_approval.asl.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | Comment: The property approval workflow ensures that its images and content is 4 | safe to publish and that there is an approved contract in place before the 5 | listing is made available to the public through the Unicorn Properties 6 | website. 7 | QueryLanguage: JSONata 8 | StartAt: LookupContract 9 | States: 10 | LookupContract: 11 | Type: Task 12 | Resource: arn:aws:states:::dynamodb:getItem 13 | Arguments: 14 | TableName: ${TableName} 15 | Key: 16 | property_id: 17 | S: "{% $states.input.detail.property_id %}" 18 | ProjectionExpression: contract_id, property_id, contract_status 19 | Next: VerifyContractExists 20 | Catch: 21 | - ErrorEquals: 22 | - States.ALL 23 | Next: NotFound 24 | VerifyContractExists: 25 | Type: Choice 26 | Choices: 27 | - Comment: Contract Exists 28 | Condition: "{% $exists($states.input.Item) %}" 29 | Next: Parallel 30 | Default: NotFound 31 | Comment: No Contract Found 32 | Parallel: 33 | Type: Parallel 34 | Next: IsContentSafe 35 | Branches: 36 | - StartAt: CheckDescriptionSentiment 37 | States: 38 | CheckDescriptionSentiment: 39 | Type: Task 40 | Resource: arn:aws:states:::aws-sdk:comprehend:detectSentiment 41 | Arguments: 42 | LanguageCode: en 43 | Text: "{% $states.context.Execution.Input.detail.description %}" 44 | End: true 45 | - StartAt: Map 46 | States: 47 | Map: 48 | Type: Map 49 | Items: "{% $states.context.Execution.Input.detail.images %}" 50 | ItemProcessor: 51 | ProcessorConfig: 52 | Mode: INLINE 53 | StartAt: CheckForUnsafeContentInImages 54 | States: 55 | CheckForUnsafeContentInImages: 56 | Type: Task 57 | Resource: arn:aws:states:::aws-sdk:rekognition:detectModerationLabels 58 | Arguments: 59 | Image: 60 | S3Object: 61 | Bucket: ${ImageUploadBucketName} 62 | Name: "{% $states.input %}" 63 | End: true 64 | End: true 65 | IsContentSafe: 66 | Type: Choice 67 | Choices: 68 | - Comment: ContentModerationPassed 69 | Condition: "{% $states.input[0].Sentiment = 'POSITIVE' and 70 | $not($exists($states.input[1].ModerationLabels.*)) %}" 71 | Next: WaitForContractApproval 72 | Default: PublishPropertyPublicationRejected 73 | WaitForContractApproval: 74 | Type: Task 75 | Resource: arn:aws:states:::lambda:invoke.waitForTaskToken 76 | Comment: ContractStatusChecker 77 | Arguments: 78 | FunctionName: ${WaitForContractApprovalArn} 79 | Payload: 80 | Input: "{% $states.context.Execution.Input.detail %}" 81 | TaskToken: "{% $states.context.Task.Token %}" 82 | Retry: 83 | - ErrorEquals: 84 | - Lambda.ServiceException 85 | - Lambda.AWSLambdaException 86 | - Lambda.SdkClientException 87 | - Lambda.TooManyRequestsException 88 | IntervalSeconds: 1 89 | MaxAttempts: 3 90 | BackoffRate: 2 91 | JitterStrategy: FULL 92 | Output: "{% $states.context.Execution.Input %}" 93 | Next: PublishPropertyPublicationApproved 94 | PublishPropertyPublicationApproved: 95 | Type: Task 96 | Resource: arn:aws:states:::events:putEvents 97 | Arguments: 98 | Entries: 99 | - EventBusName: ${EventBusName} 100 | Source: ${ServiceName} 101 | Detail: 102 | property_id: "{% $states.context.Execution.Input.detail.property_id %}" 103 | evaluation_result: APPROVED 104 | DetailType: PublicationEvaluationCompleted 105 | Next: Approved 106 | PublishPropertyPublicationRejected: 107 | Type: Task 108 | Resource: arn:aws:states:::events:putEvents 109 | Arguments: 110 | Entries: 111 | - EventBusName: ${EventBusName} 112 | Source: ${ServiceName} 113 | Detail: 114 | property_id: "{% $states.context.Execution.Input.detail.property_id %}" 115 | evaluation_result: DECLINED 116 | DetailType: PublicationEvaluationCompleted 117 | Next: Declined 118 | Declined: 119 | Type: Succeed 120 | Approved: 121 | Type: Succeed 122 | NotFound: 123 | Type: Fail 124 | -------------------------------------------------------------------------------- /unicorn_approvals/infrastructure/approvals-service/property_approval.asl.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | Comment: The property approval workflow ensures that its images and content is 4 | safe to publish and that there is an approved contract in place before the 5 | listing is made available to the public through the Unicorn Properties 6 | website. 7 | QueryLanguage: JSONata 8 | StartAt: LookupContract 9 | States: 10 | LookupContract: 11 | Type: Task 12 | Resource: arn:aws:states:::dynamodb:getItem 13 | Arguments: 14 | TableName: ${TableName} 15 | Key: 16 | property_id: 17 | S: "{% $states.input.detail.property_id %}" 18 | ProjectionExpression: contract_id, property_id, contract_status 19 | Next: VerifyContractExists 20 | Catch: 21 | - ErrorEquals: 22 | - States.ALL 23 | Next: NotFound 24 | VerifyContractExists: 25 | Type: Choice 26 | Choices: 27 | - Comment: Contract Exists 28 | Condition: "{% $exists($states.input.Item) %}" 29 | Next: Parallel 30 | Default: NotFound 31 | Comment: No Contract Found 32 | Parallel: 33 | Type: Parallel 34 | Next: IsContentSafe 35 | Branches: 36 | - StartAt: CheckDescriptionSentiment 37 | States: 38 | CheckDescriptionSentiment: 39 | Type: Task 40 | Resource: arn:aws:states:::aws-sdk:comprehend:detectSentiment 41 | Arguments: 42 | LanguageCode: en 43 | Text: "{% $states.context.Execution.Input.detail.description %}" 44 | End: true 45 | - StartAt: Map 46 | States: 47 | Map: 48 | Type: Map 49 | Items: "{% $states.context.Execution.Input.detail.images %}" 50 | ItemProcessor: 51 | ProcessorConfig: 52 | Mode: INLINE 53 | StartAt: CheckForUnsafeContentInImages 54 | States: 55 | CheckForUnsafeContentInImages: 56 | Type: Task 57 | Resource: arn:aws:states:::aws-sdk:rekognition:detectModerationLabels 58 | Arguments: 59 | Image: 60 | S3Object: 61 | Bucket: ${ImageUploadBucketName} 62 | Name: "{% $states.input %}" 63 | End: true 64 | End: true 65 | IsContentSafe: 66 | Type: Choice 67 | Choices: 68 | - Comment: ContentModerationPassed 69 | Condition: "{% $states.input[0].Sentiment = 'POSITIVE' and 70 | $not($exists($states.input[1].ModerationLabels.*)) %}" 71 | Next: WaitForContractApproval 72 | Default: PublishPropertyPublicationRejected 73 | WaitForContractApproval: 74 | Type: Task 75 | Resource: arn:aws:states:::lambda:invoke.waitForTaskToken 76 | Comment: ContractStatusChecker 77 | Arguments: 78 | FunctionName: ${WaitForContractApprovalArn} 79 | Payload: 80 | Input: "{% $states.context.Execution.Input.detail %}" 81 | TaskToken: "{% $states.context.Task.Token %}" 82 | Retry: 83 | - ErrorEquals: 84 | - Lambda.ServiceException 85 | - Lambda.AWSLambdaException 86 | - Lambda.SdkClientException 87 | - Lambda.TooManyRequestsException 88 | IntervalSeconds: 1 89 | MaxAttempts: 3 90 | BackoffRate: 2 91 | JitterStrategy: FULL 92 | Output: "{% $states.context.Execution.Input %}" 93 | Next: PublishPropertyPublicationApproved 94 | PublishPropertyPublicationApproved: 95 | Type: Task 96 | Resource: arn:aws:states:::events:putEvents 97 | Arguments: 98 | Entries: 99 | - EventBusName: ${EventBusName} 100 | Source: ${ServiceName} 101 | Detail: 102 | property_id: "{% $states.context.Execution.Input.detail.property_id %}" 103 | evaluation_result: APPROVED 104 | DetailType: PublicationEvaluationCompleted 105 | Next: Approved 106 | PublishPropertyPublicationRejected: 107 | Type: Task 108 | Resource: arn:aws:states:::events:putEvents 109 | Arguments: 110 | Entries: 111 | - EventBusName: ${EventBusName} 112 | Source: ${ServiceName} 113 | Detail: 114 | property_id: "{% $states.context.Execution.Input.detail.property_id %}" 115 | evaluation_result: DECLINED 116 | DetailType: PublicationEvaluationCompleted 117 | Next: Declined 118 | Declined: 119 | Type: Succeed 120 | Approved: 121 | Type: Succeed 122 | NotFound: 123 | Type: Fail 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build & Test Workflow](https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/actions/workflows/build_test.yml/badge.svg)](https://github.com/aws-samples/aws-serverless-developer-experience-workshop-typescript/actions/workflows/build_test.yml) 2 | 3 | # AWS Serverless Developer Experience workshop reference architecture (Typescript) 4 | 5 | AWS Serverless Developer Experience Workshop Reference Architecture 6 | 7 | This repository contains the Typescript reference architecture for the AWS Serverless Developer Experience workshop. 8 | 9 | The AWS Serverless Developer Experience Workshop is a comprehensive, hands-on training program designed to equip developers with practical serverless development skills using the [**AWS Serverless Application Model (AWS SAM)**](https://aws.amazon.com/serverless/sam/) and **AWS SAM CLI**. 10 | 11 | The workshop employs a practical, code-centric approach, emphasizing direct implementation and real-world scenario exploration to ensure you develop serverless development skills across several critical areas including distributed event-driven architectures, messaging patterns, orchestration, and observability. You will explore open-source tools, [Powertools for AWS](https://powertools.aws.dev/), and simplified CI/CD deployments with AWS SAM Pipelines. By the end, you will be familiar with serverless developer workflows, microservice composition using AWS SAM, serverless development best practices, and applied event-driven architectures. 12 | 13 | The 6-8 hour workshop assumes your practical development skills in Python, TypeScript, Java, or .NET, and familiarity with [Amazon API Gateway](https://aws.amazon.com/apigateway/), [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon EventBridge](https://aws.amazon.com/eventbridge/), [AWS Step Functions](https://aws.amazon.com/step-functions/), and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). 14 | 15 | ## Introducing the Unicorn Properties architecture 16 | 17 | ![AWS Serverless Developer Experience Workshop Reference Architecture](./docs/architecture.png) 18 | 19 | Real estate company **Unicorn Properties** needs to manage publishing of new property listings and sale contracts linked to individual properties, and provide a way for customers to view approved listings. They adopted a serverless, event-driven architecture with two primary domains: **Contracts** (managed by the Contracts Service) and **Properties** (managed by the Web and Approvals Services). 20 | 21 | **Unicorn Contracts** (using the `Unicorn.Contracts` namespace) service manages contractual relationships between property sellers and Unicorn Approvals, defining properties for sale, terms, and engagement costs. 22 | 23 | **Unicorn Approvals** (using the `Unicorn.Approvals` namespace) service approves property listings by implementing a workflow that checks for contract existence, content and image safety, and contract approval before publishing. 24 | 25 | **Unicorn Web** (using the `Unicorn.Web` namespace) manages property listing details (address, sale price, description, photos) to be published on the website, with only approved listings visible to the public. 26 | 27 | ## Credits 28 | 29 | This workshop introduces you to some open-source tools that can help you build serverless applications. This is not an exhaustive list, but a small selection of what you will be using in the workshop. 30 | 31 | Many thanks to all the AWS teams and community builders who have contributed to this list: 32 | 33 | | Tools | Description | Download / Installation Instructions | 34 | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | 35 | | cfn-lint | Validate AWS CloudFormation yaml/json templates against the AWS CloudFormation Resource Specification and additional checks. | https://github.com/aws-cloudformation/cfn-lint | 36 | | cfn-lint-serverless | Compilation of rules to validate infrastructure-as-code templates against recommended practices for serverless applications. | https://github.com/awslabs/serverless-rules | 37 | | @mhlabs/iam-policies-cli | CLI for generating AWS IAM policy documents or SAM policy templates based on the JSON definition used in the AWS Policy Generator. | https://github.com/mhlabs/iam-policies-cli | 38 | | @mhlabs/evb-cli | Pattern generator and debugging tool for Amazon EventBridge | https://github.com/mhlabs/evb-cli | 39 | -------------------------------------------------------------------------------- /unicorn_web/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Unicorn Web Service Infrastructure 2 | 3 | # Variables 4 | STAGE ?= local 5 | REGION ?= ap-southeast-2 6 | STACK_PREFIX = uni-prop-$(STAGE) 7 | 8 | .PHONY: help build deploy deploy-domain deploy-schema deploy-subscriptions deploy-service test unit-test integration-test lint clean delete 9 | 10 | help: ## Show available commands 11 | @echo "Unicorn Web Service Infrastructure" 12 | @echo "" 13 | @echo "Usage: make [target] [STAGE=local|dev|prod] [REGION=ap-southeast-2]" 14 | @echo "" 15 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}' 16 | 17 | build: ## Build the application 18 | @echo "Building TypeScript application..." 19 | @npm install 20 | @sam build --template-file infrastructure/web-service/template.yaml 21 | 22 | deploy-domain: ## Deploy domain resources (EventBridge, Schema Registry) 23 | @echo "Deploying domain resources..." 24 | @sam deploy \ 25 | --template-file infrastructure/domain.yaml \ 26 | --stack-name $(STACK_PREFIX)-web-domain \ 27 | --parameter-overrides Stage=$(STAGE) \ 28 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ 29 | --region $(REGION) \ 30 | --resolve-s3 \ 31 | --no-confirm-changeset \ 32 | --no-fail-on-empty-changeset 33 | 34 | deploy-schema: ## Deploy event schema 35 | @echo "Deploying event schema..." 36 | @sam deploy \ 37 | --template-file infrastructure/schema-registry/PublicationApprovalRequested-schema.yaml \ 38 | --stack-name $(STACK_PREFIX)-web-schema-PublicationApprovalRequested \ 39 | --parameter-overrides Stage=$(STAGE) \ 40 | --region $(REGION) \ 41 | --resolve-s3 \ 42 | --no-confirm-changeset \ 43 | --no-fail-on-empty-changeset 44 | 45 | deploy-subscriptions: ## Deploy event subscriptions 46 | @echo "Deploying event subscriptions..." 47 | @sam deploy \ 48 | --template-file infrastructure/subscriptions/unicorn-approvals-subscriptions.yaml \ 49 | --stack-name $(STACK_PREFIX)-web-subscriptions-approvals \ 50 | --parameter-overrides Stage=$(STAGE) \ 51 | --region $(REGION) \ 52 | --resolve-s3 \ 53 | --no-confirm-changeset \ 54 | --no-fail-on-empty-changeset 55 | 56 | deploy-service: build ## Deploy web service 57 | @echo "Deploying web service..." 58 | @sam deploy \ 59 | --template-file infrastructure/web-service/template.yaml \ 60 | --stack-name $(STACK_PREFIX)-web-service \ 61 | --parameter-overrides Stage=$(STAGE) \ 62 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ 63 | --region $(REGION) \ 64 | --resolve-s3 \ 65 | --no-confirm-changeset \ 66 | --no-fail-on-empty-changeset 67 | 68 | deploy: deploy-domain deploy-schema deploy-subscriptions deploy-service ## Deploy all infrastructure and application 69 | 70 | test: ## Run tests 71 | @echo "Running tests..." 72 | @npm test 73 | 74 | unit-test: ## Run unit tests only 75 | @echo "Running unit tests..." 76 | @npm run test -- --testPathPattern=tests/unit 77 | 78 | integration-test: ## Run integration tests only 79 | @echo "Running integration tests..." 80 | @npm run test -- --testPathPattern=tests/integration 81 | 82 | lint: ## Lint CloudFormation templates 83 | @echo "Linting CloudFormation templates..." 84 | @cfn-lint infrastructure/web-service.yaml -a cfn_lint_serverless.rules 85 | @cfn-lint infrastructure/domain.yaml 86 | @cfn-lint infrastructure/schema-registry/PublicationApprovalRequested-schema.yaml 87 | @cfn-lint infrastructure/subscriptions/unicorn-approvals-subscriptions.yaml 88 | 89 | clean: ## Clean build artifacts 90 | @echo "Cleaning build artifacts..." 91 | @rm -rf .aws-sam/ 92 | @rm -rf node_modules/ 93 | @rm -rf dist/ 94 | 95 | delete: ## Delete all stacks 96 | @echo "Deleting stacks in reverse order..." 97 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-web-service --region $(REGION) || true 98 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-web-subscriptions-approvals --region $(REGION) || true 99 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-web-schema-PublicationApprovalRequested --region $(REGION) || true 100 | @echo "Waiting for service, subscriptions, and schema stacks to be deleted..." 101 | @aws cloudformation wait stack-delete-complete --stack-name $(STACK_PREFIX)-web-service --region $(REGION) || true 102 | @aws cloudformation wait stack-delete-complete --stack-name $(STACK_PREFIX)-web-subscriptions-approvals --region $(REGION) || true 103 | @aws cloudformation wait stack-delete-complete --stack-name $(STACK_PREFIX)-web-schema-PublicationApprovalRequested --region $(REGION) || true 104 | @aws cloudformation delete-stack --stack-name $(STACK_PREFIX)-web-domain --region $(REGION) || true 105 | @echo "Waiting for domain stack to be deleted..." 106 | @aws cloudformation wait stack-delete-complete --stack-name $(STACK_PREFIX)-web-domain --region $(REGION) || true 107 | @echo "All stacks deleted" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### CDK-specific ignores ### 2 | *.swp 3 | cdk.context.json 4 | package-lock.json 5 | yarn.lock 6 | .cdk.staging 7 | cdk.out 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/node 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 11 | 12 | ### Node ### 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional stylelint cache 70 | .stylelintcache 71 | 72 | # Microbundle cache 73 | .rpt2_cache/ 74 | .rts2_cache_cjs/ 75 | .rts2_cache_es/ 76 | .rts2_cache_umd/ 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variable files 88 | .env 89 | .env.development.local 90 | .env.test.local 91 | .env.production.local 92 | .env.local 93 | 94 | # parcel-bundler cache (https://parceljs.org/) 95 | .cache 96 | .parcel-cache 97 | 98 | # Next.js build output 99 | .next 100 | out 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | dist 105 | 106 | # Gatsby files 107 | .cache/ 108 | # Comment in the public line in if your project uses Gatsby and not Next.js 109 | # https://nextjs.org/blog/next-9-1#public-directory-support 110 | # public 111 | 112 | # vuepress build output 113 | .vuepress/dist 114 | 115 | # vuepress v2.x temp and cache directory 116 | .temp 117 | 118 | # Docusaurus cache and generated files 119 | .docusaurus 120 | 121 | # Serverless directories 122 | .serverless/ 123 | 124 | # FuseBox cache 125 | .fusebox/ 126 | 127 | # DynamoDB Local files 128 | .dynamodb/ 129 | 130 | # TernJS port file 131 | .tern-port 132 | 133 | # Stores VSCode versions used for testing VSCode extensions 134 | .vscode-test 135 | 136 | # yarn v2 137 | .yarn/cache 138 | .yarn/unplugged 139 | .yarn/build-state.yml 140 | .yarn/install-state.gz 141 | .pnp.* 142 | 143 | ### Node Patch ### 144 | # Serverless Webpack directories 145 | .webpack/ 146 | 147 | # Optional stylelint cache 148 | 149 | # SvelteKit build / generate output 150 | .svelte-kit 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/node 153 | 154 | ### Linux ### 155 | **/*~ 156 | **/.fuse_hidden* 157 | **/.directory 158 | **/.Trash-* 159 | **/.nfs* 160 | 161 | ### OSX ### 162 | **/*.DS_Store 163 | **/.AppleDouble 164 | **/.LSOverride 165 | **/.DocumentRevisions-V100 166 | **/.fseventsd 167 | **/.Spotlight-V100 168 | **/.TemporaryItems 169 | **/.Trashes 170 | **/.VolumeIcon.icns 171 | **/.com.apple.timemachine.donotpresent 172 | 173 | ### JetBrains IDEs ### 174 | **/*.iws 175 | **/.idea/ 176 | **/.idea_modules/ 177 | 178 | ### Python ### 179 | **/__pycache__/ 180 | **/*.py[cod] 181 | **/*$py.class 182 | **/.Python 183 | **/build/ 184 | **/develop-eggs/ 185 | **/dist/ 186 | **/downloads/ 187 | **/eggs/ 188 | **/.eggs/ 189 | **/parts/ 190 | **/sdist/ 191 | **/wheels/ 192 | **/*.egg 193 | **/*.egg-info/ 194 | **/.installed.cfg 195 | **/pip-log.txt 196 | **/pip-delete-this-directory.txt 197 | 198 | ### Unit test / coverage reports ### 199 | **/.cache 200 | **/.coverage 201 | **/.hypothesis/ 202 | **/.pytest_cache/ 203 | **/.tox/ 204 | **/*.cover 205 | **/coverage.xml 206 | **/htmlcov/ 207 | **/nosetests.xml 208 | 209 | ### pyenv ### 210 | **/.python-version 211 | 212 | ### Environments ### 213 | **/.env 214 | **/.venv 215 | **/env/ 216 | **/venv/ 217 | **/ENV/ 218 | **/env.bak/ 219 | **/venv.bak/ 220 | 221 | ### VisualStudioCode ### 222 | **/.vscode/ 223 | **/.history 224 | 225 | ### Windows ### 226 | # Windows thumbnail cache files 227 | **/Thumbs.db 228 | **/ehthumbs.db 229 | **/ehthumbs_vista.db 230 | **/Desktop.ini 231 | **/$RECYCLE.BIN/ 232 | **/*.lnk 233 | 234 | ### requirements.txt ### 235 | # We ignore Python's requirements.txt as we use UV instead 236 | **/requirements.txt 237 | **/.aws-sam -------------------------------------------------------------------------------- /unicorn_web/.gitignore: -------------------------------------------------------------------------------- 1 | ### CDK-specific ignores ### 2 | *.swp 3 | cdk.context.json 4 | package-lock.json 5 | yarn.lock 6 | .cdk.staging 7 | cdk.out 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/node 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 11 | 12 | ### Node ### 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional stylelint cache 70 | .stylelintcache 71 | 72 | # Microbundle cache 73 | .rpt2_cache/ 74 | .rts2_cache_cjs/ 75 | .rts2_cache_es/ 76 | .rts2_cache_umd/ 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variable files 88 | .env 89 | .env.development.local 90 | .env.test.local 91 | .env.production.local 92 | .env.local 93 | 94 | # parcel-bundler cache (https://parceljs.org/) 95 | .cache 96 | .parcel-cache 97 | 98 | # Next.js build output 99 | .next 100 | out 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | dist 105 | 106 | # Gatsby files 107 | .cache/ 108 | # Comment in the public line in if your project uses Gatsby and not Next.js 109 | # https://nextjs.org/blog/next-9-1#public-directory-support 110 | # public 111 | 112 | # vuepress build output 113 | .vuepress/dist 114 | 115 | # vuepress v2.x temp and cache directory 116 | .temp 117 | 118 | # Docusaurus cache and generated files 119 | .docusaurus 120 | 121 | # Serverless directories 122 | .serverless/ 123 | 124 | # FuseBox cache 125 | .fusebox/ 126 | 127 | # DynamoDB Local files 128 | .dynamodb/ 129 | 130 | # TernJS port file 131 | .tern-port 132 | 133 | # Stores VSCode versions used for testing VSCode extensions 134 | .vscode-test 135 | 136 | # yarn v2 137 | .yarn/cache 138 | .yarn/unplugged 139 | .yarn/build-state.yml 140 | .yarn/install-state.gz 141 | .pnp.* 142 | 143 | ### Node Patch ### 144 | # Serverless Webpack directories 145 | .webpack/ 146 | 147 | # Optional stylelint cache 148 | 149 | # SvelteKit build / generate output 150 | .svelte-kit 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/node 153 | 154 | ### Linux ### 155 | **/*~ 156 | **/.fuse_hidden* 157 | **/.directory 158 | **/.Trash-* 159 | **/.nfs* 160 | 161 | ### OSX ### 162 | **/*.DS_Store 163 | **/.AppleDouble 164 | **/.LSOverride 165 | **/.DocumentRevisions-V100 166 | **/.fseventsd 167 | **/.Spotlight-V100 168 | **/.TemporaryItems 169 | **/.Trashes 170 | **/.VolumeIcon.icns 171 | **/.com.apple.timemachine.donotpresent 172 | 173 | ### JetBrains IDEs ### 174 | **/*.iws 175 | **/.idea/ 176 | **/.idea_modules/ 177 | 178 | ### Python ### 179 | **/__pycache__/ 180 | **/*.py[cod] 181 | **/*$py.class 182 | **/.Python 183 | **/build/ 184 | **/develop-eggs/ 185 | **/dist/ 186 | **/downloads/ 187 | **/eggs/ 188 | **/.eggs/ 189 | **/parts/ 190 | **/sdist/ 191 | **/wheels/ 192 | **/*.egg 193 | **/*.egg-info/ 194 | **/.installed.cfg 195 | **/pip-log.txt 196 | **/pip-delete-this-directory.txt 197 | 198 | ### Unit test / coverage reports ### 199 | **/.cache 200 | **/.coverage 201 | **/.hypothesis/ 202 | **/.pytest_cache/ 203 | **/.tox/ 204 | **/*.cover 205 | **/coverage.xml 206 | **/htmlcov/ 207 | **/nosetests.xml 208 | 209 | ### pyenv ### 210 | **/.python-version 211 | 212 | ### Environments ### 213 | **/.env 214 | **/.venv 215 | **/env/ 216 | **/venv/ 217 | **/ENV/ 218 | **/env.bak/ 219 | **/venv.bak/ 220 | 221 | ### VisualStudioCode ### 222 | **/.vscode/ 223 | **/.history 224 | 225 | ### Windows ### 226 | # Windows thumbnail cache files 227 | **/Thumbs.db 228 | **/ehthumbs.db 229 | **/ehthumbs_vista.db 230 | **/Desktop.ini 231 | **/$RECYCLE.BIN/ 232 | **/*.lnk 233 | 234 | ### requirements.txt ### 235 | # We ignore Python's requirements.txt as we use UV instead 236 | **/requirements.txt 237 | **/.aws-sam -------------------------------------------------------------------------------- /unicorn_approvals/.gitignore: -------------------------------------------------------------------------------- 1 | ### CDK-specific ignores ### 2 | *.swp 3 | cdk.context.json 4 | package-lock.json 5 | yarn.lock 6 | .cdk.staging 7 | cdk.out 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/node 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 11 | 12 | ### Node ### 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional stylelint cache 70 | .stylelintcache 71 | 72 | # Microbundle cache 73 | .rpt2_cache/ 74 | .rts2_cache_cjs/ 75 | .rts2_cache_es/ 76 | .rts2_cache_umd/ 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variable files 88 | .env 89 | .env.development.local 90 | .env.test.local 91 | .env.production.local 92 | .env.local 93 | 94 | # parcel-bundler cache (https://parceljs.org/) 95 | .cache 96 | .parcel-cache 97 | 98 | # Next.js build output 99 | .next 100 | out 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | dist 105 | 106 | # Gatsby files 107 | .cache/ 108 | # Comment in the public line in if your project uses Gatsby and not Next.js 109 | # https://nextjs.org/blog/next-9-1#public-directory-support 110 | # public 111 | 112 | # vuepress build output 113 | .vuepress/dist 114 | 115 | # vuepress v2.x temp and cache directory 116 | .temp 117 | 118 | # Docusaurus cache and generated files 119 | .docusaurus 120 | 121 | # Serverless directories 122 | .serverless/ 123 | 124 | # FuseBox cache 125 | .fusebox/ 126 | 127 | # DynamoDB Local files 128 | .dynamodb/ 129 | 130 | # TernJS port file 131 | .tern-port 132 | 133 | # Stores VSCode versions used for testing VSCode extensions 134 | .vscode-test 135 | 136 | # yarn v2 137 | .yarn/cache 138 | .yarn/unplugged 139 | .yarn/build-state.yml 140 | .yarn/install-state.gz 141 | .pnp.* 142 | 143 | ### Node Patch ### 144 | # Serverless Webpack directories 145 | .webpack/ 146 | 147 | # Optional stylelint cache 148 | 149 | # SvelteKit build / generate output 150 | .svelte-kit 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/node 153 | 154 | ### Linux ### 155 | **/*~ 156 | **/.fuse_hidden* 157 | **/.directory 158 | **/.Trash-* 159 | **/.nfs* 160 | 161 | ### OSX ### 162 | **/*.DS_Store 163 | **/.AppleDouble 164 | **/.LSOverride 165 | **/.DocumentRevisions-V100 166 | **/.fseventsd 167 | **/.Spotlight-V100 168 | **/.TemporaryItems 169 | **/.Trashes 170 | **/.VolumeIcon.icns 171 | **/.com.apple.timemachine.donotpresent 172 | 173 | ### JetBrains IDEs ### 174 | **/*.iws 175 | **/.idea/ 176 | **/.idea_modules/ 177 | 178 | ### Python ### 179 | **/__pycache__/ 180 | **/*.py[cod] 181 | **/*$py.class 182 | **/.Python 183 | **/build/ 184 | **/develop-eggs/ 185 | **/dist/ 186 | **/downloads/ 187 | **/eggs/ 188 | **/.eggs/ 189 | **/parts/ 190 | **/sdist/ 191 | **/wheels/ 192 | **/*.egg 193 | **/*.egg-info/ 194 | **/.installed.cfg 195 | **/pip-log.txt 196 | **/pip-delete-this-directory.txt 197 | 198 | ### Unit test / coverage reports ### 199 | **/.cache 200 | **/.coverage 201 | **/.hypothesis/ 202 | **/.pytest_cache/ 203 | **/.tox/ 204 | **/*.cover 205 | **/coverage.xml 206 | **/htmlcov/ 207 | **/nosetests.xml 208 | 209 | ### pyenv ### 210 | **/.python-version 211 | 212 | ### Environments ### 213 | **/.env 214 | **/.venv 215 | **/env/ 216 | **/venv/ 217 | **/ENV/ 218 | **/env.bak/ 219 | **/venv.bak/ 220 | 221 | ### VisualStudioCode ### 222 | **/.vscode/ 223 | **/.history 224 | 225 | ### Windows ### 226 | # Windows thumbnail cache files 227 | **/Thumbs.db 228 | **/ehthumbs.db 229 | **/ehthumbs_vista.db 230 | **/Desktop.ini 231 | **/$RECYCLE.BIN/ 232 | **/*.lnk 233 | 234 | ### requirements.txt ### 235 | # We ignore Python's requirements.txt as we use UV instead 236 | **/requirements.txt 237 | **/.aws-sam -------------------------------------------------------------------------------- /unicorn_contracts/.gitignore: -------------------------------------------------------------------------------- 1 | ### CDK-specific ignores ### 2 | *.swp 3 | cdk.context.json 4 | package-lock.json 5 | yarn.lock 6 | .cdk.staging 7 | cdk.out 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/node 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 11 | 12 | ### Node ### 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional stylelint cache 70 | .stylelintcache 71 | 72 | # Microbundle cache 73 | .rpt2_cache/ 74 | .rts2_cache_cjs/ 75 | .rts2_cache_es/ 76 | .rts2_cache_umd/ 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variable files 88 | .env 89 | .env.development.local 90 | .env.test.local 91 | .env.production.local 92 | .env.local 93 | 94 | # parcel-bundler cache (https://parceljs.org/) 95 | .cache 96 | .parcel-cache 97 | 98 | # Next.js build output 99 | .next 100 | out 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | dist 105 | 106 | # Gatsby files 107 | .cache/ 108 | # Comment in the public line in if your project uses Gatsby and not Next.js 109 | # https://nextjs.org/blog/next-9-1#public-directory-support 110 | # public 111 | 112 | # vuepress build output 113 | .vuepress/dist 114 | 115 | # vuepress v2.x temp and cache directory 116 | .temp 117 | 118 | # Docusaurus cache and generated files 119 | .docusaurus 120 | 121 | # Serverless directories 122 | .serverless/ 123 | 124 | # FuseBox cache 125 | .fusebox/ 126 | 127 | # DynamoDB Local files 128 | .dynamodb/ 129 | 130 | # TernJS port file 131 | .tern-port 132 | 133 | # Stores VSCode versions used for testing VSCode extensions 134 | .vscode-test 135 | 136 | # yarn v2 137 | .yarn/cache 138 | .yarn/unplugged 139 | .yarn/build-state.yml 140 | .yarn/install-state.gz 141 | .pnp.* 142 | 143 | ### Node Patch ### 144 | # Serverless Webpack directories 145 | .webpack/ 146 | 147 | # Optional stylelint cache 148 | 149 | # SvelteKit build / generate output 150 | .svelte-kit 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/node 153 | 154 | ### Linux ### 155 | **/*~ 156 | **/.fuse_hidden* 157 | **/.directory 158 | **/.Trash-* 159 | **/.nfs* 160 | 161 | ### OSX ### 162 | **/*.DS_Store 163 | **/.AppleDouble 164 | **/.LSOverride 165 | **/.DocumentRevisions-V100 166 | **/.fseventsd 167 | **/.Spotlight-V100 168 | **/.TemporaryItems 169 | **/.Trashes 170 | **/.VolumeIcon.icns 171 | **/.com.apple.timemachine.donotpresent 172 | 173 | ### JetBrains IDEs ### 174 | **/*.iws 175 | **/.idea/ 176 | **/.idea_modules/ 177 | 178 | ### Python ### 179 | **/__pycache__/ 180 | **/*.py[cod] 181 | **/*$py.class 182 | **/.Python 183 | **/build/ 184 | **/develop-eggs/ 185 | **/dist/ 186 | **/downloads/ 187 | **/eggs/ 188 | **/.eggs/ 189 | **/parts/ 190 | **/sdist/ 191 | **/wheels/ 192 | **/*.egg 193 | **/*.egg-info/ 194 | **/.installed.cfg 195 | **/pip-log.txt 196 | **/pip-delete-this-directory.txt 197 | 198 | ### Unit test / coverage reports ### 199 | **/.cache 200 | **/.coverage 201 | **/.hypothesis/ 202 | **/.pytest_cache/ 203 | **/.tox/ 204 | **/*.cover 205 | **/coverage.xml 206 | **/htmlcov/ 207 | **/nosetests.xml 208 | 209 | ### pyenv ### 210 | **/.python-version 211 | 212 | ### Environments ### 213 | **/.env 214 | **/.venv 215 | **/env/ 216 | **/venv/ 217 | **/ENV/ 218 | **/env.bak/ 219 | **/venv.bak/ 220 | 221 | ### VisualStudioCode ### 222 | **/.vscode/ 223 | **/.history 224 | 225 | ### Windows ### 226 | # Windows thumbnail cache files 227 | **/Thumbs.db 228 | **/ehthumbs.db 229 | **/ehthumbs_vista.db 230 | **/Desktop.ini 231 | **/$RECYCLE.BIN/ 232 | **/*.lnk 233 | 234 | ### requirements.txt ### 235 | # We ignore Python's requirements.txt as we use UV instead 236 | **/requirements.txt 237 | **/.aws-sam -------------------------------------------------------------------------------- /unicorn_web/src/publication_manager_service/publicationEvaluationEventHandler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { EventBridgeEvent, Context } from 'aws-lambda'; 4 | import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; 5 | import { MetricUnit } from '@aws-lambda-powertools/metrics'; 6 | import { logger, metrics, tracer } from './powertools'; 7 | import { 8 | DynamoDBClient, 9 | UpdateItemCommand, 10 | UpdateItemCommandInput, 11 | } from '@aws-sdk/client-dynamodb'; 12 | import { PublicationEvaluationCompleted } from '../schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted'; 13 | import { Marshaller } from '../schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller'; 14 | 15 | // Empty configuration for DynamoDB 16 | const ddbClient = new DynamoDBClient({}); 17 | const DDB_TABLE = process.env.DYNAMODB_TABLE; 18 | 19 | class PublicationEvaluationEventHandler implements LambdaInterface { 20 | /** 21 | * Handle the contract status changed event from the EventBridge instance. 22 | * @param {EventBridgeEvent} event - EventBridge Event Input Format 23 | * @returns {void} 24 | * 25 | */ 26 | @tracer.captureLambdaHandler() 27 | @metrics.logMetrics({ captureColdStartMetric: true }) 28 | @logger.injectLambdaContext({ logEvent: true }) 29 | public async handler( 30 | event: EventBridgeEvent, 31 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 32 | context: Context 33 | ): Promise { 34 | logger.info(`Property status changed: ${JSON.stringify(event.detail)}`); 35 | // Construct the entry to insert into database. 36 | const propertyEvaluation: PublicationEvaluationCompleted = 37 | Marshaller.unmarshal(event.detail, 'PublicationEvaluationCompleted'); 38 | logger.info(`Unmarshalled entry: ${JSON.stringify(propertyEvaluation)}`); 39 | 40 | try { 41 | await this.publicationApproved(propertyEvaluation); 42 | } catch (error: any) { 43 | tracer.addErrorAsMetadata(error as Error); 44 | logger.error(`Error during DDB UPDATE: ${JSON.stringify(error)}`); 45 | } 46 | metrics.addMetric('ContractUpdated', MetricUnit.Count, 1); 47 | } 48 | 49 | /** 50 | * Update the Property entry in the database 51 | * @private 52 | * @async 53 | * @method publicationApproved 54 | * @param {PublicationEvaluationCompleted} event - The EventBridge event when a contract changes 55 | * @returns {Promise} - A promise that resolves when all records have been processed. 56 | */ 57 | @tracer.captureMethod() 58 | private async publicationApproved( 59 | propertyEvaluation: PublicationEvaluationCompleted 60 | ) { 61 | tracer.putAnnotation('propertyId', propertyEvaluation.propertyId); 62 | logger.info( 63 | `Updating status: ${propertyEvaluation.evaluationResult} for ${propertyEvaluation.propertyId}` 64 | ); 65 | const propertyId = propertyEvaluation.propertyId; 66 | const { PK, SK } = this.getDynamoDBKeys(propertyId); 67 | const updateItemCommandInput: UpdateItemCommandInput = { 68 | Key: { PK: { S: PK }, SK: { S: SK } }, 69 | ExpressionAttributeNames: { 70 | '#s': 'status', 71 | }, 72 | ExpressionAttributeValues: { 73 | ':t': { 74 | S: propertyEvaluation.evaluationResult, 75 | }, 76 | }, 77 | UpdateExpression: 'SET #s = :t', 78 | TableName: DDB_TABLE, 79 | }; 80 | 81 | const data = await ddbClient.send( 82 | new UpdateItemCommand(updateItemCommandInput) 83 | ); 84 | if (data.$metadata.httpStatusCode !== 200) { 85 | throw new Error( 86 | `Unable to update status for property PK ${PK} and SK ${SK}` 87 | ); 88 | } 89 | logger.info(`Updated status for property PK ${PK} and SK ${SK}`); 90 | } 91 | 92 | private getDynamoDBKeys(property_id: string) { 93 | // Form the PK and SK from the property id. 94 | const components: string[] = property_id.split('/'); 95 | if (components.length < 4) { 96 | throw new Error(`Invalid propertyId ${property_id}`); 97 | } 98 | const country = components[0]; 99 | const city = components[1]; 100 | const street = components[2]; 101 | const number = components[3]; 102 | 103 | const pkDetails = `${country}#${city}`.replace(' ', '-').toLowerCase(); 104 | const PK = `PROPERTY#${pkDetails}`; 105 | const SK = `${street}#${number}`.replace(' ', '-').toLowerCase(); 106 | 107 | return { PK, SK }; 108 | } 109 | } 110 | 111 | const myFunction = new PublicationEvaluationEventHandler(); 112 | export const lambdaHandler = async ( 113 | event: EventBridgeEvent, 114 | context: Context 115 | ): Promise => { 116 | return myFunction.handler(event, context); 117 | }; 118 | -------------------------------------------------------------------------------- /unicorn_approvals/src/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.ts: -------------------------------------------------------------------------------- 1 | import { AWSEvent } from '../AWSEvent'; 2 | import { ContractStatusChanged } from '../ContractStatusChanged'; 3 | 4 | const primitives = [ 5 | 'string', 6 | 'boolean', 7 | 'double', 8 | 'integer', 9 | 'long', 10 | 'float', 11 | 'number', 12 | 'any', 13 | ]; 14 | 15 | const enumsMap: Record = {}; 16 | 17 | const typeMap: Record = { 18 | AWSEvent: AWSEvent, 19 | ContractStatusChanged: ContractStatusChanged, 20 | }; 21 | 22 | export class Marshaller { 23 | public static marshall(data: any, type: string) { 24 | if (data == undefined) { 25 | return data; 26 | } else if (primitives.indexOf(type.toLowerCase()) !== -1) { 27 | return data; 28 | } else if (type.lastIndexOf('Array<', 0) === 0) { 29 | // string.startsWith pre es6 30 | let subType: string = type.replace('Array<', ''); // Array => Type> 31 | subType = subType.substring(0, subType.length - 1); // Type> => Type 32 | const transformedData: any[] = []; 33 | for (const index in data) { 34 | const date = data[index]; 35 | transformedData.push(Marshaller.marshall(date, subType)); 36 | } 37 | return transformedData; 38 | } else if (type === 'Date') { 39 | return data.toString(); 40 | } else { 41 | if (enumsMap[type]) { 42 | return data; 43 | } 44 | if (!typeMap[type]) { 45 | // in case we dont know the type 46 | return data; 47 | } 48 | 49 | // get the map for the correct type. 50 | const attributeTypes = typeMap[type].getAttributeTypeMap(); 51 | const instance: Record = {}; 52 | for (const index in attributeTypes) { 53 | const attributeType = attributeTypes[index]; 54 | instance[attributeType.baseName] = Marshaller.marshall( 55 | data[attributeType.name], 56 | attributeType.type 57 | ); 58 | } 59 | return instance; 60 | } 61 | } 62 | 63 | public static unmarshalEvent(data: any, detailType: any) { 64 | typeMap['AWSEvent'].updateAttributeTypeMapDetail(detailType.name); 65 | return this.unmarshal(data, 'AWSEvent'); 66 | } 67 | 68 | public static unmarshal(data: any, type: string) { 69 | // polymorphism may change the actual type. 70 | type = Marshaller.findCorrectType(data, type); 71 | if (data == undefined) { 72 | return data; 73 | } else if (primitives.indexOf(type.toLowerCase()) !== -1) { 74 | return data; 75 | } else if (type.lastIndexOf('Array<', 0) === 0) { 76 | // string.startsWith pre es6 77 | let subType: string = type.replace('Array<', ''); // Array => Type> 78 | subType = subType.substring(0, subType.length - 1); // Type> => Type 79 | const transformedData: any[] = []; 80 | for (const index in data) { 81 | const date = data[index]; 82 | transformedData.push(Marshaller.unmarshal(date, subType)); 83 | } 84 | return transformedData; 85 | } else if (type === 'Date') { 86 | return new Date(data); 87 | } else { 88 | if (enumsMap[type]) { 89 | // is Enum 90 | return data; 91 | } 92 | 93 | if (!typeMap[type]) { 94 | // dont know the type 95 | return data; 96 | } 97 | const instance = new typeMap[type](); 98 | const attributeTypes = typeMap[type].getAttributeTypeMap(); 99 | for (const index in attributeTypes) { 100 | const attributeType = attributeTypes[index]; 101 | instance[attributeType.name] = Marshaller.unmarshal( 102 | data[attributeType.baseName], 103 | attributeType.type 104 | ); 105 | } 106 | return instance; 107 | } 108 | } 109 | 110 | private static findCorrectType(data: any, expectedType: string) { 111 | if (data == undefined) { 112 | return expectedType; 113 | } else if (primitives.indexOf(expectedType.toLowerCase()) !== -1) { 114 | return expectedType; 115 | } else if (expectedType === 'Date') { 116 | return expectedType; 117 | } else { 118 | if (enumsMap[expectedType]) { 119 | return expectedType; 120 | } 121 | 122 | if (!typeMap[expectedType]) { 123 | return expectedType; // unknown type 124 | } 125 | 126 | // Check the discriminator 127 | const discriminatorProperty = typeMap[expectedType].discriminator; 128 | if (discriminatorProperty == null) { 129 | return expectedType; // the type does not have a discriminator. use it. 130 | } else { 131 | if (data[discriminatorProperty]) { 132 | return data[discriminatorProperty]; // use the type given in the discriminator 133 | } else { 134 | return expectedType; // discriminator was not present (or an empty string) 135 | } 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /unicorn_web/src/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { AWSEvent } from '../AWSEvent'; 4 | import { PublicationEvaluationCompleted } from '../PublicationEvaluationCompleted'; 5 | 6 | const primitives = [ 7 | 'string', 8 | 'boolean', 9 | 'double', 10 | 'integer', 11 | 'long', 12 | 'float', 13 | 'number', 14 | 'any', 15 | ]; 16 | 17 | const enumsMap: Record = {}; 18 | 19 | const typeMap: Record = { 20 | AWSEvent: AWSEvent, 21 | PublicationEvaluationCompleted: PublicationEvaluationCompleted, 22 | }; 23 | 24 | export class Marshaller { 25 | public static marshall(data: any, type: string) { 26 | if (data == undefined) { 27 | return data; 28 | } else if (primitives.indexOf(type.toLowerCase()) !== -1) { 29 | return data; 30 | } else if (type.lastIndexOf('Array<', 0) === 0) { 31 | // string.startsWith pre es6 32 | let subType: string = type.replace('Array<', ''); // Array => Type> 33 | subType = subType.substring(0, subType.length - 1); // Type> => Type 34 | const transformedData: any[] = []; 35 | for (const index in data) { 36 | const date = data[index]; 37 | transformedData.push(Marshaller.marshall(date, subType)); 38 | } 39 | return transformedData; 40 | } else if (type === 'Date') { 41 | return data.toString(); 42 | } else { 43 | if (enumsMap[type]) { 44 | return data; 45 | } 46 | if (!typeMap[type]) { 47 | // in case we dont know the type 48 | return data; 49 | } 50 | 51 | // get the map for the correct type. 52 | const attributeTypes = typeMap[type].getAttributeTypeMap(); 53 | const instance: Record = {}; 54 | for (const index in attributeTypes) { 55 | const attributeType = attributeTypes[index]; 56 | instance[attributeType.baseName] = Marshaller.marshall( 57 | data[attributeType.name], 58 | attributeType.type 59 | ); 60 | } 61 | return instance; 62 | } 63 | } 64 | 65 | public static unmarshalEvent(data: any, detailType: any) { 66 | typeMap['AWSEvent'].updateAttributeTypeMapDetail(detailType.name); 67 | return this.unmarshal(data, 'AWSEvent'); 68 | } 69 | 70 | public static unmarshal(data: any, type: string) { 71 | // polymorphism may change the actual type. 72 | type = Marshaller.findCorrectType(data, type); 73 | if (data == undefined) { 74 | return data; 75 | } else if (primitives.indexOf(type.toLowerCase()) !== -1) { 76 | return data; 77 | } else if (type.lastIndexOf('Array<', 0) === 0) { 78 | // string.startsWith pre es6 79 | let subType: string = type.replace('Array<', ''); // Array => Type> 80 | subType = subType.substring(0, subType.length - 1); // Type> => Type 81 | const transformedData: any[] = []; 82 | for (const index in data) { 83 | const date = data[index]; 84 | transformedData.push(Marshaller.unmarshal(date, subType)); 85 | } 86 | return transformedData; 87 | } else if (type === 'Date') { 88 | return new Date(data); 89 | } else { 90 | if (enumsMap[type]) { 91 | // is Enum 92 | return data; 93 | } 94 | 95 | if (!typeMap[type]) { 96 | // dont know the type 97 | return data; 98 | } 99 | const instance = new typeMap[type](); 100 | const attributeTypes = typeMap[type].getAttributeTypeMap(); 101 | for (const index in attributeTypes) { 102 | const attributeType = attributeTypes[index]; 103 | instance[attributeType.name] = Marshaller.unmarshal( 104 | data[attributeType.baseName], 105 | attributeType.type 106 | ); 107 | } 108 | return instance; 109 | } 110 | } 111 | 112 | private static findCorrectType(data: any, expectedType: string) { 113 | if (data == undefined) { 114 | return expectedType; 115 | } else if (primitives.indexOf(expectedType.toLowerCase()) !== -1) { 116 | return expectedType; 117 | } else if (expectedType === 'Date') { 118 | return expectedType; 119 | } else { 120 | if (enumsMap[expectedType]) { 121 | return expectedType; 122 | } 123 | 124 | if (!typeMap[expectedType]) { 125 | return expectedType; // unknown type 126 | } 127 | 128 | // Check the discriminator 129 | const discriminatorProperty = typeMap[expectedType].discriminator; 130 | if (discriminatorProperty == null) { 131 | return expectedType; // the type does not have a discriminator. use it. 132 | } else { 133 | if (data[discriminatorProperty]) { 134 | return data[discriminatorProperty]; // use the type given in the discriminator 135 | } else { 136 | return expectedType; // discriminator was not present (or an empty string) 137 | } 138 | } 139 | } 140 | } 141 | } 142 | --------------------------------------------------------------------------------