├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── maintenance.yml ├── PULL_REQUEST_TEMPLATE.md ├── auto_assign.yml ├── boring-cyborg.yml ├── scripts │ ├── comment_on_large_pr.js │ ├── constants.js │ ├── download_pr_artifact.js │ ├── enforce_acknowledgment.js │ ├── label_missing_acknowledgement_section.js │ ├── label_missing_related_issue.js │ ├── label_pr_based_on_title.js │ ├── label_related_issue.js │ └── save_pr_details.js ├── semantic.yml └── workflows │ ├── auto_assign.yml │ ├── build.yml │ ├── codeql-analysis.yml │ ├── label_pr_on_title.yml │ ├── on_label_added.yml │ ├── on_merged_pr.yml │ ├── on_opened_pr.yml │ ├── record_pr.yml │ └── reusable_export_pr_details.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── architecture.png └── workshop_logo.png ├── pom.xml ├── unicorn_contracts ├── .gitignore ├── ContractsFunction │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── contracts │ │ │ │ ├── ContractEventHandler.java │ │ │ │ └── utils │ │ │ │ ├── Address.java │ │ │ │ ├── Contract.java │ │ │ │ ├── ContractStatusChangedEvent.java │ │ │ │ ├── ContractStatusEnum.java │ │ │ │ └── ResponseParser.java │ │ └── resources │ │ │ └── log4j2.xml │ │ └── test │ │ ├── events │ │ ├── create_empty_dict_body_event.json │ │ ├── create_missing_body_event.json │ │ ├── create_valid_event.json │ │ ├── create_wrong_event.json │ │ ├── update_empty_dict_body_event.json │ │ ├── update_missing_body_event.json │ │ ├── update_valid_event.json │ │ └── update_wrong_event.json │ │ └── java │ │ └── contracts │ │ └── CreateContractTests.java ├── README.md ├── api.yaml ├── events │ ├── contract_status_changed.json │ ├── event.json │ └── put_events.json ├── integration │ ├── ContractStatusChanged.json │ ├── event-schemas.yaml │ └── subscriber-policies.yaml ├── samconfig.toml └── template.yaml ├── unicorn_properties ├── .gitignore ├── PropertyFunctions │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ ├── properties │ │ │ │ ├── ContractStatusChangedHandlerFunction.java │ │ │ │ ├── ContractStatusNotFoundException.java │ │ │ │ ├── PropertiesApprovalSyncFunction.java │ │ │ │ ├── WaitForContractApprovalFunction.java │ │ │ │ └── dao │ │ │ │ │ └── ContractStatus.java │ │ │ └── schema │ │ │ │ └── unicorn_contracts │ │ │ │ └── contractstatuschanged │ │ │ │ ├── ContractStatusChanged.java │ │ │ │ ├── Event.java │ │ │ │ └── marshaller │ │ │ │ └── Marshaller.java │ │ └── resources │ │ │ └── log4j2.xml │ │ └── test │ │ ├── events │ │ ├── dbb_stream_events │ │ │ ├── contract_status_changed_draft.json │ │ │ ├── sfn_check_exists.json │ │ │ ├── sfn_wait_approval.json │ │ │ ├── status_approved_waiting_for_approval.json │ │ │ └── status_approved_with_no_workflow.json │ │ ├── eventbridge │ │ │ ├── contract_status_changed_event_contract_1_approved.json │ │ │ ├── contract_status_changed_event_contract_1_draft.json │ │ │ ├── contract_status_changed_event_contract_2_approved.json │ │ │ ├── contract_status_changed_event_contract_2_draft.json │ │ │ ├── publication_approval_requested_event.json │ │ │ ├── publication_approval_requested_event_all_good.json │ │ │ ├── publication_approval_requested_event_inappropriate_description.json │ │ │ ├── publication_approval_requested_event_inappropriate_images.json │ │ │ ├── publication_approval_requested_event_non_existing_contract.json │ │ │ ├── publication_approval_requested_event_pause_workflow.json │ │ │ ├── publication_evaluation_completed_event.json │ │ │ └── put_event_property_approval_requested.json │ │ ├── lambda │ │ │ ├── contract_status_changed.json │ │ │ └── contract_status_checker.json │ │ ├── put_event_contract_status_changed.json │ │ ├── put_event_publication_approval_requested.json │ │ └── send_events_cli.json │ │ └── java │ │ └── properties │ │ └── ContractStatusTests.java ├── README.md ├── integration │ ├── PublicationEvaluationCompleted.json │ ├── event-schemas.yaml │ ├── subscriber-policies.yaml │ └── subscriptions.yaml ├── samconfig.toml ├── state_machine │ └── property_approval.asl.yaml └── template.yaml ├── unicorn_shared ├── Makefile ├── uni-prop-images.yaml └── uni-prop-namespaces.yaml └── unicorn_web ├── .gitignore ├── Data ├── load_data.sh └── property_data.json ├── PropertyFunctions ├── pom.xml └── src │ └── main │ ├── java │ ├── property │ │ ├── dao │ │ │ └── Property.java │ │ ├── populate │ │ │ └── PopulateDataFunction.java │ │ ├── requestapproval │ │ │ ├── PublicationApprovedFunction.java │ │ │ └── RequestApprovalFunction.java │ │ └── search │ │ │ └── PropertySearchFunction.java │ └── schema │ │ └── unicorn_properties │ │ └── publicationevaluationcompleted │ │ ├── AWSEvent.java │ │ ├── PublicationEvaluationCompleted.java │ │ └── marshaller │ │ └── Marshaller.java │ └── resources │ └── log4j2.xml ├── README.md ├── api.yaml ├── integration ├── PublicationApprovalRequested.json ├── event-schemas.yaml ├── subscriber-policies.yaml └── subscriptions.yaml ├── samconfig.toml ├── template.yaml └── tests └── events └── eventbridge └── put_event_publication_evaluation_completed.json /.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-java -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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-java/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 | -------------------------------------------------------------------------------- /.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 | - sankeyraut 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 | -------------------------------------------------------------------------------- /.github/boring-cyborg.yml: -------------------------------------------------------------------------------- 1 | ##### Labeler ########################################################################################################## 2 | labelPRBasedOnFilePath: 3 | service/contracts: 4 | - Unicorn.Contracts 5 | service/properties: 6 | - Unicorn.Properties 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. -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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-java/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_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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@v1.2.5 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Java 2 | 3 | on: 4 | push: 5 | branches: [develop, main] 6 | paths: 7 | - 'unicorn_contracts/**' 8 | - 'unicorn_properties/**' 9 | - 'unicorn_web/**' 10 | pull_request: 11 | branches: [develop, main] 12 | paths: 13 | - 'unicorn_contracts/**' 14 | - 'unicorn_properties/**' 15 | - 'unicorn_web/**' 16 | 17 | defaults: 18 | run: 19 | working-directory: ./ 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 5 25 | strategy: 26 | max-parallel: 4 27 | matrix: 28 | # test against latest update of each major Java version, as well as specific updates of LTS versions: 29 | java: [17] 30 | name: Java ${{ matrix.java }} 31 | env: 32 | JAVA: ${{ matrix.java }} 33 | AWS_REGION: us-west-2 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Setup java 37 | uses: actions/setup-java@v4 38 | with: 39 | distribution: 'zulu' 40 | java-version: ${{ matrix.java }} 41 | cache: maven 42 | cache-dependency-path: '**/pom.xml' 43 | - name: Build with Maven 44 | run: mvn compile test -------------------------------------------------------------------------------- /.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 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'java' ] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | - name: Setup Java JDK 32 | uses: actions/setup-java@v3 33 | with: 34 | distribution: 'temurin' 35 | java-version: 17 36 | 37 | # Initializes the CodeQL tools for scanning. 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2 40 | with: 41 | languages: ${{ matrix.language }} 42 | 43 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 44 | # If this step fails, then you should remove it and run the build manually (see below) 45 | - name: Autobuild 46 | uses: github/codeql-action/autobuild@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2 47 | 48 | # ℹ️ Command-line programs to run using the OS shell. 49 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 50 | 51 | # If the Autobuild fails above, remove it and uncomment the following three lines. 52 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 53 | 54 | # - run: | 55 | # echo "Run, Build Application using script" 56 | # ./location_of_script_within_repo/buildscript.sh 57 | 58 | - name: Perform CodeQL Analysis 59 | uses: github/codeql-action/analyze@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2 60 | -------------------------------------------------------------------------------- /.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@v3 26 | - name: "Label PR based on title" 27 | uses: actions/github-script@v6 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 | -------------------------------------------------------------------------------- /.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@v3 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@v6 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@v3 24 | - name: "Label PR related issue for release" 25 | uses: actions/github-script@v6 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/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@v3 23 | - name: "Ensure related issue is present" 24 | uses: actions/github-script@v6 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@v3 40 | - name: "Ensure acknowledgement section is present" 41 | uses: actions/github-script@v6 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 | -------------------------------------------------------------------------------- /.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@v3 13 | - name: "Extract PR details" 14 | uses: actions/github-script@v6 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@v3 20 | with: 21 | name: pr 22 | path: pr.txt 23 | -------------------------------------------------------------------------------- /.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-java' 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@v3 57 | - name: "Download previously saved PR" 58 | uses: actions/github-script@v6 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | unicorn_properties/PropertyFunctions/target/* 2 | unicorn_contracts/ContractsFunction/target/** 3 | unicorn_web/PropertyFunctions/target/** 4 | **/.aws-sam/ 5 | .DS_Store** 6 | .vscode/settings.json 7 | buildall.sh 8 | deleteall.sh 9 | /.idea/** 10 | /cloudapp.iml 11 | /unicorn_properties/PropertyFunctions/PropertyService.iml 12 | /unicorn_web/PropertyFunctions/PropertyWeb.iml 13 | /unicorn_contracts/ContractsFunction/ContractsModule.iml 14 | **/cdk.out/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AWS Serverless Developer Experience Workshop Reference Architecture 2 | 3 | # AWS Serverless Developer Experience workshop reference architecture (Java) 4 | 5 | This repository contains the reference architecture for the AWS Serverless Developer Experience workshop. 6 | 7 | The AWS Serverless Developer Experience workshop provides you with an immersive experience as a serverless developer. The goal of this workshop is to provide you with hands-on experience building a serverless solution using the [**AWS Serverless Application Model (AWS SAM)**](https://aws.amazon.com/serverless/sam/) and **AWS SAM CLI**. 8 | 9 | Along the way, you will learn about principals of distributed event-driven architectures, messaging patterns, orchestration, and observability and how to apply them in code. You will explore exciting open-source tools, the core features of Powertools for AWS Lambda, and simplified CI/CD deployments supported by AWS SAM Pipelines. 10 | 11 | At the end of this workshop, you will be familiar with Serverless developer workflows and microservice composition using AWS SAM, Serverless development best practices, and applied event-driven architectures. 12 | 13 | ## Introducing the Unicorn Properties architecture 14 | 15 | ![AWS Serverless Developer Experience Workshop Reference Architecture](./docs/architecture.png) 16 | 17 | Our use case is based on a real estate company called **Unicorn Properties**. 18 | 19 | As a real estate agency, **Unicorn Properties** needs to manage the publishing of new property listings and sale contracts linked to individual properties, and provide a way for their customers to view approved property listings. 20 | 21 | To support their needs, Unicorn Properties have adopted a serverless, event-driven approach to designing their architecture. This architecture is centred around two primary domains: **Contracts** (managed by the Contracts Service) and **Properties** (managed by the Web and Properties Services). 22 | 23 | The **Unicorn Contracts** service (namespace: `Unicorn.Contracts`) is a simplified service that manages the contractual relationship between a seller of a property and Unicorn Properties. Contracts are drawn up that define the property for sale, the terms and conditions that Unicorn Properties sets, and how much it will cost the seller to engage the services of the agency. 24 | 25 | The **Unicorn Web** (namespace: `Unicorn.Web`) manages the details of a property listing to be published on the Unicorn Properties website. Every property listing has an address, a sale price, a description of the property, and some photos that members of the public can look at to get them interested in purchasing the property. Only properties that have been approved for publication can be made visible to the public. 26 | 27 | The **Unicorn Properties** service (namespace: `Unicorn.Properties`) approves a property listings. This service implements a workflow that checks for the existence of a contract, makes sure that the content and the images are safe to publish, and finally checks that the contract has been approved. We don’t want to publish a property until we have an approved contract! 28 | 29 | Have a go at building this architecture yourself! Head over to the [Serverless Developer Experience Workshop](https://catalog.workshops.aws/serverless-developer-experience) for more details. 30 | 31 | ## Credits 32 | 33 | Throughout this workshop we wanted to introduce you to some Open Source tools that can help you build serverless applications. This is not an exhaustive list, just a small selection of what we will be using in the workshop. 34 | 35 | Many thanks to all the AWS teams and community builders who have contributed to this list: 36 | 37 | | Tools | Description | Download / Installation Instructions | 38 | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | 39 | | cfn-lint | Validate AWS CloudFormation yaml/json templates against the AWS CloudFormation Resource Specification and additional checks. | https://github.com/aws-cloudformation/cfn-lint | 40 | | cfn-lint-serverless | Compilation of rules to validate infrastructure-as-code templates against recommended practices for serverless applications. | https://github.com/awslabs/serverless-rules | 41 | | @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 | 42 | | @mhlabs/evb-cli | Pattern generator and debugging tool for Amazon EventBridge | https://github.com/mhlabs/evb-cli | 43 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-java/64ebff3aadb850342cfa02b628570a1b1c77e105/docs/architecture.png -------------------------------------------------------------------------------- /docs/workshop_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-java/64ebff3aadb850342cfa02b628570a1b1c77e105/docs/workshop_logo.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | unicornproperties 6 | cloudapp 7 | 1.0 8 | pom 9 | 10 | 11 | unicorn_contracts/ContractsFunction 12 | unicorn_properties/PropertyFunctions 13 | unicorn_web/PropertyFunctions 14 | 15 | -------------------------------------------------------------------------------- /unicorn_contracts/.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | **/*~ 3 | **/.fuse_hidden* 4 | **/.directory 5 | **/.Trash-* 6 | **/.nfs* 7 | 8 | ### OSX ### 9 | **/*.DS_Store 10 | **/.AppleDouble 11 | **/.LSOverride 12 | **/.DocumentRevisions-V100 13 | **/.fseventsd 14 | **/.Spotlight-V100 15 | **/.TemporaryItems 16 | **/.Trashes 17 | **/.VolumeIcon.icns 18 | **/.com.apple.timemachine.donotpresent 19 | 20 | ### JetBrains IDEs ### 21 | **/*.iws 22 | **/.idea/ 23 | **/.idea_modules/ 24 | 25 | ### Python ### 26 | **/__pycache__/ 27 | **/*.py[cod] 28 | **/*$py.class 29 | **/.Python 30 | **/build/ 31 | **/develop-eggs/ 32 | **/dist/ 33 | **/downloads/ 34 | **/eggs/ 35 | **/.eggs/ 36 | **/parts/ 37 | **/sdist/ 38 | **/wheels/ 39 | **/*.egg 40 | **/*.egg-info/ 41 | **/.installed.cfg 42 | **/pip-log.txt 43 | **/pip-delete-this-directory.txt 44 | 45 | ### Unit test / coverage reports ### 46 | **/.cache 47 | **/.coverage 48 | **/.hypothesis/ 49 | **/.pytest_cache/ 50 | **/.tox/ 51 | **/*.cover 52 | **/coverage.xml 53 | **/htmlcov/ 54 | **/nosetests.xml 55 | 56 | ### pyenv ### 57 | **/.python-version 58 | 59 | ### Environments ### 60 | **/.env 61 | **/.venv 62 | **/env/ 63 | **/venv/ 64 | **/ENV/ 65 | **/env.bak/ 66 | **/venv.bak/ 67 | 68 | ### VisualStudioCode ### 69 | **/.vscode/ 70 | **/.history 71 | 72 | ### Windows ### 73 | # Windows thumbnail cache files 74 | **/Thumbs.db 75 | **/ehthumbs.db 76 | **/ehthumbs_vista.db 77 | **/Desktop.ini 78 | **/$RECYCLE.BIN/ 79 | **/*.lnk 80 | 81 | ### requirements.txt ### 82 | # We ignore Python's requirements.txt as we use Poetry instead 83 | **/requirements.txt 84 | **/.aws-sam 85 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Address.java: -------------------------------------------------------------------------------- 1 | package contracts.utils; 2 | 3 | public class Address { 4 | 5 | String country; 6 | String city; 7 | String street; 8 | int number; 9 | 10 | public String getCountry() { 11 | return this.country; 12 | } 13 | 14 | public void setCountry(String country) { 15 | this.country = country; 16 | } 17 | 18 | public String getCity() { 19 | return this.city; 20 | } 21 | 22 | public void setCity(String city) { 23 | this.city = city; 24 | } 25 | 26 | public String getStreet() { 27 | return this.street; 28 | } 29 | 30 | public void setStreet(String street) { 31 | this.street = street; 32 | } 33 | 34 | public int getNumber() { 35 | return this.number; 36 | } 37 | 38 | public void setNumber(int number) { 39 | this.number = number; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Contract.java: -------------------------------------------------------------------------------- 1 | package contracts.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAlias; 4 | 5 | public class Contract { 6 | 7 | Address address; 8 | @JsonAlias("property_id") 9 | String propertyId; 10 | @JsonAlias("contract_id") 11 | String contractId; 12 | @JsonAlias("seller_name") 13 | String sellerName; 14 | @JsonAlias("contract_status") 15 | ContractStatusEnum contractStatus; 16 | @JsonAlias("contract_created") 17 | Long contractCreated; 18 | @JsonAlias("contract_last_modified_on") 19 | Long contractLastModifiedOn; 20 | 21 | public Address getAddress() { 22 | return this.address; 23 | } 24 | 25 | public void setAddress(Address address) { 26 | this.address = address; 27 | } 28 | 29 | public String getPropertyId() { 30 | return this.propertyId; 31 | } 32 | 33 | public void setPropertyId(String propertyId) { 34 | this.propertyId = propertyId; 35 | } 36 | 37 | public String getContractId() { 38 | return this.contractId; 39 | } 40 | 41 | public void setContractId(String contractId) { 42 | this.contractId = contractId; 43 | } 44 | 45 | public String getSellerName() { 46 | return this.sellerName; 47 | } 48 | 49 | public void setSellerName(String sellerName) { 50 | this.sellerName = sellerName; 51 | } 52 | 53 | public ContractStatusEnum getContractStatus() { 54 | return this.contractStatus; 55 | } 56 | 57 | public void setContractStatus(ContractStatusEnum contractStatus) { 58 | this.contractStatus = contractStatus; 59 | } 60 | 61 | public Long getContractCreated() { 62 | return this.contractCreated; 63 | } 64 | 65 | public void setContractCreated(Long contractCreated) { 66 | this.contractCreated = contractCreated; 67 | } 68 | 69 | public Long getContractLastModifiedOn() { 70 | return this.contractLastModifiedOn; 71 | } 72 | 73 | public void setContractLastModifiedOn(Long contractLastModifiedOn) { 74 | this.contractLastModifiedOn = contractLastModifiedOn; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ContractStatusChangedEvent.java: -------------------------------------------------------------------------------- 1 | package contracts.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class ContractStatusChangedEvent { 6 | @JsonProperty("contract_last_modified_on") 7 | Long contractLastModifiedOn; 8 | @JsonProperty("contract_id") 9 | String contractId; 10 | @JsonProperty("property_id") 11 | String propertyId; 12 | @JsonProperty("contract_status") 13 | ContractStatusEnum contractStatus; 14 | 15 | public Long getContractLastModifiedOn() { 16 | return contractLastModifiedOn; 17 | } 18 | 19 | public void setContractLastModifiedOn(Long contractLastModifiedOn) { 20 | this.contractLastModifiedOn = contractLastModifiedOn; 21 | } 22 | 23 | public String getContractId() { 24 | return contractId; 25 | } 26 | 27 | public void setContractId(String contractId) { 28 | this.contractId = contractId; 29 | } 30 | 31 | public String getPropertyId() { 32 | return propertyId; 33 | } 34 | 35 | public void setPropertyId(String propertyId) { 36 | this.propertyId = propertyId; 37 | } 38 | 39 | public ContractStatusEnum getContractStatus() { 40 | return contractStatus; 41 | } 42 | 43 | public void setContractStatus(ContractStatusEnum contractStatus) { 44 | this.contractStatus = contractStatus; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ContractStatusEnum.java: -------------------------------------------------------------------------------- 1 | package contracts.utils; 2 | 3 | public enum ContractStatusEnum { 4 | 5 | DRAFT, APPROVED, CANCELLED, EXPIRED, CLOSED 6 | 7 | } 8 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ResponseParser.java: -------------------------------------------------------------------------------- 1 | package contracts.utils; 2 | 3 | import java.util.Map; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.JsonMappingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | 9 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 10 | 11 | public class ResponseParser { 12 | 13 | Contract parseResponse(Map queryResponse) 14 | throws JsonMappingException, JsonProcessingException { 15 | Contract response = new Contract(); 16 | ObjectMapper objectMapper = new ObjectMapper(); 17 | Address address = objectMapper.readValue(queryResponse.get("address").s(), Address.class); 18 | response.setAddress(address); 19 | response.setContractCreated( 20 | Long.valueOf(queryResponse.get("contract_created").s())); 21 | response.setContractId(queryResponse.get("contract_id").s()); 22 | response.setContractLastModifiedOn( 23 | Long.valueOf(queryResponse.get("contract_last_modified_on").s())); 24 | response.setContractStatus(ContractStatusEnum.valueOf(queryResponse.get("contract_status").s())); 25 | response.setPropertyId(queryResponse.get("property_id").s()); 26 | response.setSellerName(queryResponse.get("seller_name").s()); 27 | return response; 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/create_empty_dict_body_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "POST" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/create_missing_body_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{'hello':'world'}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "POST" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/create_valid_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{ \"address\": { \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 123 }, \"seller_name\": \"John Smith\", \"property_id\": \"usa/anytown/main-street/123\"}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "POST" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/create_wrong_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{\n \"add\": \"St.1 , Building 10\",\n \"seller\": \"John Smith\",\n \"property\": \"4781231c-bc30-4f30-8b30-7145f4dd1adb\"\n}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "POST" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/update_empty_dict_body_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "PUT" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/update_missing_body_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "PUT" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/update_valid_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{\n \"property_id\": \"usa/anytown/main-street/123\"}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "PUT" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/events/update_wrong_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", 5 | "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", 6 | "body": "{\n \"add\": \"St.1 , Building 10\",\n \"sell\": \"John Smith\",\n \"prop\": \"4781231c-bc30-4f30-8b30-7145f4dd1adb\",\n \"cont\": \"8155fdc5-ba1d-4e51-bcbf-7b417c01a4f3\"}", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1545082649183", 10 | "SenderId": "AIDAIENQZJOLO23YVJ4VO", 11 | "ApproximateFirstReceiveTimestamp": "1545082649185" 12 | }, 13 | "messageAttributes": { 14 | "HttpMethod": { 15 | "Type": "String", 16 | "Value": "PUT" 17 | } 18 | }, 19 | "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", 20 | "eventSource": "aws:sqs", 21 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", 22 | "awsRegion": "us-west-2" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_contracts/ContractsFunction/src/test/java/contracts/CreateContractTests.java: -------------------------------------------------------------------------------- 1 | package contracts; 2 | 3 | import static org.mockito.Mockito.mock; 4 | 5 | import com.amazonaws.services.lambda.runtime.Context; 6 | 7 | import com.amazonaws.services.lambda.runtime.events.SQSEvent; 8 | import com.amazonaws.services.lambda.runtime.tests.annotations.Event; 9 | 10 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 11 | 12 | import org.junit.Before; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class CreateContractTests { 19 | 20 | Context context; 21 | 22 | ContractEventHandler handler; 23 | 24 | DynamoDbClient client; 25 | 26 | @Before 27 | public void setUp() throws Exception { 28 | 29 | client = mock(DynamoDbClient.class); 30 | context = mock(Context.class); 31 | 32 | } 33 | 34 | @ParameterizedTest 35 | @Event(value = "src/test/events/create_valid_event.json", type = SQSEvent.class) 36 | public void validEvent(SQSEvent event) { 37 | DynamoDbClient client = mock(DynamoDbClient.class); 38 | handler = new ContractEventHandler(); 39 | handler.setDynamodbClient(client); 40 | handler.handleRequest(event, context); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /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 | Unicorn Contract manages the contractual relationship between the customers and the Unicorn Properties agency. It's primary function is to allow Unicorn Properties agents to create a new contract for a property listing, and to have the contract approved once it's ready. 8 | 9 | The architecture is fairly straight forward. An API exposes the create contract and update contract methods. This information is recorded in a Amazon DynamoDB table which will contain all latest information about the contract and it's status. 10 | 11 | Each time a new contract is created or updated, Unicorn Contracts publishes a `ContractStatusChanged` event to Amazon EventBridge signalling changes to the contract status. These events are consumed by **Unicorn Properties**, so it can track changes to contracts, without needing to take a direct dependency on Unicorn Contracts and it's database. 12 | 13 | Here is an example of an event that is published to EventBridge: 14 | 15 | ```json 16 | { 17 | "version": "0", 18 | "account": "123456789012", 19 | "region": "us-east-1", 20 | "detail-type": "ContractStatusChanged", 21 | "source": "unicorn.contracts", 22 | "time": "2022-08-14T22:06:31Z", 23 | "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", 24 | "resources": [], 25 | "detail": { 26 | "contract_updated_on": "10/08/2022 19:56:30", 27 | "ContractId": "617dda8c-e79b-406a-bc5b-3a4712f5e4d7", 28 | "PropertyId": "usa/anytown/main-street/111", 29 | "ContractStatus": "DRAFT" 30 | } 31 | } 32 | ``` 33 | 34 | ### Testing the APIs 35 | 36 | ```bash 37 | export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-contract --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text` 38 | 39 | curl --location --request POST "${API}contract" \ 40 | --header 'Content-Type: application/json' \ 41 | --data-raw '{ 42 | "address": { 43 | "country": "USA", 44 | "city": "Anytown", 45 | "street": "Main Street", 46 | "number": 111 47 | }, 48 | "seller_name": "John Doe", 49 | "property_id": "usa/anytown/main-street/111" 50 | }' 51 | 52 | 53 | curl --location --request PUT "${API}contract" \ 54 | --header 'Content-Type: application/json' \ 55 | --data-raw '{"property_id": "usa/anytown/main-street/111"}' | jq 56 | ``` 57 | -------------------------------------------------------------------------------- /unicorn_contracts/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | info: 3 | title: "Unicorn Contracts API" 4 | version: "1.0.0" 5 | description: Unicorn Properties Contract Service API 6 | paths: 7 | /contracts: 8 | post: 9 | requestBody: 10 | content: 11 | application/json: 12 | schema: 13 | $ref: "#/components/schemas/CreateContractModel" 14 | required: true 15 | responses: 16 | "200": 17 | description: "200 response" 18 | content: 19 | application/json: 20 | schema: 21 | $ref: "#/components/schemas/Empty" 22 | x-amazon-apigateway-request-validator: "Validate body" 23 | x-amazon-apigateway-integration: 24 | credentials: 25 | Fn::GetAtt: [UnicornContractsApiIntegrationRole, Arn] 26 | httpMethod: "POST" 27 | uri: 28 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" 29 | responses: 30 | default: 31 | statusCode: "200" 32 | responseTemplates: 33 | application/json: '{"message":"OK"}' 34 | requestParameters: 35 | integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" 36 | requestTemplates: 37 | application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" 38 | passthroughBehavior: "never" 39 | type: "aws" 40 | options: 41 | responses: 42 | "200": 43 | description: "200 response" 44 | headers: 45 | Access-Control-Allow-Origin: 46 | schema: 47 | type: "string" 48 | Access-Control-Allow-Methods: 49 | schema: 50 | type: "string" 51 | Access-Control-Allow-Headers: 52 | schema: 53 | type: "string" 54 | content: 55 | application/json: 56 | schema: 57 | $ref: "#/components/schemas/Empty" 58 | x-amazon-apigateway-integration: 59 | responses: 60 | default: 61 | statusCode: "200" 62 | responseParameters: 63 | method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" 64 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" 65 | method.response.header.Access-Control-Allow-Origin: "'*'" 66 | requestTemplates: 67 | application/json: '{"statusCode": 200}' 68 | passthroughBehavior: "when_no_match" 69 | type: "mock" 70 | put: 71 | requestBody: 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/UpdateContractModel" 76 | required: true 77 | responses: 78 | "200": 79 | description: "200 response" 80 | content: 81 | application/json: 82 | schema: 83 | $ref: "#/components/schemas/Empty" 84 | x-amazon-apigateway-request-validator: "Validate body" 85 | x-amazon-apigateway-integration: 86 | credentials: 87 | Fn::GetAtt: [UnicornContractsApiIntegrationRole, Arn] 88 | httpMethod: "POST" 89 | uri: 90 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${UnicornContractsIngestQueue.QueueName}" 91 | responses: 92 | default: 93 | statusCode: "200" 94 | responseTemplates: 95 | application/json: '{"message":"OK"}' 96 | requestParameters: 97 | integration.request.header.Content-Type: "'application/x-www-form-urlencoded'" 98 | requestTemplates: 99 | application/json: "Action=SendMessage&MessageBody=$input.body&MessageAttribute.1.Name=HttpMethod&MessageAttribute.1.Value.StringValue=$context.httpMethod&MessageAttribute.1.Value.DataType=String" 100 | passthroughBehavior: "never" 101 | type: "aws" 102 | components: 103 | schemas: 104 | CreateContractModel: 105 | required: 106 | - "property_id" 107 | - "seller_name" 108 | - "address" 109 | type: "object" 110 | properties: 111 | property_id: 112 | type: "string" 113 | seller_name: 114 | type: "string" 115 | address: 116 | required: 117 | - "city" 118 | - "country" 119 | - "number" 120 | - "street" 121 | type: "object" 122 | properties: 123 | country: 124 | type: "string" 125 | city: 126 | type: "string" 127 | street: 128 | type: "string" 129 | number: 130 | type: "integer" 131 | UpdateContractModel: 132 | required: 133 | - "property_id" 134 | type: "object" 135 | properties: 136 | $ref: "#/components/schemas/CreateContractModel/properties" 137 | # property_id: 138 | # type: "string" 139 | Empty: 140 | title: "Empty Schema" 141 | type: "object" 142 | x-amazon-apigateway-request-validators: 143 | Validate body: 144 | validateRequestParameters: false 145 | validateRequestBody: true 146 | -------------------------------------------------------------------------------- /unicorn_contracts/events/contract_status_changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "account": "123456789012", 4 | "region": "us-east-1", 5 | "detail-type": "Application Insights Problem Update", 6 | "source": "Unicorn.Contracts", 7 | "time": "2022-08-14T22:06:31Z", 8 | "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", 9 | "resources": [ 10 | ], 11 | "detail": { 12 | "contract_updated_on": "10/08/2022 19:56:30", 13 | "contract_id": 222, 14 | "property_id": "bbb", 15 | "contract_status": "DRAFT" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /unicorn_contracts/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"Address\":\"St.1 , Building 10\",\"SellerName\":\"John Smith\",\"PropertyId\":\"4781231c-bc30-4f30-8b30-7145f4dd1adb\"}", 3 | "resource": "/contract", 4 | "path": "/", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "requestTime": "09/Apr/2015:12:34:56 +0000", 42 | "requestTimeEpoch": 1428582896000, 43 | "identity": { 44 | "cognitoIdentityPoolId": null, 45 | "accountId": null, 46 | "cognitoIdentityId": null, 47 | "caller": null, 48 | "accessKey": null, 49 | "sourceIp": "127.0.0.1", 50 | "cognitoAuthenticationType": null, 51 | "cognitoAuthenticationProvider": null, 52 | "userArn": null, 53 | "userAgent": "Custom User Agent String", 54 | "user": null 55 | }, 56 | "path": "/prod/hello", 57 | "resourcePath": "/hello", 58 | "httpMethod": "POST", 59 | "apiId": "1234567890", 60 | "protocol": "HTTP/1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /unicorn_contracts/events/put_events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "Unicorn.Contracts", 4 | "Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": 111,\"property_id\":\"aaa\",\"contract_status\":\"DRAFT\"}", 5 | "DetailType": "ContractStatusChanged", 6 | "EventBusName": "Dev-UnicornPropertiesEventBus" 7 | }, 8 | { 9 | "Source": "Unicorn.Contracts", 10 | "Detail": "{\"contract_updated_on\":\"10/08/2022 19:56:30\",\"contract_id\":222,\"property_id\":\"bbb\",\"contract_status\":\"DRAFT\"}", 11 | "DetailType": "ContractStatusChanged", 12 | "EventBusName": "Dev-UnicornPropertiesEventBus" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /unicorn_contracts/integration/ContractStatusChanged.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "ContractStatusChanged" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "AWSEvent": { 11 | "type": "object", 12 | "required": [ 13 | "detail-type", 14 | "resources", 15 | "detail", 16 | "id", 17 | "source", 18 | "time", 19 | "region", 20 | "version", 21 | "account" 22 | ], 23 | "x-amazon-events-detail-type": "ContractStatusChanged", 24 | "x-amazon-events-source": "unicorn.contracts", 25 | "properties": { 26 | "detail": { 27 | "$ref": "#/components/schemas/ContractStatusChanged" 28 | }, 29 | "account": { 30 | "type": "string" 31 | }, 32 | "detail-type": { 33 | "type": "string" 34 | }, 35 | "id": { 36 | "type": "string" 37 | }, 38 | "region": { 39 | "type": "string" 40 | }, 41 | "resources": { 42 | "type": "array", 43 | "items": { 44 | "type": "object" 45 | } 46 | }, 47 | "source": { 48 | "type": "string" 49 | }, 50 | "time": { 51 | "type": "string", 52 | "format": "date-time" 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "ContractStatusChanged": { 60 | "type": "object", 61 | "required": [ 62 | "contract_last_modified_on", 63 | "contract_id", 64 | "contract_status", 65 | "property_id" 66 | ], 67 | "properties": { 68 | "contract_id": { 69 | "type": "string" 70 | }, 71 | "contract_last_modified_on": { 72 | "type": "string", 73 | "format": "date-time" 74 | }, 75 | "contract_status": { 76 | "type": "string" 77 | }, 78 | "property_id": { 79 | "type": "string" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /unicorn_contracts/integration/event-schemas.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 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by Unicorn Contracts Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | EventRegistry: 19 | Type: AWS::EventSchemas::Registry 20 | Properties: 21 | Description: 'Event schemas for Unicorn Contracts' 22 | RegistryName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}-${Stage}" 24 | 25 | EventRegistryPolicy: 26 | Type: AWS::EventSchemas::RegistryPolicy 27 | Properties: 28 | RegistryName: 29 | Fn::GetAtt: EventRegistry.RegistryName 30 | Policy: 31 | Version: "2012-10-17" 32 | Statement: 33 | - Sid: AllowExternalServices 34 | Effect: Allow 35 | Principal: 36 | AWS: 37 | - Ref: AWS::AccountId 38 | Action: 39 | - schemas:DescribeCodeBinding 40 | - schemas:DescribeRegistry 41 | - schemas:DescribeSchema 42 | - schemas:GetCodeBindingSource 43 | - schemas:ListSchemas 44 | - schemas:ListSchemaVersions 45 | - schemas:SearchSchemas 46 | Resource: 47 | - Fn::GetAtt: EventRegistry.RegistryArn 48 | - Fn::Sub: "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" 49 | 50 | ContractStatusChangedEventSchema: 51 | Type: AWS::EventSchemas::Schema 52 | Properties: 53 | Type: 'OpenApi3' 54 | RegistryName: 55 | Fn::GetAtt: EventRegistry.RegistryName 56 | SchemaName: 57 | Fn::Sub: "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}@ContractStatusChanged" 58 | Description: 'The schema for a request to publish a property' 59 | Content: 60 | Fn::Sub: | 61 | { 62 | "openapi": "3.0.0", 63 | "info": { 64 | "version": "1.0.0", 65 | "title": "ContractStatusChanged" 66 | }, 67 | "paths": {}, 68 | "components": { 69 | "schemas": { 70 | "AWSEvent": { 71 | "type": "object", 72 | "required": [ 73 | "detail-type", 74 | "resources", 75 | "detail", 76 | "id", 77 | "source", 78 | "time", 79 | "region", 80 | "version", 81 | "account" 82 | ], 83 | "x-amazon-events-detail-type": "ContractStatusChanged", 84 | "x-amazon-events-source": "${EventRegistry.RegistryName}", 85 | "properties": { 86 | "detail": { 87 | "$ref": "#/components/schemas/ContractStatusChanged" 88 | }, 89 | "account": { 90 | "type": "string" 91 | }, 92 | "detail-type": { 93 | "type": "string" 94 | }, 95 | "id": { 96 | "type": "string" 97 | }, 98 | "region": { 99 | "type": "string" 100 | }, 101 | "resources": { 102 | "type": "array", 103 | "items": { 104 | "type": "object" 105 | } 106 | }, 107 | "source": { 108 | "type": "string" 109 | }, 110 | "time": { 111 | "type": "string", 112 | "format": "date-time" 113 | }, 114 | "version": { 115 | "type": "string" 116 | } 117 | } 118 | }, 119 | "ContractStatusChanged": { 120 | "type": "object", 121 | "required": [ 122 | "contract_last_modified_on", 123 | "contract_id", 124 | "contract_status", 125 | "property_id" 126 | ], 127 | "properties": { 128 | "contract_id": { 129 | "type": "string" 130 | }, 131 | "contract_last_modified_on": { 132 | "type": "string", 133 | "format": "date-time" 134 | }, 135 | "contract_status": { 136 | "type": "string" 137 | }, 138 | "property_id": { 139 | "type": "string" 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /unicorn_contracts/integration/subscriber-policies.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 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by the Contracts Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | # This policy defines who can create rules on the event bus. Only principals subscribing to 19 | # Contracts Service events can create rule on the bus. No rules without a defined source. 20 | CrossServiceCreateRulePolicy: 21 | Type: AWS::Events::EventBusPolicy 22 | Properties: 23 | EventBusName: 24 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" 25 | StatementId: 26 | Fn::Sub: "OnlyRulesForContractServiceEvents-${Stage}" 27 | Statement: 28 | Effect: Allow 29 | Principal: 30 | AWS: 31 | Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 32 | Action: 33 | - events:PutRule 34 | - events:DeleteRule 35 | - events:DescribeRule 36 | - events:DisableRule 37 | - events:EnableRule 38 | - events:PutTargets 39 | - events:RemoveTargets 40 | Resource: 41 | - Fn::Sub: 42 | - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* 43 | - eventBusName: 44 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" 45 | Condition: 46 | StringEqualsIfExists: 47 | "events:creatorAccount": "${aws:PrincipalAccount}" 48 | StringEquals: 49 | "events:source": 50 | - "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}" 51 | "Null": 52 | "events:source": "false" 53 | -------------------------------------------------------------------------------- /unicorn_contracts/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" -------------------------------------------------------------------------------- /unicorn_properties/.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | **/*~ 3 | **/.fuse_hidden* 4 | **/.directory 5 | **/.Trash-* 6 | **/.nfs* 7 | 8 | ### OSX ### 9 | **/*.DS_Store 10 | **/.AppleDouble 11 | **/.LSOverride 12 | **/.DocumentRevisions-V100 13 | **/.fseventsd 14 | **/.Spotlight-V100 15 | **/.TemporaryItems 16 | **/.Trashes 17 | **/.VolumeIcon.icns 18 | **/.com.apple.timemachine.donotpresent 19 | 20 | ### JetBrains IDEs ### 21 | **/*.iws 22 | **/.idea/ 23 | **/.idea_modules/ 24 | 25 | ### Python ### 26 | **/__pycache__/ 27 | **/*.py[cod] 28 | **/*$py.class 29 | **/.Python 30 | **/build/ 31 | **/develop-eggs/ 32 | **/dist/ 33 | **/downloads/ 34 | **/eggs/ 35 | **/.eggs/ 36 | **/parts/ 37 | **/sdist/ 38 | **/wheels/ 39 | **/*.egg 40 | **/*.egg-info/ 41 | **/.installed.cfg 42 | **/pip-log.txt 43 | **/pip-delete-this-directory.txt 44 | 45 | ### Unit test / coverage reports ### 46 | **/.cache 47 | **/.coverage 48 | **/.hypothesis/ 49 | **/.pytest_cache/ 50 | **/.tox/ 51 | **/*.cover 52 | **/coverage.xml 53 | **/htmlcov/ 54 | **/nosetests.xml 55 | 56 | ### pyenv ### 57 | **/.python-version 58 | 59 | ### Environments ### 60 | **/.env 61 | **/.venv 62 | **/env/ 63 | **/venv/ 64 | **/ENV/ 65 | **/env.bak/ 66 | **/venv.bak/ 67 | 68 | ### VisualStudioCode ### 69 | **/.vscode/ 70 | **/.history 71 | 72 | ### Windows ### 73 | # Windows thumbnail cache files 74 | **/Thumbs.db 75 | **/ehthumbs.db 76 | **/ehthumbs_vista.db 77 | **/Desktop.ini 78 | **/$RECYCLE.BIN/ 79 | **/*.lnk 80 | 81 | ### requirements.txt ### 82 | # We ignore Python's requirements.txt as we use Poetry instead 83 | **/requirements.txt 84 | **/.aws-sam 85 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusChangedHandlerFunction.java: -------------------------------------------------------------------------------- 1 | package properties; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.io.OutputStreamWriter; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import com.amazonaws.services.lambda.runtime.Context; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | 14 | import org.apache.logging.log4j.LogManager; 15 | import org.apache.logging.log4j.Logger; 16 | 17 | import schema.unicorn_contracts.contractstatuschanged.Event; 18 | import schema.unicorn_contracts.contractstatuschanged.ContractStatusChanged; 19 | import schema.unicorn_contracts.contractstatuschanged.marshaller.Marshaller; 20 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 21 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 22 | import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; 23 | import software.amazon.lambda.powertools.logging.Logging; 24 | import software.amazon.lambda.powertools.metrics.Metrics; 25 | import software.amazon.lambda.powertools.tracing.Tracing; 26 | 27 | /** 28 | * Lambda handler to update the contract status change 29 | */ 30 | public class ContractStatusChangedHandlerFunction { 31 | 32 | Logger logger = LogManager.getLogger(); 33 | 34 | final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE"); 35 | 36 | ObjectMapper objectMapper = new ObjectMapper(); 37 | 38 | DynamoDbClient dynamodbClient = DynamoDbClient.builder() 39 | .build(); 40 | 41 | /** 42 | * 43 | * @param inputStream 44 | * @param outputStream 45 | * @param context 46 | * @return 47 | * @throws IOException 48 | * 49 | */ 50 | @Tracing 51 | @Metrics(captureColdStart = true) 52 | @Logging(logEvent = true) 53 | public void handleRequest(InputStream inputStream, OutputStream outputStream, 54 | Context context) throws IOException { 55 | 56 | // deseralised and save contract status change in dynamodb table 57 | 58 | Event event = Marshaller.unmarshal(inputStream, 59 | Event.class); 60 | // save to database 61 | ContractStatusChanged contractStatusChanged = event.getDetail(); 62 | saveContractStatus(contractStatusChanged.getPropertyId(), contractStatusChanged.getContractStatus(), 63 | contractStatusChanged.getContractId(), 64 | contractStatusChanged.getContractLastModifiedOn()); 65 | 66 | OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); 67 | writer.write(objectMapper.writeValueAsString(event.getDetail())); 68 | writer.close(); 69 | } 70 | 71 | @Tracing 72 | void saveContractStatus(String propertyId, 73 | String contractStatus, String contractId, Long contractLastModifiedOn) { 74 | Map key = new HashMap(); 75 | AttributeValue keyvalue = AttributeValue.fromS(propertyId); 76 | key.put("property_id", keyvalue); 77 | 78 | Map expressionAttributeValues = new HashMap(); 79 | expressionAttributeValues.put(":t", AttributeValue.fromS(contractStatus)); 80 | expressionAttributeValues.put(":c", AttributeValue.fromS(contractId)); 81 | expressionAttributeValues.put(":m", AttributeValue 82 | .fromN(String.valueOf(contractLastModifiedOn))); 83 | 84 | UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() 85 | .key(key) 86 | .tableName(TABLE_NAME) 87 | .updateExpression( 88 | "set contract_status=:t, contract_last_modified_on=:m, contract_id=:c") 89 | .expressionAttributeValues(expressionAttributeValues) 90 | .build(); 91 | 92 | dynamodbClient.updateItem(updateItemRequest); 93 | } 94 | 95 | public void setDynamodbClient(DynamoDbClient dynamodbClient) { 96 | this.dynamodbClient = dynamodbClient; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusNotFoundException.java: -------------------------------------------------------------------------------- 1 | package properties; 2 | 3 | public class ContractStatusNotFoundException extends Exception { 4 | public ContractStatusNotFoundException(String errorMessage) { 5 | super(errorMessage); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/properties/PropertiesApprovalSyncFunction.java: -------------------------------------------------------------------------------- 1 | package properties; 2 | 3 | import com.amazonaws.services.lambda.runtime.Context; 4 | import com.amazonaws.services.lambda.runtime.RequestHandler; 5 | import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; 6 | import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; 7 | import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue; 8 | import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord; 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | 12 | import org.apache.logging.log4j.LogManager; 13 | import org.apache.logging.log4j.Logger; 14 | 15 | import properties.dao.ContractStatus; 16 | import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; 17 | import software.amazon.awssdk.services.sfn.SfnAsyncClient; 18 | import software.amazon.awssdk.services.sfn.model.SendTaskSuccessRequest; 19 | 20 | import java.io.Serializable; 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | import software.amazon.lambda.powertools.logging.Logging; 27 | import software.amazon.lambda.powertools.metrics.Metrics; 28 | import software.amazon.lambda.powertools.tracing.Tracing; 29 | 30 | public class PropertiesApprovalSyncFunction implements RequestHandler { 31 | 32 | Logger logger = LogManager.getLogger(); 33 | SfnAsyncClient snfClient = SfnAsyncClient.builder() 34 | .httpClientBuilder(NettyNioAsyncHttpClient.builder() 35 | .maxConcurrency(100) 36 | .maxPendingConnectionAcquires(10_000)) 37 | .build(); 38 | 39 | @Tracing 40 | @Metrics(captureColdStart = true) 41 | @Logging(logEvent = true) 42 | public StreamsEventResponse handleRequest(DynamodbEvent input, Context context) { 43 | 44 | List batchItemFailures = new ArrayList<>(); 45 | String curRecordSequenceNumber = ""; 46 | 47 | for (DynamodbEvent.DynamodbStreamRecord dynamodbStreamRecord : input.getRecords()) { 48 | try { 49 | // Process your record 50 | 51 | StreamRecord dynamodbRecord = dynamodbStreamRecord.getDynamodb(); 52 | curRecordSequenceNumber = dynamodbRecord.getSequenceNumber(); 53 | Map newImage = dynamodbRecord.getNewImage(); 54 | Map oldImage = dynamodbRecord.getOldImage(); 55 | if (oldImage == null) { 56 | oldImage = new HashMap(); 57 | } 58 | if (newImage == null) { 59 | logger.debug("New image is null. Hence return empty stream response"); 60 | return new StreamsEventResponse(); 61 | } 62 | // if there is no token do nothing 63 | if (newImage.get("sfn_wait_approved_task_token") == null 64 | && oldImage.get("sfn_wait_approved_task_token") == null) { 65 | logger.debug("No task token in both the images. Hence return empty stream response"); 66 | return new StreamsEventResponse(); 67 | } 68 | 69 | // if contract status is approved, send the task token 70 | 71 | if (!newImage.get("contract_status").getS().equalsIgnoreCase("APPROVED")) { 72 | logger.debug("Contract status for property is not APPROVED : " + 73 | newImage.get("property_id").getS()); 74 | return new StreamsEventResponse(); 75 | } 76 | logger.debug("Contract status for property is APPROVED : " + 77 | newImage.get("property_id").getS()); 78 | 79 | // send task successful token 80 | taskSuccessful(newImage.get("sfn_wait_approved_task_token").getS(), newImage); 81 | 82 | } catch (Exception e) { 83 | /* 84 | * Since we are working with streams, we can return the failed item immediately. 85 | * Lambda will immediately begin to retry processing from this failed item 86 | * onwards. 87 | */ 88 | batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(curRecordSequenceNumber)); 89 | return new StreamsEventResponse(batchItemFailures); 90 | } 91 | } 92 | 93 | return new StreamsEventResponse(); 94 | } 95 | 96 | private void taskSuccessful(String s, Map item) throws JsonProcessingException { 97 | // create the json structure and send the token 98 | ObjectMapper mapper = new ObjectMapper(); 99 | ContractStatus contractStatus = new ContractStatus(); 100 | contractStatus.setContract_id(item.get("contract_id").getS()); 101 | contractStatus.setContract_status(item.get("contract_status").getS()); 102 | contractStatus.setProperty_id(item.get("property_id").getS()); 103 | contractStatus.setSfn_wait_approved_task_token(item.get("sfn_wait_approved_task_token").getS()); 104 | String taskResult = mapper.writeValueAsString(contractStatus); 105 | 106 | SendTaskSuccessRequest request = SendTaskSuccessRequest.builder() 107 | .taskToken(contractStatus.getSfn_wait_approved_task_token()) 108 | .output(taskResult) 109 | .build(); 110 | snfClient.sendTaskSuccess(request).join(); 111 | 112 | } 113 | } -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/properties/WaitForContractApprovalFunction.java: -------------------------------------------------------------------------------- 1 | package properties; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.io.OutputStreamWriter; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import com.amazonaws.services.lambda.runtime.Context; 12 | import com.fasterxml.jackson.databind.JsonNode; 13 | import com.fasterxml.jackson.databind.ObjectMapper; 14 | 15 | import org.apache.logging.log4j.LogManager; 16 | import org.apache.logging.log4j.Logger; 17 | 18 | import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; 19 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 20 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 21 | import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; 22 | import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; 23 | import software.amazon.lambda.powertools.logging.Logging; 24 | import software.amazon.lambda.powertools.metrics.Metrics; 25 | import software.amazon.lambda.powertools.tracing.Tracing; 26 | 27 | /** 28 | * Lambda handler to update the contract status change 29 | */ 30 | public class WaitForContractApprovalFunction { 31 | 32 | Logger logger = LogManager.getLogger(); 33 | 34 | final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE"); 35 | 36 | DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() 37 | .httpClientBuilder(NettyNioAsyncHttpClient.builder() 38 | .maxConcurrency(100) 39 | .maxPendingConnectionAcquires(10_000)) 40 | .build(); 41 | 42 | @Tracing 43 | @Metrics(captureColdStart = true) 44 | @Logging(logEvent = true) 45 | public void handleRequest(InputStream inputStream, OutputStream outputStream, 46 | Context context) throws IOException, ContractStatusNotFoundException { 47 | 48 | // deseralised to contract status 49 | ObjectMapper objectMapper = new ObjectMapper(); 50 | String srtInput = new String(inputStream.readAllBytes()); 51 | JsonNode event = objectMapper.readTree(srtInput); 52 | String propertyId = event.get("Input").get("property_id").asText(); 53 | String taskToken = event.get("TaskToken").asText(); 54 | 55 | logger.info("task Token : ", taskToken); 56 | logger.info("Property Id : ", propertyId); 57 | 58 | // get contract status 59 | Map dynamodbItem = getContractStatus(propertyId); 60 | updateTokenAndPauseExecution(taskToken, dynamodbItem.get("property_id").s()); 61 | 62 | String responseString = event.get("Input").asText(); 63 | OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); 64 | logger.debug(responseString); 65 | writer.write(responseString); 66 | writer.close(); 67 | 68 | } 69 | 70 | private void updateTokenAndPauseExecution(String taskToken, String propertyId) { 71 | Map key = new HashMap(); 72 | AttributeValue keyvalue = AttributeValue.fromS(propertyId); 73 | key.put("property_id", keyvalue); 74 | 75 | Map expressionAttributeValues = new HashMap(); 76 | expressionAttributeValues.put(":g", AttributeValue.fromS(taskToken)); 77 | 78 | UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() 79 | .key(key) 80 | .tableName(TABLE_NAME) 81 | .updateExpression( 82 | "set sfn_wait_approved_task_token = :g") 83 | .expressionAttributeValues(expressionAttributeValues) 84 | .build(); 85 | dynamodbClient.updateItem(updateItemRequest).join(); 86 | } 87 | 88 | private Map getContractStatus(String propertyId) 89 | throws ContractStatusNotFoundException { 90 | HashMap keyToGet = new HashMap(); 91 | 92 | keyToGet.put("property_id", AttributeValue.builder() 93 | .s(propertyId).build()); 94 | 95 | GetItemRequest request = GetItemRequest.builder() 96 | .key(keyToGet) 97 | .tableName(TABLE_NAME) 98 | .build(); 99 | Map returnvalue = null; 100 | try { 101 | returnvalue = dynamodbClient.getItem(request).join().item(); 102 | } catch (Exception exception) { 103 | throw new ContractStatusNotFoundException(exception.getLocalizedMessage()); 104 | } 105 | 106 | return returnvalue; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/properties/dao/ContractStatus.java: -------------------------------------------------------------------------------- 1 | package properties.dao; 2 | 3 | public class ContractStatus { 4 | String contract_id; 5 | String contract_status; 6 | String property_id; 7 | String sfn_wait_approved_task_token; 8 | 9 | @Override 10 | public String toString() { 11 | return "Property [contract_id=" + contract_id + ", contract_status=" + contract_status + ", property_id=" 12 | + property_id + ", sfn_wait_approved_task_token=" + sfn_wait_approved_task_token + "]"; 13 | } 14 | 15 | public String getContract_id() { 16 | return contract_id; 17 | } 18 | 19 | public void setContract_id(String contract_id) { 20 | this.contract_id = contract_id; 21 | } 22 | 23 | public String getContract_status() { 24 | return contract_status; 25 | } 26 | 27 | public void setContract_status(String contract_status) { 28 | this.contract_status = contract_status; 29 | } 30 | 31 | public String getProperty_id() { 32 | return property_id; 33 | } 34 | 35 | public void setProperty_id(String property_id) { 36 | this.property_id = property_id; 37 | } 38 | 39 | public String getSfn_wait_approved_task_token() { 40 | return sfn_wait_approved_task_token; 41 | } 42 | 43 | public void setSfn_wait_approved_task_token(String sfn_wait_approved_task_token) { 44 | this.sfn_wait_approved_task_token = sfn_wait_approved_task_token; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java: -------------------------------------------------------------------------------- 1 | package schema.unicorn_contracts.contractstatuschanged; 2 | 3 | import java.util.Objects; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonValue; 7 | import java.io.Serializable; 8 | 9 | public class ContractStatusChanged implements Serializable { 10 | private static final long serialVersionUID = 1L; 11 | 12 | @JsonProperty("contract_id") 13 | private String contractId = null; 14 | 15 | @JsonProperty("contract_last_modified_on") 16 | private Long contractLastModifiedOn = null; 17 | 18 | @JsonProperty("contract_status") 19 | private String contractStatus = null; 20 | 21 | @JsonProperty("property_id") 22 | private String propertyId = null; 23 | 24 | public ContractStatusChanged contractId(String contractId) { 25 | this.contractId = contractId; 26 | return this; 27 | } 28 | 29 | 30 | public String getContractId() { 31 | return contractId; 32 | } 33 | 34 | public void setContractId(String contractId) { 35 | this.contractId = contractId; 36 | } 37 | 38 | public ContractStatusChanged contractLastModifiedOn(Long contractLastModifiedOn) { 39 | this.contractLastModifiedOn = contractLastModifiedOn; 40 | return this; 41 | } 42 | 43 | 44 | public Long getContractLastModifiedOn() { 45 | return contractLastModifiedOn; 46 | } 47 | 48 | public void setContractLastModifiedOn(Long contractLastModifiedOn) { 49 | this.contractLastModifiedOn = contractLastModifiedOn; 50 | } 51 | 52 | public ContractStatusChanged contractStatus(String contractStatus) { 53 | this.contractStatus = contractStatus; 54 | return this; 55 | } 56 | 57 | 58 | public String getContractStatus() { 59 | return contractStatus; 60 | } 61 | 62 | public void setContractStatus(String contractStatus) { 63 | this.contractStatus = contractStatus; 64 | } 65 | 66 | public ContractStatusChanged propertyId(String propertyId) { 67 | this.propertyId = propertyId; 68 | return this; 69 | } 70 | 71 | 72 | public String getPropertyId() { 73 | return propertyId; 74 | } 75 | 76 | public void setPropertyId(String propertyId) { 77 | this.propertyId = propertyId; 78 | } 79 | 80 | @Override 81 | public boolean equals(java.lang.Object o) { 82 | if (this == o) { 83 | return true; 84 | } 85 | if (o == null || getClass() != o.getClass()) { 86 | return false; 87 | } 88 | ContractStatusChanged contractStatusChanged = (ContractStatusChanged) o; 89 | return Objects.equals(this.contractId, contractStatusChanged.contractId) && 90 | Objects.equals(this.contractLastModifiedOn, contractStatusChanged.contractLastModifiedOn) && 91 | Objects.equals(this.contractStatus, contractStatusChanged.contractStatus) && 92 | Objects.equals(this.propertyId, contractStatusChanged.propertyId); 93 | } 94 | 95 | @Override 96 | public int hashCode() { 97 | return java.util.Objects.hash(contractId, contractLastModifiedOn, contractStatus, propertyId); 98 | } 99 | 100 | 101 | @Override 102 | public String toString() { 103 | StringBuilder sb = new StringBuilder(); 104 | sb.append("class ContractStatusChanged {\n"); 105 | 106 | sb.append(" contractId: ").append(toIndentedString(contractId)).append("\n"); 107 | sb.append(" contractLastModifiedOn: ").append(toIndentedString(contractLastModifiedOn)).append("\n"); 108 | sb.append(" contractStatus: ").append(toIndentedString(contractStatus)).append("\n"); 109 | sb.append(" propertyId: ").append(toIndentedString(propertyId)).append("\n"); 110 | sb.append("}"); 111 | return sb.toString(); 112 | } 113 | 114 | private String toIndentedString(java.lang.Object o) { 115 | if (o == null) { 116 | return "null"; 117 | } 118 | return o.toString().replace("\n", "\n "); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java: -------------------------------------------------------------------------------- 1 | package schema.unicorn_contracts.contractstatuschanged; 2 | 3 | import java.util.Objects; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonValue; 7 | import java.util.ArrayList; 8 | import java.util.Date; 9 | import java.util.List; 10 | import schema.unicorn_contracts.contractstatuschanged.ContractStatusChanged; 11 | import java.io.Serializable; 12 | 13 | public class Event implements Serializable { 14 | private static final long serialVersionUID = 1L; 15 | 16 | @JsonProperty("account") 17 | private String account = null; 18 | 19 | @JsonProperty("detail") 20 | private ContractStatusChanged detail = null; 21 | 22 | @JsonProperty("detail-type") 23 | private String detailType = null; 24 | 25 | @JsonProperty("id") 26 | private String id = null; 27 | 28 | @JsonProperty("region") 29 | private String region = null; 30 | 31 | @JsonProperty("resources") 32 | private List resources = null; 33 | 34 | @JsonProperty("source") 35 | private String source = null; 36 | 37 | @JsonProperty("time") 38 | private Date time = null; 39 | 40 | @JsonProperty("version") 41 | private String version = null; 42 | 43 | public Event account(String account) { 44 | this.account = account; 45 | return this; 46 | } 47 | 48 | 49 | public String getAccount() { 50 | return account; 51 | } 52 | 53 | public void setAccount(String account) { 54 | this.account = account; 55 | } 56 | 57 | public Event detail(ContractStatusChanged detail) { 58 | this.detail = detail; 59 | return this; 60 | } 61 | 62 | 63 | public ContractStatusChanged getDetail() { 64 | return detail; 65 | } 66 | 67 | public void setDetail(ContractStatusChanged detail) { 68 | this.detail = detail; 69 | } 70 | 71 | public Event detailType(String detailType) { 72 | this.detailType = detailType; 73 | return this; 74 | } 75 | 76 | 77 | public String getDetailType() { 78 | return detailType; 79 | } 80 | 81 | public void setDetailType(String detailType) { 82 | this.detailType = detailType; 83 | } 84 | 85 | public Event id(String id) { 86 | this.id = id; 87 | return this; 88 | } 89 | 90 | 91 | public String getId() { 92 | return id; 93 | } 94 | 95 | public void setId(String id) { 96 | this.id = id; 97 | } 98 | 99 | public Event region(String region) { 100 | this.region = region; 101 | return this; 102 | } 103 | 104 | 105 | public String getRegion() { 106 | return region; 107 | } 108 | 109 | public void setRegion(String region) { 110 | this.region = region; 111 | } 112 | 113 | public Event resources(List resources) { 114 | this.resources = resources; 115 | return this; 116 | } 117 | public Event addResourcesItem(Object resourcesItem) { 118 | if (this.resources == null) { 119 | this.resources = new ArrayList(); 120 | } 121 | this.resources.add(resourcesItem); 122 | return this; 123 | } 124 | 125 | public List getResources() { 126 | return resources; 127 | } 128 | 129 | public void setResources(List resources) { 130 | this.resources = resources; 131 | } 132 | 133 | public Event source(String source) { 134 | this.source = source; 135 | return this; 136 | } 137 | 138 | 139 | public String getSource() { 140 | return source; 141 | } 142 | 143 | public void setSource(String source) { 144 | this.source = source; 145 | } 146 | 147 | public Event time(Date time) { 148 | this.time = time; 149 | return this; 150 | } 151 | 152 | 153 | public Date getTime() { 154 | return time; 155 | } 156 | 157 | public void setTime(Date time) { 158 | this.time = time; 159 | } 160 | 161 | public Event version(String version) { 162 | this.version = version; 163 | return this; 164 | } 165 | 166 | 167 | public String getVersion() { 168 | return version; 169 | } 170 | 171 | public void setVersion(String version) { 172 | this.version = version; 173 | } 174 | 175 | @Override 176 | public boolean equals(java.lang.Object o) { 177 | if (this == o) { 178 | return true; 179 | } 180 | if (o == null || getClass() != o.getClass()) { 181 | return false; 182 | } 183 | Event event = (Event) o; 184 | return Objects.equals(this.account, event.account) && 185 | Objects.equals(this.detail, event.detail) && 186 | Objects.equals(this.detailType, event.detailType) && 187 | Objects.equals(this.id, event.id) && 188 | Objects.equals(this.region, event.region) && 189 | Objects.equals(this.resources, event.resources) && 190 | Objects.equals(this.source, event.source) && 191 | Objects.equals(this.time, event.time) && 192 | Objects.equals(this.version, event.version); 193 | } 194 | 195 | @Override 196 | public int hashCode() { 197 | return java.util.Objects.hash(account, detail, detailType, id, region, resources, source, time, version); 198 | } 199 | 200 | 201 | @Override 202 | public String toString() { 203 | StringBuilder sb = new StringBuilder(); 204 | sb.append("class Event {\n"); 205 | 206 | sb.append(" account: ").append(toIndentedString(account)).append("\n"); 207 | sb.append(" detail: ").append(toIndentedString(detail)).append("\n"); 208 | sb.append(" detailType: ").append(toIndentedString(detailType)).append("\n"); 209 | sb.append(" id: ").append(toIndentedString(id)).append("\n"); 210 | sb.append(" region: ").append(toIndentedString(region)).append("\n"); 211 | sb.append(" resources: ").append(toIndentedString(resources)).append("\n"); 212 | sb.append(" source: ").append(toIndentedString(source)).append("\n"); 213 | sb.append(" time: ").append(toIndentedString(time)).append("\n"); 214 | sb.append(" version: ").append(toIndentedString(version)).append("\n"); 215 | sb.append("}"); 216 | return sb.toString(); 217 | } 218 | 219 | private String toIndentedString(java.lang.Object o) { 220 | if (o == null) { 221 | return "null"; 222 | } 223 | return o.toString().replace("\n", "\n "); 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java: -------------------------------------------------------------------------------- 1 | package schema.unicorn_contracts.contractstatuschanged.marshaller; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.type.TypeFactory; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | 11 | public class Marshaller { 12 | 13 | private static final ObjectMapper MAPPER = createObjectMapper(); 14 | 15 | public static void marshal(OutputStream output, T value) throws IOException { 16 | MAPPER.writeValue(output, value); 17 | } 18 | 19 | public static T unmarshal(InputStream input, Class type) throws IOException { 20 | return MAPPER.readValue(input, type); 21 | } 22 | 23 | private static ObjectMapper createObjectMapper() { 24 | return new ObjectMapper() 25 | .findAndRegisterModules() 26 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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:123456789:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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:123456789:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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:123456789:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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:123456789:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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:123456789:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"APPROVED\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"DRAFT\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"APPROVED\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"DRAFT\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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": "123456789", 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 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_all_good.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-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 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-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 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-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 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-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 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-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 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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.properties", 6 | "account": "123456789", 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 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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\": [ \"prop1_exterior1.jpg\", \"prop1_interior1.jpg\", \"prop1_interior2.jpg\", \"prop1_interior3.jpg\", \"prop1_interior4-bad.jpg\" ] }", 5 | "DetailType": "PublicationApprovalRequested", 6 | "EventBusName": "UnicornPropertiesBus-local" 7 | } 8 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/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_updated_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_properties/PropertyFunctions/src/test/events/lambda/contract_status_checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Input": { 3 | "property_id": "usa/anytown/main-street/123", 4 | "address": { 5 | "country": "USA", 6 | "city": "Anytown", 7 | "street": "Main Street", 8 | "number": 123 9 | }, 10 | "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.", 11 | "contract": "sale", 12 | "listprice": 200, 13 | "currency": "SPL", 14 | "images": [ 15 | "property_images/prop1_exterior1.jpg", 16 | "property_images/prop1_interior1.jpg", 17 | "property_images/prop1_interior2.jpg", 18 | "property_images/prop1_interior3.jpg", 19 | "property_images/prop1_interior4.jpg" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/put_event_contract_status_changed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "unicorn.contracts", 4 | "Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": \"199\",\"property_id\":\"bbb\",\"contract_status\":\"APPROVED\"}", 5 | "DetailType": "ContractStatusChanged", 6 | "EventBusName": "Dev-UnicornPropertiesEventBus" 7 | } 8 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/put_event_publication_approval_requested.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "EventBusName": "Dev-UnicornPropertiesEventBus", 4 | "Source": "unicorn.properties.web", 5 | "DetailType": "PublicationApprovalRequested", 6 | "Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"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\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}" 7 | } 8 | ] -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/events/send_events_cli.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "EventBusName": "Dev-UnicornPropertiesEventBus", 4 | "Source": "Unicorn.Web", 5 | "DetailType": "PublicationApprovalRequested", 6 | "Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"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\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/PropertyFunctions/src/test/java/properties/ContractStatusTests.java: -------------------------------------------------------------------------------- 1 | package properties; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | import static org.mockito.Mockito.mock; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.io.IOException; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | import com.amazonaws.services.lambda.runtime.Context; 15 | 16 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 17 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 18 | 19 | import org.junit.Before; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.runner.RunWith; 22 | import org.mockito.junit.MockitoJUnitRunner; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class ContractStatusTests { 26 | 27 | Context context; 28 | DynamoDbClient client; 29 | 30 | ContractStatusChangedHandlerFunction contractStatusChangedHandler; 31 | 32 | Map response = new HashMap(); 33 | 34 | @Before 35 | public void setUp() throws Exception { 36 | 37 | context = mock(Context.class); 38 | client = mock(DynamoDbClient.class); 39 | 40 | } 41 | 42 | @Test 43 | public void validStatusCheckEvent() throws IOException { 44 | 45 | contractStatusChangedHandler = new ContractStatusChangedHandlerFunction(); 46 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 47 | File resourceFile = new File("src/test/events/lambda/contract_status_changed.json"); 48 | client = mock(DynamoDbClient.class); 49 | contractStatusChangedHandler.setDynamodbClient(client); 50 | 51 | FileInputStream fis = new FileInputStream(resourceFile); 52 | 53 | contractStatusChangedHandler.handleRequest(fis, outputStream, context); 54 | ByteArrayInputStream inStream = new ByteArrayInputStream(outputStream.toByteArray()); 55 | String response = new String(inStream.readAllBytes()); 56 | assertTrue("Successful Response", response.contains("contract_id")); 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /unicorn_properties/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Properties 2 | 3 | ![Properties Approval Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-properties.png) 4 | 5 | ## Architecture overview 6 | 7 | Unicorn Properties is primarily responsible for approving property listings for Unicorn Web. 8 | 9 | A core component of Unicorn Properties is the approvals workflow. The approvals workflow is implemented using an AWS Step Functions state machine. At a high level, the workflow will: 10 | 11 | * Check whether or not it has any contract information for the property it needs to approve. If there is no contract information, the approval process cannot be completed. 12 | * Ensure the sentiment of the property description is positive and that there no unsafe images. All checks must pass for the listing to be made public. 13 | * Ensure that the contract is in an APPROVED state before it can approve the listing. This accounts for a situation where the property listings are created before the contract has been signed and the services for Unicorn Properties are paid for. 14 | * Publish the result of the workflow via the `PublicationEvaluationCompleted` event. 15 | 16 | The workflow is initiated by a request made by an Unicorn Properties **agent** to have the property approved for publication. Once they have created a property listing (added property details and photos), they initiate the request in Unicorn Web, which generates a `PublicationApprovalRequested` event. This event contains the property information which the workflow processes. 17 | 18 | In order process the approvals workflow successfully, the properties service needs to know the current status of a contract. To remain fully decoupled from the **Contracts Service**, it maintains a local copy of contract status by consuming the `ContractStatusChanged` event. This is eliminates the need for the Contracts service to expose an API that gives other services access to its database, and allows the Properties service to function autonomously. 19 | 20 | When the workflow is paused to check to see whether or not the contract is in an approved state, the `WaitForContractApproval` state will update a contract status for a specified property with its task token. This initiates a stream event on the DynamoDB table. The Property approvals sync function handles DynamoDB stream events. It determines whether or not to pass AWS Step Function task token back to the state machine based on the contract state. 21 | 22 | If workflow is completed successfully, it will emit a `PublicationEvaluationCompleted` event, with an evaluation result of `APPROVED` or `DECLINED`. This is what the Property Web will listen to in order to make the list available for publication. 23 | 24 | ## Note: 25 | 26 | 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. 27 | -------------------------------------------------------------------------------- /unicorn_properties/integration/PublicationEvaluationCompleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "PublicationEvaluationCompleted" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "AWSEvent": { 11 | "type": "object", 12 | "required": [ 13 | "detail-type", 14 | "resources", 15 | "detail", 16 | "id", 17 | "source", 18 | "time", 19 | "region", 20 | "version", 21 | "account" 22 | ], 23 | "x-amazon-events-detail-type": "PublicationEvaluationCompleted", 24 | "x-amazon-events-source": "unicorn.web", 25 | "properties": { 26 | "detail": { 27 | "$ref": "#/components/schemas/PublicationEvaluationCompleted" 28 | }, 29 | "account": { 30 | "type": "string" 31 | }, 32 | "detail-type": { 33 | "type": "string" 34 | }, 35 | "id": { 36 | "type": "string" 37 | }, 38 | "region": { 39 | "type": "string" 40 | }, 41 | "resources": { 42 | "type": "array", 43 | "items": { 44 | "type": "string" 45 | } 46 | }, 47 | "source": { 48 | "type": "string" 49 | }, 50 | "time": { 51 | "type": "string", 52 | "format": "date-time" 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "PublicationEvaluationCompleted": { 60 | "type": "object", 61 | "required": [ 62 | "property_id", 63 | "evaluation_result" 64 | ], 65 | "properties": { 66 | "property_id": { 67 | "type": "string" 68 | }, 69 | "evaluation_result": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /unicorn_properties/integration/event-schemas.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: Event Schemas for use by the Properties Service 5 | 6 | Parameters: 7 | Stage: 8 | Type: String 9 | Default: local 10 | AllowedValues: 11 | - local 12 | - dev 13 | - prod 14 | 15 | Resources: 16 | EventRegistry: 17 | Type: AWS::EventSchemas::Registry 18 | Properties: 19 | Description: 'Event schemas for Unicorn Properties' 20 | RegistryName: 21 | Fn::Sub: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}-${Stage}" 22 | 23 | EventRegistryPolicy: 24 | Type: AWS::EventSchemas::RegistryPolicy 25 | Properties: 26 | RegistryName: 27 | Fn::GetAtt: EventRegistry.RegistryName 28 | Policy: 29 | Version: "2012-10-17" 30 | Statement: 31 | - Sid: AllowExternalServices 32 | Effect: Allow 33 | Principal: 34 | AWS: 35 | - Ref: AWS::AccountId 36 | Action: 37 | - schemas:DescribeCodeBinding 38 | - schemas:DescribeRegistry 39 | - schemas:DescribeSchema 40 | - schemas:GetCodeBindingSource 41 | - schemas:ListSchemas 42 | - schemas:ListSchemaVersions 43 | - schemas:SearchSchemas 44 | Resource: 45 | - Fn::GetAtt: EventRegistry.RegistryArn 46 | - Fn::Sub: "arn:${AWS::Partition}:schemas:${AWS::Region}:${AWS::AccountId}:schema/${EventRegistry.RegistryName}*" 47 | 48 | PublicationEvaluationCompleted: 49 | Type: AWS::EventSchemas::Schema 50 | Properties: 51 | Type: 'OpenApi3' 52 | RegistryName: 53 | Fn::GetAtt: EventRegistry.RegistryName 54 | SchemaName: 55 | Fn::Sub: '{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}@PublicationEvaluationCompleted' 56 | Description: 'The schema for when a property evaluation is completed' 57 | Content: 58 | Fn::Sub: | 59 | { 60 | "openapi": "3.0.0", 61 | "info": { 62 | "version": "1.0.0", 63 | "title": "PublicationEvaluationCompleted" 64 | }, 65 | "paths": {}, 66 | "components": { 67 | "schemas": { 68 | "AWSEvent": { 69 | "type": "object", 70 | "required": [ 71 | "detail-type", 72 | "resources", 73 | "detail", 74 | "id", 75 | "source", 76 | "time", 77 | "region", 78 | "version", 79 | "account" 80 | ], 81 | "x-amazon-events-detail-type": "PublicationEvaluationCompleted", 82 | "x-amazon-events-source": "${EventRegistry.RegistryName}", 83 | "properties": { 84 | "detail": { 85 | "$ref": "#/components/schemas/PublicationEvaluationCompleted" 86 | }, 87 | "account": { 88 | "type": "string" 89 | }, 90 | "detail-type": { 91 | "type": "string" 92 | }, 93 | "id": { 94 | "type": "string" 95 | }, 96 | "region": { 97 | "type": "string" 98 | }, 99 | "resources": { 100 | "type": "array", 101 | "items": { 102 | "type": "string" 103 | } 104 | }, 105 | "source": { 106 | "type": "string" 107 | }, 108 | "time": { 109 | "type": "string", 110 | "format": "date-time" 111 | }, 112 | "version": { 113 | "type": "string" 114 | } 115 | } 116 | }, 117 | "PublicationEvaluationCompleted": { 118 | "type": "object", 119 | "required": [ 120 | "property_id", 121 | "evaluation_result" 122 | ], 123 | "properties": { 124 | "property_id": { 125 | "type": "string" 126 | }, 127 | "evaluation_result": { 128 | "type": "string" 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /unicorn_properties/integration/subscriber-policies.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 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by Unicorn Properties Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | # Update this policy as you get new subscribers by adding their namespace to events:source 19 | CrossServiceCreateRulePolicy: 20 | Type: AWS::Events::EventBusPolicy 21 | Properties: 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" 24 | StatementId: 25 | Fn::Sub: "OnlyRulesForPropertiesServiceEvents-${Stage}" 26 | Statement: 27 | Effect: Allow 28 | Principal: 29 | AWS: 30 | Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 31 | Action: 32 | - events:PutRule 33 | - events:DeleteRule 34 | - events:DescribeRule 35 | - events:DisableRule 36 | - events:EnableRule 37 | - events:PutTargets 38 | - events:RemoveTargets 39 | Resource: 40 | - Fn::Sub: 41 | - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* 42 | - eventBusName: 43 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" 44 | Condition: 45 | StringEqualsIfExists: 46 | "events:creatorAccount": "${aws:PrincipalAccount}" 47 | StringEquals: 48 | "events:source": 49 | - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}" 50 | "Null": 51 | "events:source": "false" 52 | -------------------------------------------------------------------------------- /unicorn_properties/integration/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: Defines the rule for the events (subscriptions) that Unicorn Properties wants to consume. 5 | 6 | Parameters: 7 | Stage: 8 | Type: String 9 | Default: local 10 | AllowedValues: 11 | - local 12 | - dev 13 | - prod 14 | 15 | Resources: 16 | #### UNICORN CONTRACTS EVENT SUBSCRIPTIONS 17 | ContractStatusChangedSubscriptionRule: 18 | Type: AWS::Events::Rule 19 | Properties: 20 | Name: unicorn.properties-ContractStatusChanged 21 | Description: Contract Status Changed subscription 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}" 24 | EventPattern: 25 | source: 26 | - "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}" 27 | detail-type: 28 | - ContractStatusChanged 29 | State: ENABLED 30 | Targets: 31 | - Id: SendEventTo 32 | Arn: 33 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 34 | RoleArn: 35 | Fn::GetAtt: [ UnicornPropertiesSubscriptionRole, Arn ] 36 | 37 | #### UNICORN WEB EVENT SUBSCRIPTIONS 38 | PublicationApprovalRequestedSubscriptionRule: 39 | Type: AWS::Events::Rule 40 | Properties: 41 | Name: unicorn.properties-PublicationApprovalRequested 42 | Description: Publication evaluation completed subscription 43 | EventBusName: 44 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" 45 | EventPattern: 46 | source: 47 | - "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}" 48 | detail-type: 49 | - PublicationApprovalRequested 50 | State: ENABLED 51 | Targets: 52 | - Id: SendEventTo 53 | Arn: 54 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 55 | RoleArn: 56 | Fn::GetAtt: [ UnicornPropertiesSubscriptionRole, Arn ] 57 | 58 | 59 | # This IAM role allows EventBridge to assume the permissions necessary to send events 60 | # from the publishing event bus, to the subscribing event bus (UnicornPropertiesEventBusArn) 61 | UnicornPropertiesSubscriptionRole: 62 | Type: AWS::IAM::Role 63 | Properties: 64 | AssumeRolePolicyDocument: 65 | Statement: 66 | - Effect: Allow 67 | Action: sts:AssumeRole 68 | Principal: 69 | Service: events.amazonaws.com 70 | Policies: 71 | - PolicyName: PutEventsOnUnicornPropertiesEventBus 72 | PolicyDocument: 73 | Version: "2012-10-17" 74 | Statement: 75 | - Effect: Allow 76 | Action: events:PutEvents 77 | Resource: 78 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 79 | 80 | Outputs: 81 | ContractStatusChangedSubscription: 82 | Description: Rule ARN for Contract service event subscription 83 | Value: 84 | Fn::GetAtt: [ ContractStatusChangedSubscriptionRule, Arn ] 85 | 86 | PublicationApprovalRequestedSubscription: 87 | Description: Rule ARN for Web service event subscription 88 | Value: 89 | Fn::GetAtt: [ PublicationApprovalRequestedSubscriptionRule, Arn ] 90 | -------------------------------------------------------------------------------- /unicorn_properties/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-properties" 5 | s3_prefix = "uni-prop-local-properties" 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" -------------------------------------------------------------------------------- /unicorn_properties/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_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 | -------------------------------------------------------------------------------- /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://ws-assets-prod-iad-r-iad-ed304a55c2ca1aee.s3.us-east-1.amazonaws.com/9a27e484-7336-4ed0-8f90-f2747e4ac65c/{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" 133 | 134 | -------------------------------------------------------------------------------- /unicorn_shared/uni-prop-namespaces.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | Global namespaces for Unicorn Properties applications and services. 5 | This only needs to be deployed once. 6 | 7 | 8 | Resources: 9 | 10 | UnicornContractsNamespaceParam: 11 | Type: AWS::SSM::Parameter 12 | Properties: 13 | Type: String 14 | Name: !Sub /uni-prop/UnicornContractsNamespace 15 | Value: "unicorn.contracts" 16 | 17 | UnicornPropertiesNamespaceParam: 18 | Type: AWS::SSM::Parameter 19 | Properties: 20 | Type: String 21 | Name: !Sub /uni-prop/UnicornPropertiesNamespace 22 | Value: "unicorn.properties" 23 | 24 | UnicornWebNamespaceParam: 25 | Type: AWS::SSM::Parameter 26 | Properties: 27 | Type: String 28 | Name: !Sub /uni-prop/UnicornWebNamespace 29 | Value: "unicorn.web" 30 | 31 | 32 | Outputs: 33 | 34 | UnicornContractsNamespace: 35 | Description: Unicorn Contracts namespace parameter 36 | Value: !Ref UnicornContractsNamespaceParam 37 | 38 | UnicornPropertiesNamespace: 39 | Description: Unicorn Properties namespace parameter 40 | Value: !Ref UnicornPropertiesNamespaceParam 41 | 42 | UnicornWebNamespace: 43 | Description: Unicorn Web namespace parameter 44 | Value: !Ref UnicornWebNamespaceParam 45 | 46 | UnicornContractsNamespaceVale: 47 | Description: Unicorn Contracts namespace parameter value 48 | Value: !GetAtt UnicornContractsNamespaceParam.Value 49 | 50 | UnicornPropertiesNamespaceValue: 51 | Description: Unicorn Properties namespace parameter value 52 | Value: !GetAtt UnicornPropertiesNamespaceParam.Value 53 | 54 | UnicornWebNamespaceValue: 55 | Description: Unicorn Web namespace parameter value 56 | Value: !GetAtt UnicornWebNamespaceParam.Value -------------------------------------------------------------------------------- /unicorn_web/.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | **/*~ 3 | **/.fuse_hidden* 4 | **/.directory 5 | **/.Trash-* 6 | **/.nfs* 7 | 8 | ### OSX ### 9 | **/*.DS_Store 10 | **/.AppleDouble 11 | **/.LSOverride 12 | **/.DocumentRevisions-V100 13 | **/.fseventsd 14 | **/.Spotlight-V100 15 | **/.TemporaryItems 16 | **/.Trashes 17 | **/.VolumeIcon.icns 18 | **/.com.apple.timemachine.donotpresent 19 | 20 | ### JetBrains IDEs ### 21 | **/*.iws 22 | **/.idea/ 23 | **/.idea_modules/ 24 | 25 | ### Python ### 26 | **/__pycache__/ 27 | **/*.py[cod] 28 | **/*$py.class 29 | **/.Python 30 | **/build/ 31 | **/develop-eggs/ 32 | **/dist/ 33 | **/downloads/ 34 | **/eggs/ 35 | **/.eggs/ 36 | **/parts/ 37 | **/sdist/ 38 | **/wheels/ 39 | **/*.egg 40 | **/*.egg-info/ 41 | **/.installed.cfg 42 | **/pip-log.txt 43 | **/pip-delete-this-directory.txt 44 | 45 | ### Unit test / coverage reports ### 46 | **/.cache 47 | **/.coverage 48 | **/.hypothesis/ 49 | **/.pytest_cache/ 50 | **/.tox/ 51 | **/*.cover 52 | **/coverage.xml 53 | **/htmlcov/ 54 | **/nosetests.xml 55 | 56 | ### pyenv ### 57 | **/.python-version 58 | 59 | ### Environments ### 60 | **/.env 61 | **/.venv 62 | **/env/ 63 | **/venv/ 64 | **/ENV/ 65 | **/env.bak/ 66 | **/venv.bak/ 67 | 68 | ### VisualStudioCode ### 69 | **/.vscode/ 70 | **/.history 71 | 72 | ### Windows ### 73 | # Windows thumbnail cache files 74 | **/Thumbs.db 75 | **/ehthumbs.db 76 | **/ehthumbs_vista.db 77 | **/Desktop.ini 78 | **/$RECYCLE.BIN/ 79 | **/*.lnk 80 | 81 | ### requirements.txt ### 82 | # We ignore Python's requirements.txt as we use Poetry instead 83 | **/requirements.txt 84 | **/.aws-sam 85 | -------------------------------------------------------------------------------- /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 | STACK_NAME="uni-prop-local-web" 5 | 6 | JSON_FILE="$(cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/property_data.json" 7 | echo "JSON_FILE: '${JSON_FILE}'" 8 | 9 | DDB_TBL_NAME="$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query 'Stacks[0].Outputs[?ends_with(OutputKey, `WebTableName`)].OutputValue' --output text)" 10 | echo "DDB_TABLE_NAME: '${DDB_TBL_NAME}'" 11 | 12 | echo "LOADING ITEMS TO DYNAMODB:" 13 | aws ddb put ${DDB_TBL_NAME} file://${JSON_FILE} 14 | echo "DONE!" 15 | -------------------------------------------------------------------------------- /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_web/PropertyFunctions/src/main/java/property/dao/Property.java: -------------------------------------------------------------------------------- 1 | package property.dao; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | 7 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; 8 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; 9 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; 10 | import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; 11 | 12 | @DynamoDbBean 13 | public class Property { 14 | 15 | String country; 16 | String city; 17 | String street; 18 | String propertyNumber; 19 | String description; 20 | String contract; 21 | Float listprice; 22 | String currency; 23 | List images; 24 | String status; 25 | @JsonIgnore 26 | String pk; 27 | @JsonIgnore 28 | String sk; 29 | String id; 30 | 31 | @DynamoDbPartitionKey 32 | @DynamoDbAttribute("PK") 33 | public String getPk() { 34 | return ("PROPERTY#" + getCountry() + "#" + getCity()).replace(' ', '-').toLowerCase(); 35 | } 36 | 37 | public void setPk(String pk) { 38 | this.pk = pk; 39 | } 40 | 41 | @DynamoDbSortKey 42 | @DynamoDbAttribute("SK") 43 | public String getSk() { 44 | return (getStreet() + "#" + getPropertyNumber()).replace(' ', '-').toLowerCase(); 45 | } 46 | 47 | public void setSk(String sk) { 48 | this.sk = sk; 49 | } 50 | 51 | @JsonIgnore 52 | @software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore 53 | public String getId() { 54 | return (getPk() + '/' + getSk()).replace('#', '/'); 55 | } 56 | 57 | public void setId(String id) { 58 | this.id = id; 59 | } 60 | 61 | public String getCountry() { 62 | return country; 63 | } 64 | 65 | public void setCountry(String country) { 66 | this.country = country; 67 | } 68 | 69 | public String getCity() { 70 | return city; 71 | } 72 | 73 | public void setCity(String city) { 74 | this.city = city; 75 | } 76 | 77 | public String getStreet() { 78 | return street; 79 | } 80 | 81 | public void setStreet(String street) { 82 | this.street = street; 83 | } 84 | 85 | @DynamoDbAttribute(value = "number") 86 | public String getPropertyNumber() { 87 | return propertyNumber; 88 | } 89 | 90 | public void setPropertyNumber(String propertyNumber) { 91 | this.propertyNumber = propertyNumber; 92 | } 93 | 94 | public String getDescription() { 95 | return description; 96 | } 97 | 98 | public void setDescription(String description) { 99 | this.description = description; 100 | } 101 | 102 | public String getContract() { 103 | return contract; 104 | } 105 | 106 | public void setContract(String contract) { 107 | this.contract = contract; 108 | } 109 | 110 | public Float getListprice() { 111 | return listprice; 112 | } 113 | 114 | public void setListprice(Float listprice) { 115 | this.listprice = listprice; 116 | } 117 | 118 | public String getCurrency() { 119 | return currency; 120 | } 121 | 122 | public void setCurrency(String currency) { 123 | this.currency = currency; 124 | } 125 | 126 | public List getImages() { 127 | return images; 128 | } 129 | 130 | public void setImages(List images) { 131 | this.images = images; 132 | } 133 | 134 | public String getStatus() { 135 | return status; 136 | } 137 | 138 | public void setStatus(String status) { 139 | this.status = status; 140 | } 141 | 142 | @Override 143 | public String toString() { 144 | return "Property [city=" + city + ", contract=" + contract + ", country=" + country + ", currency=" + currency 145 | + ", description=" + description + ", id=" + getId() + ", images=" + images + ", listprice=" + listprice 146 | + ", pk=" + getPk() + ", propertyNumber=" + propertyNumber + ", sk=" + getSk() + ", status=" + status 147 | + ", street=" + street + "]"; 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /unicorn_web/PropertyFunctions/src/main/java/property/populate/PopulateDataFunction.java: -------------------------------------------------------------------------------- 1 | package property.populate; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import com.amazonaws.services.lambda.runtime.Context; 7 | import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; 8 | 9 | import property.dao.Property; 10 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; 11 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; 12 | import software.amazon.awssdk.enhanced.dynamodb.TableSchema; 13 | 14 | import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; 15 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 16 | import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; 17 | import software.amazon.lambda.powertools.cloudformation.Response; 18 | 19 | public class PopulateDataFunction extends AbstractCustomResourceHandler { 20 | 21 | String DYNAMODB_TABLE = System.getenv("DYNAMODB_TABLE"); 22 | 23 | String[] validKeys = { "country", "city", "street", "number", "description", "contract", "listprice", "currency", 24 | "images" }; 25 | 26 | DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() 27 | .httpClientBuilder(NettyNioAsyncHttpClient.builder() 28 | .maxConcurrency(100) 29 | .maxPendingConnectionAcquires(10_000)) 30 | .build(); 31 | 32 | DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() 33 | .dynamoDbClient(dynamodbClient) 34 | .build(); 35 | 36 | @Override 37 | protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) { 38 | try { 39 | return handleEvent(createEvent); 40 | } catch (Exception e) { 41 | return Response.builder() 42 | .value(Map.of("Resource", DYNAMODB_TABLE + "-" + createEvent.getLogicalResourceId())) 43 | .status(Response.Status.FAILED).build(); 44 | } 45 | 46 | } 47 | 48 | @Override 49 | protected Response update(CloudFormationCustomResourceEvent updateEvent, Context context) { 50 | try { 51 | return handleEvent(updateEvent); 52 | } catch (Exception e) { 53 | return Response.builder() 54 | .value(Map.of("Resource", DYNAMODB_TABLE + "-" + updateEvent.getLogicalResourceId())) 55 | .status(Response.Status.FAILED).build(); 56 | } 57 | } 58 | 59 | @Override 60 | protected Response delete(CloudFormationCustomResourceEvent deleteEvent, Context context) { 61 | return null; 62 | } 63 | 64 | private void saveInDatabase(Property property, String table_name) { 65 | 66 | DynamoDbAsyncTable propertyTable = enhancedClient.table(table_name, 67 | TableSchema.fromBean(Property.class)); 68 | propertyTable.putItem(property).join(); 69 | 70 | } 71 | 72 | Property createPropertyFromEvent(CloudFormationCustomResourceEvent event) throws Exception { 73 | Map propertyMap = event.getResourceProperties(); 74 | // Iterate over map and check the keys 75 | for (String strKey : validKeys) { 76 | 77 | if (!propertyMap.containsKey(strKey)) { 78 | throw new Exception("Invalid input: missing mandatory field " + strKey); 79 | } 80 | 81 | } 82 | 83 | Property property = new Property(); 84 | property.setCountry((String) propertyMap.get("country")); 85 | property.setCity((String) propertyMap.get("city")); 86 | property.setStreet((String) propertyMap.get("street")); 87 | property.setPropertyNumber((String) propertyMap.get("number")); 88 | property.setDescription((String) propertyMap.get("description")); 89 | property.setContract((String) propertyMap.get("contract")); 90 | property.setListprice(Float.parseFloat((String) propertyMap.get("listprice"))); 91 | property.setCurrency((String) propertyMap.get("currency")); 92 | List images = (List) propertyMap.get("images"); 93 | property.setImages(images); 94 | property.setStatus("NEW"); 95 | return property; 96 | 97 | } 98 | 99 | private Response handleEvent(CloudFormationCustomResourceEvent createEvent) throws Exception { 100 | 101 | Property property = createPropertyFromEvent(createEvent); 102 | saveInDatabase(property, DYNAMODB_TABLE); 103 | return Response.builder().value(Map.of("Resource", DYNAMODB_TABLE + "-" + createEvent.getLogicalResourceId())) 104 | .status(Response.Status.SUCCESS).build(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/PublicationApprovedFunction.java: -------------------------------------------------------------------------------- 1 | package property.requestapproval; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.io.OutputStreamWriter; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | import com.amazonaws.services.lambda.runtime.Context; 10 | import com.fasterxml.jackson.databind.JsonNode; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | 13 | import org.apache.logging.log4j.LogManager; 14 | import org.apache.logging.log4j.Logger; 15 | 16 | import property.dao.Property; 17 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; 18 | import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; 19 | import software.amazon.awssdk.enhanced.dynamodb.Key; 20 | import software.amazon.awssdk.enhanced.dynamodb.TableSchema; 21 | import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; 22 | import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; 23 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 24 | import software.amazon.lambda.powertools.logging.Logging; 25 | import software.amazon.lambda.powertools.metrics.Metrics; 26 | import software.amazon.lambda.powertools.tracing.Tracing; 27 | import schema.unicorn_properties.publicationevaluationcompleted.marshaller.Marshaller; 28 | import schema.unicorn_properties.publicationevaluationcompleted.AWSEvent; 29 | import schema.unicorn_properties.publicationevaluationcompleted.PublicationEvaluationCompleted; 30 | 31 | /** 32 | * Function checks for the existence of a contract status entry for a specified 33 | * property. 34 | * 35 | * If an entry exists, pause the workflow, and update the record with task 36 | * token. 37 | */ 38 | public class PublicationApprovedFunction { 39 | 40 | Logger logger = LogManager.getLogger(); 41 | 42 | final String TABLE_NAME = System.getenv("DYNAMODB_TABLE"); 43 | DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() 44 | .httpClientBuilder(NettyNioAsyncHttpClient.builder()) 45 | .build(); 46 | 47 | DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() 48 | .dynamoDbClient(dynamodbClient) 49 | .build(); 50 | 51 | DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME, 52 | TableSchema.fromBean(Property.class)); 53 | 54 | @Tracing 55 | @Metrics(captureColdStart = true) 56 | @Logging(logEvent = true) 57 | public void handleRequest(InputStream inputStream, OutputStream outputStream, 58 | Context context) throws IOException { 59 | 60 | AWSEvent event = Marshaller.unmarshalEvent(inputStream, 61 | PublicationEvaluationCompleted.class); 62 | 63 | String propertyId = event.getDetail().getPropertyId(); 64 | String evaluationResult = event.getDetail().getEvaluationResult(); 65 | 66 | publicationApproved(evaluationResult, propertyId); 67 | 68 | ObjectMapper objectMapper = new ObjectMapper(); 69 | OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); 70 | writer.write(objectMapper.writeValueAsString("'result': 'Successfully updated property status'")); 71 | writer.close(); 72 | 73 | } 74 | 75 | @Tracing 76 | private void publicationApproved(String evaluationResult, String propertyId) { 77 | 78 | String[] splitString = propertyId.split("/"); 79 | String country = splitString[0]; 80 | String city = splitString[1]; 81 | String street = splitString[2]; 82 | String number = splitString[3]; 83 | String strPartionKey = ("property#" + country + "#" + city).replace(' ', '-').toLowerCase(); 84 | String strSortKey = (street + "#" + number).replace(' ', '-').toLowerCase(); 85 | 86 | Key key = Key.builder().partitionValue(strPartionKey).sortValue(strSortKey).build(); 87 | Property existingProperty = propertyTable.getItem(key).join(); 88 | 89 | if (existingProperty == null) { 90 | logger.error("Property not found for ID: {}", propertyId); 91 | throw new RuntimeException("Property not found with ID: " + propertyId); 92 | } 93 | 94 | // Always set the property number explicitly to ensure it's correct 95 | existingProperty.setPropertyNumber(number); 96 | existingProperty.setStatus(evaluationResult); 97 | 98 | logger.info("Updating property with status: {} and propertyNumber: {}", 99 | evaluationResult, existingProperty.getPropertyNumber()); 100 | propertyTable.putItem(existingProperty).join(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/AWSEvent.java: -------------------------------------------------------------------------------- 1 | package schema.unicorn_properties.publicationevaluationcompleted; 2 | 3 | import java.util.Objects; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonValue; 7 | import java.util.ArrayList; 8 | import java.util.Date; 9 | import java.util.List; 10 | import schema.unicorn_properties.publicationevaluationcompleted.PublicationEvaluationCompleted; 11 | import java.io.Serializable; 12 | 13 | public class AWSEvent { 14 | 15 | @JsonProperty("detail") 16 | private T detail = null; 17 | 18 | @JsonProperty("detail-type") 19 | private String detailType = null; 20 | 21 | @JsonProperty("resources") 22 | private List resources = null; 23 | 24 | @JsonProperty("id") 25 | private String id = null; 26 | 27 | @JsonProperty("source") 28 | private String source = null; 29 | 30 | @JsonProperty("time") 31 | private Date time = null; 32 | 33 | @JsonProperty("region") 34 | private String region = null; 35 | 36 | @JsonProperty("version") 37 | private String version = null; 38 | 39 | @JsonProperty("account") 40 | private String account = null; 41 | 42 | public AWSEvent detail(T detail) { 43 | this.detail = detail; 44 | return this; 45 | } 46 | 47 | public T getDetail() { 48 | return detail; 49 | } 50 | 51 | public void setDetail(T detail) { 52 | this.detail = detail; 53 | } 54 | 55 | public AWSEvent detailType(String detailType) { 56 | this.detailType = detailType; 57 | return this; 58 | } 59 | 60 | public String getDetailType() { 61 | return detailType; 62 | } 63 | 64 | public void setDetailType(String detailType) { 65 | this.detailType = detailType; 66 | } 67 | 68 | public AWSEvent resources(List resources) { 69 | this.resources = resources; 70 | return this; 71 | } 72 | 73 | public List getResources() { 74 | return resources; 75 | } 76 | 77 | public void setResources(List resources) { 78 | this.resources = resources; 79 | } 80 | 81 | public AWSEvent id(String id) { 82 | this.id = id; 83 | return this; 84 | } 85 | 86 | public String getId() { 87 | return id; 88 | } 89 | 90 | public void setId(String id) { 91 | this.id = id; 92 | } 93 | 94 | public AWSEvent source(String source) { 95 | this.source = source; 96 | return this; 97 | } 98 | 99 | public String getSource() { 100 | return source; 101 | } 102 | 103 | public void setSource(String source) { 104 | this.source = source; 105 | } 106 | 107 | public AWSEvent time(Date time) { 108 | this.time = time; 109 | return this; 110 | } 111 | 112 | public Date getTime() { 113 | return time; 114 | } 115 | 116 | public void setTime(Date time) { 117 | this.time = time; 118 | } 119 | 120 | public AWSEvent region(String region) { 121 | this.region = region; 122 | return this; 123 | } 124 | 125 | public String getRegion() { 126 | return region; 127 | } 128 | 129 | public void setRegion(String region) { 130 | this.region = region; 131 | } 132 | 133 | public AWSEvent version(String version) { 134 | this.version = version; 135 | return this; 136 | } 137 | 138 | public String getVersion() { 139 | return version; 140 | } 141 | 142 | public void setVersion(String version) { 143 | this.version = version; 144 | } 145 | 146 | public AWSEvent account(String account) { 147 | this.account = account; 148 | return this; 149 | } 150 | 151 | public String getAccount() { 152 | return account; 153 | } 154 | 155 | public void setAccount(String account) { 156 | this.account = account; 157 | } 158 | 159 | @Override 160 | public boolean equals(java.lang.Object o) { 161 | if (this == o) { 162 | return true; 163 | } 164 | if (o == null || getClass() != o.getClass()) { 165 | return false; 166 | } 167 | AWSEvent awSEvent = (AWSEvent) o; 168 | return Objects.equals(this.detail, awSEvent.detail) && 169 | Objects.equals(this.detailType, awSEvent.detailType) && 170 | Objects.equals(this.resources, awSEvent.resources) && 171 | Objects.equals(this.id, awSEvent.id) && 172 | Objects.equals(this.source, awSEvent.source) && 173 | Objects.equals(this.time, awSEvent.time) && 174 | Objects.equals(this.region, awSEvent.region) && 175 | Objects.equals(this.version, awSEvent.version) && 176 | Objects.equals(this.account, awSEvent.account); 177 | } 178 | 179 | @Override 180 | public int hashCode() { 181 | return java.util.Objects.hash(detail, detailType, resources, id, source, time, region, version, account); 182 | } 183 | 184 | 185 | @Override 186 | public String toString() { 187 | StringBuilder sb = new StringBuilder(); 188 | sb.append("class AWSEvent {\n"); 189 | 190 | sb.append(" detail: ").append(toIndentedString(detail)).append("\n"); 191 | sb.append(" detailType: ").append(toIndentedString(detailType)).append("\n"); 192 | sb.append(" resources: ").append(toIndentedString(resources)).append("\n"); 193 | sb.append(" id: ").append(toIndentedString(id)).append("\n"); 194 | sb.append(" source: ").append(toIndentedString(source)).append("\n"); 195 | sb.append(" time: ").append(toIndentedString(time)).append("\n"); 196 | sb.append(" region: ").append(toIndentedString(region)).append("\n"); 197 | sb.append(" version: ").append(toIndentedString(version)).append("\n"); 198 | sb.append(" account: ").append(toIndentedString(account)).append("\n"); 199 | sb.append("}"); 200 | return sb.toString(); 201 | } 202 | 203 | private String toIndentedString(java.lang.Object o) { 204 | if (o == null) { 205 | return "null"; 206 | } 207 | return o.toString().replace("\n", "\n "); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/PublicationEvaluationCompleted.java: -------------------------------------------------------------------------------- 1 | package schema.unicorn_properties.publicationevaluationcompleted; 2 | 3 | import java.util.Objects; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonValue; 7 | import java.io.Serializable; 8 | 9 | public class PublicationEvaluationCompleted implements Serializable { 10 | private static final long serialVersionUID = 1L; 11 | 12 | @JsonProperty("evaluation_result") 13 | private String evaluationResult = null; 14 | 15 | @JsonProperty("property_id") 16 | private String propertyId = null; 17 | 18 | public PublicationEvaluationCompleted evaluationResult(String evaluationResult) { 19 | this.evaluationResult = evaluationResult; 20 | return this; 21 | } 22 | 23 | 24 | public String getEvaluationResult() { 25 | return evaluationResult; 26 | } 27 | 28 | public void setEvaluationResult(String evaluationResult) { 29 | this.evaluationResult = evaluationResult; 30 | } 31 | 32 | public PublicationEvaluationCompleted propertyId(String propertyId) { 33 | this.propertyId = propertyId; 34 | return this; 35 | } 36 | 37 | 38 | public String getPropertyId() { 39 | return propertyId; 40 | } 41 | 42 | public void setPropertyId(String propertyId) { 43 | this.propertyId = propertyId; 44 | } 45 | 46 | @Override 47 | public boolean equals(java.lang.Object o) { 48 | if (this == o) { 49 | return true; 50 | } 51 | if (o == null || getClass() != o.getClass()) { 52 | return false; 53 | } 54 | PublicationEvaluationCompleted publicationEvaluationCompleted = (PublicationEvaluationCompleted) o; 55 | return Objects.equals(this.evaluationResult, publicationEvaluationCompleted.evaluationResult) && 56 | Objects.equals(this.propertyId, publicationEvaluationCompleted.propertyId); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return java.util.Objects.hash(evaluationResult, propertyId); 62 | } 63 | 64 | 65 | @Override 66 | public String toString() { 67 | StringBuilder sb = new StringBuilder(); 68 | sb.append("class PublicationEvaluationCompleted {\n"); 69 | 70 | sb.append(" evaluationResult: ").append(toIndentedString(evaluationResult)).append("\n"); 71 | sb.append(" propertyId: ").append(toIndentedString(propertyId)).append("\n"); 72 | sb.append("}"); 73 | return sb.toString(); 74 | } 75 | 76 | private String toIndentedString(java.lang.Object o) { 77 | if (o == null) { 78 | return "null"; 79 | } 80 | return o.toString().replace("\n", "\n "); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/marshaller/Marshaller.java: -------------------------------------------------------------------------------- 1 | package schema.unicorn_properties.publicationevaluationcompleted.marshaller; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.type.TypeFactory; 6 | import schema.unicorn_properties.publicationevaluationcompleted.AWSEvent; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | 12 | public class Marshaller { 13 | 14 | private static final ObjectMapper MAPPER = createObjectMapper(); 15 | 16 | public static void marshal(OutputStream output, T value) throws IOException { 17 | MAPPER.writeValue(output, value); 18 | } 19 | 20 | public static T unmarshal(InputStream input, Class type) throws IOException { 21 | return MAPPER.readValue(input, type); 22 | } 23 | 24 | public static AWSEvent unmarshalEvent(InputStream input, Class type) throws IOException { 25 | final TypeFactory typeFactory = MAPPER.getTypeFactory(); 26 | return MAPPER.readValue(input, typeFactory.constructParametricType(AWSEvent.class, type)); 27 | } 28 | 29 | private static ObjectMapper createObjectMapper() { 30 | return new ObjectMapper() 31 | .findAndRegisterModules() 32 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /unicorn_web/PropertyFunctions/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 is primarily responsible for allowing customers to search and view property listings. It also supports ability for agents to request approval for specific property. Those approval requests are sent to Property service for validation, before Properties table is updated with approval evaluation results. 8 | 9 | A core component of Unicorn Web are the Lambda functions which are responsible with completing API Gateway requests to: 10 | 11 | - search approved property listings 12 | This function interacts with DynamoDB table to retrieve property listings marked as `APPROVED`. The API Gateway implementation and lambda code support multiple types of search patterns, and allow searching by city, street, or house number. 13 | 14 | - request approval of property listing 15 | This function sends an event to EventBridge requesting an approval for a property listing specified in the payload sent from client 16 | 17 | - publication approved function 18 | There is also a lambda function responsible for receiving any "Approval Evaluation Completed" events from EventBridge. This function writes the evaluation result to DynamoDB table. 19 | 20 | ### Testing the APIs 21 | 22 | ```bash 23 | export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-web --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text` 24 | 25 | curl --location --request POST "${API}request_approval" \ 26 | --header 'Content-Type: application/json' \ 27 | --data-raw '{"property_id": "usa/anytown/main-street/111"}' 28 | 29 | 30 | curl -X POST ${API_URL}request_approval \ 31 | -H 'Content-Type: application/json' \ 32 | -d '{"property_id":"usa/anytown/main-street/111"}' | jq 33 | ``` 34 | -------------------------------------------------------------------------------- /unicorn_web/integration/PublicationApprovalRequested.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "PublicationApprovalRequested" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "AWSEvent": { 11 | "type": "object", 12 | "required": [ 13 | "detail-type", 14 | "resources", 15 | "detail", 16 | "id", 17 | "source", 18 | "time", 19 | "region", 20 | "version", 21 | "account" 22 | ], 23 | "x-amazon-events-detail-type": "PublicationApprovalRequested", 24 | "x-amazon-events-source": "unicorn.web", 25 | "properties": { 26 | "detail": { 27 | "$ref": "#/components/schemas/PublicationApprovalRequested" 28 | }, 29 | "account": { 30 | "type": "string" 31 | }, 32 | "detail-type": { 33 | "type": "string" 34 | }, 35 | "id": { 36 | "type": "string" 37 | }, 38 | "region": { 39 | "type": "string" 40 | }, 41 | "resources": { 42 | "type": "array", 43 | "items": { 44 | "type": "string" 45 | } 46 | }, 47 | "source": { 48 | "type": "string" 49 | }, 50 | "time": { 51 | "type": "string", 52 | "format": "date-time" 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "PublicationApprovalRequested": { 60 | "type": "object", 61 | "required": [ 62 | "images", 63 | "address", 64 | "listprice", 65 | "contract", 66 | "description", 67 | "currency", 68 | "property_id", 69 | "status" 70 | ], 71 | "properties": { 72 | "address": { 73 | "$ref": "#/components/schemas/Address" 74 | }, 75 | "contract": { 76 | "type": "string" 77 | }, 78 | "currency": { 79 | "type": "string" 80 | }, 81 | "description": { 82 | "type": "string" 83 | }, 84 | "images": { 85 | "type": "array", 86 | "items": { 87 | "type": "string" 88 | } 89 | }, 90 | "listprice": { 91 | "type": "string" 92 | }, 93 | "property_id": { 94 | "type": "string" 95 | }, 96 | "status": { 97 | "type": "string" 98 | } 99 | } 100 | }, 101 | "Address": { 102 | "type": "object", 103 | "required": [ 104 | "country", 105 | "number", 106 | "city", 107 | "street" 108 | ], 109 | "properties": { 110 | "city": { 111 | "type": "string" 112 | }, 113 | "country": { 114 | "type": "string" 115 | }, 116 | "number": { 117 | "type": "string" 118 | }, 119 | "street": { 120 | "type": "string" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /unicorn_web/integration/subscriber-policies.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 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by Unicorn Web Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | # Update this policy as you get new subscribers by adding their namespace to events:source 19 | CrossServiceCreateRulePolicy: 20 | Type: AWS::Events::EventBusPolicy 21 | Properties: 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" 24 | StatementId: 25 | Fn::Sub: "OnlyRulesForWebServiceEvents-${Stage}" 26 | Statement: 27 | Effect: Allow 28 | Principal: 29 | AWS: 30 | Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 31 | Action: 32 | - events:PutRule 33 | - events:DeleteRule 34 | - events:DescribeRule 35 | - events:DisableRule 36 | - events:EnableRule 37 | - events:PutTargets 38 | - events:RemoveTargets 39 | Resource: 40 | - Fn::Sub: 41 | - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* 42 | - eventBusName: 43 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" 44 | Condition: 45 | StringEqualsIfExists: 46 | "events:creatorAccount": "${aws:PrincipalAccount}" 47 | StringEquals: 48 | "events:source": 49 | - "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}" 50 | "Null": 51 | "events:source": "false" 52 | -------------------------------------------------------------------------------- /unicorn_web/integration/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: Defines the rule for the events (subscriptions) that Unicorn Properties wants to consume. 5 | 6 | Parameters: 7 | Stage: 8 | Type: String 9 | Default: local 10 | AllowedValues: 11 | - local 12 | - dev 13 | - prod 14 | 15 | Resources: 16 | #### UNICORN PROPERTIES EVENT SUBSCRIPTIONS 17 | PublicationEvaluationCompletedSubscriptionRule: 18 | Type: AWS::Events::Rule 19 | Properties: 20 | Name: unicorn.web-PublicationEvaluationCompleted 21 | Description: PublicationEvaluationCompleted subscription 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 24 | EventPattern: 25 | source: 26 | - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}" 27 | detail-type: 28 | - PublicationEvaluationCompleted 29 | State: ENABLED 30 | Targets: 31 | - Id: SendEventTo 32 | Arn: 33 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" 34 | RoleArn: 35 | Fn::GetAtt: [ UnicornPropertiesEventBusToUnicornWebEventBusRole, Arn ] 36 | 37 | # This IAM role allows EventBridge to assume the permissions necessary to send events 38 | # from the publishing event bus, to the subscribing event bus (UnicornWebEventBusArn) 39 | UnicornPropertiesEventBusToUnicornWebEventBusRole: 40 | Type: AWS::IAM::Role 41 | Properties: 42 | AssumeRolePolicyDocument: 43 | Statement: 44 | - Effect: Allow 45 | Action: sts:AssumeRole 46 | Principal: 47 | Service: events.amazonaws.com 48 | Policies: 49 | - PolicyName: PutEventsOnUnicornWebEventBus 50 | PolicyDocument: 51 | Version: "2012-10-17" 52 | Statement: 53 | - Effect: Allow 54 | Action: events:PutEvents 55 | Resource: 56 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" 57 | 58 | Outputs: 59 | PublicationEvaluationCompletedSubscription: 60 | Description: Rule ARN for Property service event subscription 61 | Value: 62 | Fn::GetAtt: [ PublicationEvaluationCompletedSubscriptionRule, Arn ] 63 | -------------------------------------------------------------------------------- /unicorn_web/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" -------------------------------------------------------------------------------- /unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "unicorn.properties", 4 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}", 5 | "DetailType": "PublicationEvaluationCompleted", 6 | "EventBusName": "UnicornWebBus-local" 7 | } 8 | ] 9 | --------------------------------------------------------------------------------