├── .github └── workflows │ └── run-unit-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CommunityFridgeMapApi ├── .gitignore ├── README.md ├── __init__.py ├── api_contract.yaml ├── dependencies │ ├── __init__.py │ └── python │ │ ├── __init__.py │ │ ├── db.py │ │ ├── requirements.txt │ │ └── s3_service.py ├── events │ ├── event.json │ ├── local-contact-email.json │ ├── local-event-get-fridge.json │ ├── local-event-get-fridges-with-tag.json │ ├── local-event-get-fridges.json │ ├── local-fridge-report-event.json │ └── local-post-fridge-event.json ├── functions │ ├── __init__.py │ ├── dev │ │ ├── README.md │ │ ├── __init__.py │ │ ├── hello_world │ │ │ ├── __init__.py │ │ │ ├── app.py │ │ │ └── requirements.txt │ │ └── load_fridge_data │ │ │ ├── __init__.py │ │ │ ├── app.py │ │ │ └── requirements.txt │ ├── email_service │ │ └── v1 │ │ │ ├── __init__.py │ │ │ └── app.py │ ├── fridge_reports │ │ ├── __init__.py │ │ ├── app.py │ │ └── get_latest_fridge_reports │ │ │ ├── __init__.py │ │ │ └── app.py │ ├── fridges │ │ └── v1 │ │ │ ├── __init__.py │ │ │ └── app.py │ └── image │ │ └── v1 │ │ ├── __init__.py │ │ └── app.py ├── requirements.txt ├── template.yaml └── tests │ ├── __init__.py │ ├── assert_resposne.py │ ├── conftest.py │ ├── integration │ ├── __init__.py │ └── test_api_gateway.py │ ├── requirements.txt │ ├── s3_service_stub.py │ └── unit │ ├── __init__.py │ ├── test_db_item.py │ ├── test_db_response.py │ ├── test_email_service.py │ ├── test_fridge.py │ ├── test_fridge_handler.py │ ├── test_fridge_report.py │ ├── test_fridge_report_handler.py │ ├── test_get_latest_fridge_report.py │ ├── test_hello_world.py │ ├── test_image.py │ ├── test_layer.py │ ├── test_post_fridge_handler.py │ ├── test_s3_service.py │ └── test_tag.py ├── LICENSE ├── README.md ├── docker-compose.yml ├── schema ├── fridge.json ├── fridge_history.json ├── fridge_report.json └── tag.json └── scripts └── create_local_dynamodb_tables.py /.github/workflows/run-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | REQUIREMENTS="CommunityFridgeMapApi/tests/requirements.txt" 20 | if [ -f $REQUIREMENTS ]; then pip install -r $REQUIREMENTS --user; fi 21 | - name: Run tests 22 | run: | 23 | cd CommunityFridgeMapApi/ 24 | python -m pytest tests/unit -v 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # AWS SAM 2 | /volume 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/psf/black 8 | rev: 22.3.0 9 | hooks: 10 | - id: black 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct: Fridge Finder 2 | 3 | Like the technical community as a whole, the Fridge Map team is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. 4 | 5 | This isn't an exhaustive list of things that you can't do. Rather, take it in the spirit in which it's intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. This code of conduct applies to all spaces managed by the Fridge Map project. This includes the Discord Server, the Trello Board, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them. 6 | 7 | If you believe someone is violating the code of conduct, we ask that you report it by emailing . 8 | 9 | - **Be friendly and patient.** 10 | 11 | - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 12 | 13 | - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. 14 | 15 | - **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Fridge Map community should be respectful when dealing with other members as well as with people outside the Fridge Map community. 16 | 17 | - **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do 18 | not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: 19 | 20 | - Violent threats or language directed against another person. 21 | - Discriminatory jokes and language. 22 | - Posting sexually explicit or violent material. 23 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 24 | - Personal insults, especially those using racist or sexist terms. 25 | - Unwelcome sexual attention. 26 | - Advocating for, or encouraging, any of the above behavior. 27 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 28 | 29 | - **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Fridge Map is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we're different. The strength of Fridge Map comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn't mean that they're wrong. Don't forget that it is human to err and blaming each other doesn't get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 30 | 31 | Original text courtesy of the [Django Project](https://www.djangoproject.com/conduct/) 32 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | 246 | # AWS Sam 247 | *.aws-sam 248 | local-event.json 249 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/README.md: -------------------------------------------------------------------------------- 1 | # CommunityFridgeMapApi 2 | 3 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. 4 | 5 | - hello_world - Code for the application's Lambda function. 6 | - events - Invocation events that you can use to invoke the function. 7 | - tests - Unit tests for the application code. 8 | - template.yaml - A template that defines the application's AWS resources. 9 | 10 | The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. 11 | 12 | If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. 13 | The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. 14 | 15 | * [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 16 | * [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 17 | * [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 18 | * [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 19 | * [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 20 | * [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 21 | * [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 22 | * [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 23 | * [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 24 | * [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) 25 | * [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) 26 | 27 | ## Deploy the sample application 28 | 29 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. 30 | 31 | To use the SAM CLI, you need the following tools. 32 | 33 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 34 | * [Python 3 installed](https://www.python.org/downloads/) 35 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 36 | 37 | To build and deploy your application for the first time, run the following in your shell: 38 | 39 | ```bash 40 | sam build --use-container 41 | sam deploy --guided 42 | ``` 43 | 44 | The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts: 45 | 46 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. 47 | * **AWS Region**: The AWS region you want to deploy your app to. 48 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. 49 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 50 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. 51 | 52 | You can find your API Gateway Endpoint URL in the output values displayed after deployment. 53 | 54 | ## Use the SAM CLI to build and test locally 55 | 56 | Build your application with the `sam build --use-container` command. 57 | 58 | ```bash 59 | CommunityFridgeMapApi$ sam build --use-container 60 | ``` 61 | 62 | The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. 63 | 64 | Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. 65 | 66 | Run functions locally and invoke them with the `sam local invoke` command. 67 | 68 | ```bash 69 | CommunityFridgeMapApi$ sam local invoke HelloWorldFunction --event events/event.json 70 | ``` 71 | 72 | The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. 73 | 74 | ```bash 75 | CommunityFridgeMapApi$ sam local start-api 76 | CommunityFridgeMapApi$ curl http://localhost:3000/ 77 | ``` 78 | 79 | The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. 80 | 81 | ```yaml 82 | Events: 83 | HelloWorld: 84 | Type: Api 85 | Properties: 86 | Path: /hello 87 | Method: get 88 | ``` 89 | 90 | ## Add a resource to your application 91 | The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. 92 | 93 | ## Fetch, tail, and filter Lambda function logs 94 | 95 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. 96 | 97 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 98 | 99 | ```bash 100 | CommunityFridgeMapApi$ sam logs -n HelloWorldFunction --stack-name CommunityFridgeMapApi --tail 101 | ``` 102 | 103 | You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 104 | 105 | ## Tests 106 | 107 | Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests. 108 | 109 | ```bash 110 | CommunityFridgeMapApi$ pip install -r tests/requirements.txt --user 111 | # unit test 112 | CommunityFridgeMapApi$ python -m pytest tests/unit -v 113 | # integration test, requiring deploying the stack first. 114 | # Create the env variable AWS_SAM_STACK_NAME with the name of the stack we are testing 115 | CommunityFridgeMapApi$ AWS_SAM_STACK_NAME= python -m pytest tests/integration -v 116 | ``` 117 | 118 | ## Cleanup 119 | 120 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: 121 | 122 | ```bash 123 | aws cloudformation delete-stack --stack-name CommunityFridgeMapApi 124 | ``` 125 | 126 | ## Resources 127 | 128 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 129 | 130 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) 131 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/api_contract.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | description: Community Fridge Map API 4 | version: 1.0.0 5 | title: Community Fridge Map API 6 | contact: 7 | email: fridgefinderapp@gmail.com 8 | license: 9 | name: Apache 2.0 10 | url: http://www.apache.org/licenses/LICENSE-2.0.html 11 | 12 | tags: 13 | - name: fridge 14 | description: Fridge root record 15 | - name: report 16 | description: Fridge status reports 17 | - name: contact 18 | description: Website contact form data 19 | - name: tag 20 | description: Hash tag for filtering and searching 21 | 22 | paths: 23 | /v1/fridges: 24 | get: 25 | summary: Read all Fridge objects. 26 | tags: 27 | - fridge 28 | responses: 29 | "200": 30 | description: Array of Fridge objects as JSON 31 | content: 32 | application/json: 33 | schema: 34 | type: array 35 | items: 36 | $ref: "#/components/schemas/Fridge" 37 | "400": 38 | description: Request could not be understood due to incorrect syntax. 39 | "401": 40 | description: Request requires user authentication. 41 | "404": 42 | description: Fridge table not found in database. 43 | "500": 44 | description: Unexpected error prevented server from fulfilling request. 45 | 46 | post: 47 | summary: Create a Fridge object. 48 | tags: 49 | - fridge 50 | requestBody: 51 | required: true 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Fridge" 56 | responses: 57 | "201": 58 | description: Fridge object was created successfully. 59 | content: 60 | application/json: 61 | schema: 62 | type: object 63 | properties: 64 | id: 65 | type: string 66 | pattern: "^[a-z0-9]{2,}$" 67 | description: ID of the Fridge object (fridgeId). 68 | example: greenpointfridge 69 | "400": 70 | description: Request could not be understood due to incorrect syntax. 71 | "401": 72 | description: Request requires user authentication. 73 | "404": 74 | description: Fridge table not found in database. 75 | "409": 76 | description: There is an existing Fridge object with the same fridge name. 77 | "500": 78 | description: Unexpected error prevented server from fulfilling request. 79 | 80 | /v1/fridges/{id}: 81 | parameters: 82 | - in: path 83 | name: id 84 | schema: 85 | type: string 86 | pattern: "^[a-z0-9]{2,}$" 87 | required: true 88 | description: ID of the Fridge object (fridgeId). 89 | example: greenpointfridge 90 | 91 | get: 92 | summary: Read a Fridge object. 93 | tags: 94 | - fridge 95 | responses: 96 | "200": 97 | description: A Fridge object as JSON 98 | content: 99 | application/json: 100 | schema: 101 | $ref: "#/components/schemas/Fridge" 102 | "400": 103 | description: Invalid Fridge ID. 104 | "401": 105 | description: Request requires user authentication. 106 | "404": 107 | description: Fridge object not found. 108 | "500": 109 | description: Unexpected error prevented server from fulfilling request. 110 | 111 | put: 112 | summary: Update a Fridge object. 113 | tags: 114 | - fridge 115 | requestBody: 116 | required: true 117 | content: 118 | application/json: 119 | schema: 120 | type: object 121 | properties: 122 | fridge: 123 | $ref: "#/components/schemas/Fridge" 124 | responses: 125 | "201": 126 | description: Fridge object was updated successfully. 127 | content: 128 | application/json: 129 | schema: 130 | type: object 131 | properties: 132 | id: 133 | type: string 134 | pattern: "^[a-z0-9]{2,}$" 135 | description: ID of the Fridge object (fridgeId). 136 | example: greenpointfridge 137 | "400": 138 | description: Invalid Fridge ID. 139 | "401": 140 | description: Request requires user authentication. 141 | "404": 142 | description: Fridge object not found. 143 | "500": 144 | description: Unexpected error prevented server from fulfilling request. 145 | 146 | delete: 147 | summary: Delete a Fridge object. 148 | tags: 149 | - fridge 150 | responses: 151 | "204": 152 | description: The Fridge object was deleted successfully. 153 | "400": 154 | description: Invalid Fridge ID. 155 | "401": 156 | description: Request requires user authentication. 157 | "404": 158 | description: Fridge object not found. 159 | "500": 160 | description: Unexpected error prevented server from fulfilling request. 161 | 162 | /v1/fridges/{id}/reports: 163 | parameters: 164 | - in: path 165 | name: id 166 | schema: 167 | type: string 168 | pattern: "^[a-z0-9]{2,}$" 169 | required: true 170 | description: ID of the Fridge object (fridgeId). 171 | example: greenpointfridge 172 | 173 | get: 174 | summary: Read all Report objects for the specified fridge. 175 | tags: 176 | - report 177 | responses: 178 | "200": 179 | description: Array of Report objects 180 | content: 181 | application/json: 182 | schema: 183 | type: array 184 | items: 185 | $ref: "#/components/schemas/Report" 186 | "400": 187 | description: Invalid Fridge ID. 188 | "401": 189 | description: Request requires user authentication. 190 | "404": 191 | description: Report objects not found for specified Fridge ID. 192 | "500": 193 | description: Unexpected error prevented server from fulfilling request. 194 | 195 | post: 196 | summary: Create a new Report object. 197 | tags: 198 | - report 199 | requestBody: 200 | required: true 201 | content: 202 | application/json: 203 | schema: 204 | $ref: "#/components/schemas/Report" 205 | responses: 206 | "201": 207 | description: Report object was created successfully. 208 | content: 209 | application/json: 210 | schema: 211 | type: object 212 | properties: 213 | timestamp: 214 | type: string 215 | format: date-time 216 | example: "2022-03-29T18:10:38.547Z" 217 | fridgeId: 218 | type: string 219 | pattern: "^[a-z0-9]{2,}$" 220 | example: greenpointfridge 221 | "400": 222 | description: Invalid Fridge ID. 223 | "401": 224 | description: Request requires user authentication. 225 | "404": 226 | description: Report objects not found for specified Fridge ID. 227 | "500": 228 | description: Unexpected error prevented server from fulfilling request. 229 | 230 | /v1/photo: 231 | post: 232 | summary: Create a new image file in the S3 bucket. 233 | tags: 234 | - fridge 235 | - report 236 | requestBody: 237 | required: true 238 | content: 239 | image/webp: 240 | schema: 241 | description: Blob data of the image. 242 | type: string 243 | format: binary 244 | 245 | responses: 246 | "201": 247 | description: Image was created successfully. 248 | content: 249 | application/json: 250 | schema: 251 | type: object 252 | properties: 253 | photoUrl: 254 | description: Fully qualified URL path to the image. 255 | format: uri 256 | example: https://s3.amazonaws.com/bucket/path/fridge.webp 257 | "400": 258 | description: Request could not be understood due to incorrect syntax. 259 | "401": 260 | description: Request requires user authentication. 261 | "500": 262 | description: Unexpected error prevented server from fulfilling request. 263 | 264 | /v1/contact: 265 | post: 266 | summary: Store website user message 267 | tags: 268 | - contact 269 | requestBody: 270 | required: true 271 | content: 272 | application/json: 273 | schema: 274 | $ref: '#/components/schemas/Contact' 275 | responses: 276 | '201': 277 | description: Message was successfully received. 278 | '400': 279 | description: Request could not be understood due to incorrect syntax. 280 | '500': 281 | description: Unexpected error prevented server from fulfilling request. 282 | 283 | components: 284 | schemas: 285 | Fridge: 286 | type: object 287 | 288 | required: 289 | - id 290 | - name 291 | - location 292 | 293 | properties: 294 | id: 295 | description: The database key for this fridge record. 296 | type: string 297 | pattern: '^[a-z0-9]{4,60}$' 298 | minLength: 4 299 | maxLength: 60 300 | readOnly: true 301 | nullable: false 302 | example: greenpointfridge 303 | 304 | name: 305 | description: A descriptive title for this fridge location. 306 | type: string 307 | minLength: 4 308 | maxLength: 60 309 | example: Greenpoint Fridge 310 | 311 | location: 312 | $ref: '#/components/schemas/Location' 313 | 314 | tags: 315 | description: Arbitrary words to associate with this fridge. 316 | type: array 317 | items: 318 | type: string 319 | maxLength: 140 320 | nullable: true 321 | example: [harlem, halal, kashrut] 322 | 323 | maintainer: 324 | $ref: '#/components/schemas/Maintainer' 325 | nullable: true 326 | 327 | photoUrl: 328 | description: Fully qualified URL path to a picture of the fridge. 329 | type: string 330 | format: uri 331 | nullable: true 332 | example: https://s3.amazonaws.com/bucket/path/fridge.webp 333 | 334 | notes: 335 | description: Notes about the fridge. 336 | type: string 337 | minLength: 1 338 | maxLength: 300 339 | nullable: true 340 | example: Next to Lot Radio. 341 | 342 | verified: 343 | description: Signifies that the fridge data has been verified by the fridge maintainer. 344 | type: boolean 345 | default: false 346 | readOnly: true 347 | example: false 348 | 349 | Location: 350 | type: object 351 | 352 | required: 353 | - street 354 | - city 355 | - state 356 | - zip 357 | - geoLat 358 | - geoLng 359 | 360 | properties: 361 | name: 362 | type: string 363 | maxLength: 70 364 | example: Ralph and Nash Deli 365 | 366 | street: 367 | type: string 368 | maxLength: 55 369 | example: 352 West 116th Street 370 | 371 | city: 372 | type: string 373 | maxLength: 35 374 | example: New York 375 | 376 | state: 377 | type: string 378 | pattern: '^[A-Z]{2}$' 379 | minLength: 2 380 | maxLength: 2 381 | example: NY 382 | 383 | zip: 384 | type: string 385 | pattern: '(^\d{5}$)|(^\d{5}-\d{4}$)' 386 | example: 10026 387 | 388 | geoLat: 389 | description: Geographic latitude. 390 | type: number 391 | readOnly: true 392 | example: 40.8049571 393 | 394 | geoLng: 395 | description: Geographic longitude. 396 | type: number 397 | readOnly: true 398 | example: -73.9570766 399 | 400 | Maintainer: 401 | type: object 402 | 403 | properties: 404 | name: 405 | description: Name of the contact person. 406 | type: string 407 | maxLength: 70 408 | example: Emily Ward 409 | 410 | email: 411 | type: string 412 | format: email 413 | example: eward@example.net 414 | 415 | organization: 416 | description: Name of the organization. 417 | type: string 418 | maxLength: 80 419 | nullable: true 420 | example: Food For People 421 | 422 | phone: 423 | type: string 424 | format: phone 425 | pattern: '^\(\d{3}\) \d{3}-\d{4}$' 426 | nullable: true 427 | example: (782) 654-4748 428 | 429 | website: 430 | type: string 431 | format: uri 432 | nullable: true 433 | example: http://gray.com/ 434 | 435 | instagram: 436 | type: string 437 | format: uri 438 | nullable: true 439 | example: https://www.instagram.com/greenpointfridge/?hl=en 440 | 441 | Report: 442 | type: object 443 | 444 | required: 445 | - timestamp 446 | - condition 447 | - foodPercentage 448 | 449 | properties: 450 | timestamp: 451 | description: The ISO formatted date/time of this report. 452 | type: string 453 | format: date-time 454 | readOnly: true 455 | example: "2022-03-29T18:10:38.547Z" 456 | 457 | condition: 458 | description: The condition of the fridge. Fridge is ... 459 | type: string 460 | enum: [good, dirty, out of order, not at location] 461 | 462 | foodPercentage: 463 | description: The percentage of food in the fridge. 464 | type: integer 465 | enum: [0, 1, 2, 3] 466 | 467 | photoUrl: 468 | description: Fully qualified URL path to a picture of the food. 469 | type: string 470 | format: uri 471 | nullable: true 472 | example: https://s3.amazonaws.com/bucket/path/food.webp 473 | 474 | notes: 475 | description: Notes to be included with this report. 476 | type: string 477 | maxLength: 300 478 | nullable: true 479 | example: Filled with Mars bars and M&M candy. 480 | 481 | Contact: 482 | type: object 483 | 484 | required: 485 | - name 486 | - email 487 | - subject 488 | - message 489 | 490 | properties: 491 | name: 492 | description: Senders name. 493 | type: string 494 | minLength: 1 495 | maxLength: 70 496 | example: Emily Ward 497 | 498 | email: 499 | description: Senders email. 500 | type: string 501 | format: email 502 | example: eward@example.net 503 | 504 | subject: 505 | description: Title of the message. 506 | type: string 507 | minLength: 1 508 | maxLength: 70 509 | example: Questions About Press Opportunity 510 | 511 | message: 512 | description: Body of the message. 513 | type: string 514 | minLength: 1 515 | maxLength: 2048 516 | example: We are looking to write a piece about this website. 517 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/dependencies/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/dependencies/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/dependencies/python/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/dependencies/python/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | import time 4 | from botocore.exceptions import ClientError 5 | import logging 6 | from typing import Tuple 7 | import re 8 | import json 9 | from dataclasses import dataclass 10 | import datetime 11 | 12 | logger = logging.getLogger() 13 | logger.setLevel(logging.INFO) 14 | 15 | 16 | def get_ddb_connection( 17 | env: str = os.getenv("Environment", "") 18 | ) -> "botocore.client.DynamoDB": 19 | ddbclient = "" 20 | if env == "local": 21 | ddbclient = boto3.client("dynamodb", endpoint_url="http://localstack:4566/") 22 | else: 23 | ddbclient = boto3.client("dynamodb") 24 | return ddbclient 25 | 26 | 27 | def layer_test() -> str: 28 | return "hello world" 29 | 30 | 31 | @dataclass 32 | class Field_Validator: 33 | is_valid: str 34 | message: str 35 | 36 | 37 | class DB_Response: 38 | def __init__( 39 | self, success: bool, status_code: int, message: str, json_data: str = None): 40 | self.message = message 41 | self.status_code = status_code 42 | self.success = success 43 | self.json_data = json_data 44 | 45 | def is_successful(self) -> bool: 46 | return self.success 47 | 48 | def get_dict_form(self) -> dict: 49 | return { 50 | "messsage": self.message, 51 | "success": self.success, 52 | "status_code": self.status_code, 53 | "json_data": self.json_data, 54 | } 55 | 56 | def set_json_data(self, json_data: str): 57 | self.json_data = json_data 58 | 59 | def api_format(self) -> dict: 60 | if self.json_data: 61 | body = self.json_data 62 | else: 63 | body = json.dumps({"message": self.message}) 64 | return { 65 | "statusCode": self.status_code, 66 | "headers": { 67 | "Content-Type": "application/json", 68 | "Access-Control-Allow-Origin": "*", 69 | }, 70 | "body": (body), 71 | } 72 | 73 | 74 | class DB_Item: 75 | 76 | REQUIRED_FIELDS = [] 77 | ITEM_TYPES = {} 78 | TABLE_NAME = "" 79 | FIELD_VALIDATION = {} 80 | STAGE = os.getenv("Stage", "") 81 | 82 | def __init__(self, db_client: "botocore.client.DynamoDB"): 83 | self.db_client = db_client 84 | pass 85 | 86 | def add_item(self, conditional_expression=None) -> DB_Response: 87 | """ 88 | adds item to database 89 | Parameters: 90 | conditional_expression (str): conditional expression for boto3 function put_item 91 | 92 | Returns: 93 | db_response (DB_Response): returns a DB_Response 94 | """ 95 | field_validation = self.validate_fields() 96 | if not field_validation.is_valid: 97 | return DB_Response( 98 | message=field_validation.message, status_code=400, success=False 99 | ) 100 | item = self.format_dynamodb_item_v2() 101 | try: 102 | if conditional_expression: 103 | self.db_client.put_item( 104 | TableName=self.TABLE_NAME, 105 | Item=item, 106 | ConditionExpression=conditional_expression, 107 | ) 108 | else: 109 | self.db_client.put_item(TableName=self.TABLE_NAME, Item=item) 110 | except self.db_client.exceptions.ConditionalCheckFailedException as e: 111 | return DB_Response( 112 | message=f"{self.TABLE_NAME} already exists, pick a different Name", 113 | status_code=409, 114 | success=False, 115 | ) 116 | except self.db_client.exceptions.ResourceNotFoundException as e: 117 | message = f"Cannot do operations on a non-existent table: {self.TABLE_NAME}" 118 | logging.error(message) 119 | return DB_Response(message=message, status_code=500, success=False) 120 | except ClientError as e: 121 | logging.error(e) 122 | return DB_Response( 123 | message="Unexpected AWS service exception", 124 | status_code=500, 125 | success=False, 126 | ) 127 | return DB_Response( 128 | message=f"{self.TABLE_NAME} was succesfully added", 129 | status_code=201, 130 | success=True, 131 | ) 132 | 133 | def update_item(self): 134 | pass 135 | 136 | def get_item(self, primary_key): 137 | pass 138 | 139 | def delete_item(self): 140 | pass 141 | 142 | def get_all_items(self): 143 | """ 144 | Gets all the Fridge Items 145 | NOTE: This function is probably fine for the fridges in NYC. But as more fridges 146 | are added to the database, this will be a bottleneck. Ideally we would be querying 147 | based on proximity. Here is an option for if we ever need to transition: 148 | https://hometechtime.com/how-to-build-a-dynamodb-geo-database-to-store-and-query-geospatial-data/ 149 | """ 150 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.scan 151 | return self.db_client.scan(TableName=self.TABLE_NAME) 152 | 153 | def has_required_fields(self) -> tuple: 154 | for field in self.REQUIRED_FIELDS: 155 | if getattr(self, field) is None: 156 | return (False, field) 157 | return (True, None) 158 | 159 | def get_class_field_value(self, key: str): 160 | """ 161 | Gets the class field value based on a key. 162 | The parameter key can be the class field name or in the case of a dictionary can contain "/" 163 | "/" is used when a class field is a dictionary and the client wants to get the value of a child field 164 | Example: location: {geoLng: 23.4323} 165 | key = "location/geoLng" 166 | In order to get the value of geoLng the client would use the key "location/geoLng" 167 | Parameters: 168 | key (str): the name of the class field the client wants to obtain the value of 169 | 170 | Returns: 171 | object_dict (dict): the class field value 172 | """ 173 | class_field_value = None 174 | if "/" in key: 175 | key_split = key.split("/") 176 | parent_key = key_split[0] 177 | class_field_value = getattr(self, parent_key) 178 | for i in range(1, len(key_split)): 179 | if class_field_value is None: 180 | break 181 | child_key = key_split[i] 182 | class_field_value = class_field_value.get(child_key, None) 183 | else: 184 | class_field_value = getattr(self, key) 185 | return class_field_value 186 | 187 | def format_dynamodb_item_v2(self): 188 | """ 189 | Creates a dictionary in the syntax that dynamodb expects 190 | https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.put_item 191 | """ 192 | fridge_item = {} 193 | fridge_json = {} 194 | for key in self.FIELD_VALIDATION: 195 | if "/" in key: 196 | continue 197 | val = getattr(self, key) 198 | if val is not None: 199 | fridge_json[key] = val 200 | if isinstance(val, int) and not isinstance(val, bool): 201 | val = str(val) 202 | if isinstance(val, dict): 203 | val = json.dumps(val) 204 | elif isinstance(val, list): 205 | fridge_json[key] = val.copy() 206 | list_type = self.FIELD_VALIDATION[key]["list_type"] 207 | val = val.copy() 208 | for index, v in enumerate(val): 209 | val[index] = {list_type: v} 210 | fridge_item[key] = {self.FIELD_VALIDATION[key]["type"]: val} 211 | fridge_item["json_data"] = {"S": json.dumps(fridge_json)} 212 | return fridge_item 213 | 214 | @staticmethod 215 | def process_fields(object_dict: dict) -> dict: 216 | """ 217 | Removes extra white spaces, trailing spaces, leading spaces from object_dict values 218 | If the length of the field is ZERO after removing the white spaces, sets the value to None 219 | Parameters: 220 | object_dict (dict): a dictinary 221 | 222 | Returns: 223 | object_dict (dict): 224 | """ 225 | for key, value in object_dict.items(): 226 | if isinstance(value, str): 227 | object_dict[key] = DB_Item.remove_extra_whitespace(value) 228 | if isinstance(value, dict): 229 | object_dict[key] = DB_Item.process_fields(value) 230 | if isinstance(value, list): 231 | for index, val in enumerate(value): 232 | if isinstance(val, str): 233 | value[index] = DB_Item.remove_extra_whitespace(val) 234 | return object_dict 235 | 236 | @staticmethod 237 | def remove_extra_whitespace(value: str): 238 | """ 239 | Removes extra white spaces, trailing spaces, and leading spaces 240 | Example: Input: " hi there " Output: "hi there" 241 | Parameters: 242 | value (str): a string 243 | Returns: 244 | value (str): the value parameter without extra white spaces, 245 | trailing spaces, and leading spaces 246 | """ 247 | value = re.sub(" +", " ", value).strip() 248 | if len(value) == 0: 249 | value = None 250 | return value 251 | 252 | def validate_fields(self) -> Field_Validator: 253 | """ 254 | Validates that all the fields are valid. 255 | All fields are valid if they pass all the constraints set in FIELD_VALIDATION 256 | """ 257 | for key, field_validation in self.FIELD_VALIDATION.items(): 258 | class_field_value = self.get_class_field_value(key) 259 | is_not_none = class_field_value is not None 260 | required = field_validation.get("required", None) 261 | min_length = field_validation.get("min_length", None) 262 | max_length = field_validation.get("max_length", None) 263 | choices = field_validation.get("choices", None) 264 | if required: 265 | if class_field_value is None: 266 | return Field_Validator( 267 | is_valid=False, message=f"Missing Required Field: {key}" 268 | ) 269 | if min_length and is_not_none: 270 | if len(str(class_field_value)) < min_length: 271 | return Field_Validator( 272 | is_valid=False, 273 | message=f"{key} character length must be >= {min_length}", 274 | ) 275 | if max_length and is_not_none: 276 | if len(str(class_field_value)) > max_length: 277 | return Field_Validator( 278 | is_valid=False, 279 | message=f"{key} character length must be <= {max_length}", 280 | ) 281 | if choices and is_not_none: 282 | if class_field_value not in choices: 283 | return Field_Validator( 284 | is_valid=False, 285 | message=f"{key} must to be one of: {str(choices)}", 286 | ) 287 | return Field_Validator( 288 | is_valid=True, message="All Fields Were Successfully Validated" 289 | ) 290 | 291 | def warm_lambda(self): 292 | key = {"id": {"S": "collectivefocusresourcehub"}} 293 | self.db_client.get_item(TableName=self.TABLE_NAME, Key=key) 294 | 295 | 296 | class Fridge(DB_Item): 297 | MIN_ID_LENGTH = 3 298 | MAX_ID_LENGTH = 100 299 | MAX_NAME_LENGTH = MAX_ID_LENGTH + 10 300 | FIELD_VALIDATION = { 301 | "id": { 302 | "required": True, 303 | "min_length": MIN_ID_LENGTH, 304 | "max_length": MAX_ID_LENGTH, 305 | "type": "S", 306 | }, 307 | "name": { 308 | "required": True, 309 | "min_length": MIN_ID_LENGTH, 310 | "max_length": MAX_NAME_LENGTH, 311 | "type": "S", 312 | }, 313 | "tags": {"required": False, "type": "L", "list_type": "S"}, 314 | "location": {"required": True, "type": "S"}, 315 | "location/name": {"required": False, "max_length": 256}, 316 | "location/street": {"required": False, "max_length": 256}, 317 | "location/city": {"required": False, "max_length": 256}, 318 | "location/state": {"required": False, "max_length": 256}, 319 | "location/zip": {"required": False, "max_length": 10}, 320 | "location/geoLat": {"required": True, "max_length": 20}, 321 | "location/geoLng": {"required": True, "max_length": 20}, 322 | "location/country": {"required": False, "max_length": 256}, 323 | "maintainer": {"required": False, "type": "S"}, 324 | "maintainer/name": {"required": False, "max_length": 256}, 325 | "maintainer/organization": {"required": False, "max_length": 256}, 326 | "maintainer/phone": {"required": False, "min_length": 10, "max_length": 20}, 327 | "maintainer/email": {"required": False, "max_length": 320}, 328 | "maintainer/website": {"required": False, "max_length": 2048}, 329 | "maintainer/instagram": {"required": False, "max_length": 64}, 330 | "notes": {"required": False, "max_length": 700, "type": "S"}, 331 | "food_accepts": {"required": False, "type": "L", "list_type": "S"}, 332 | "food_restrictions": {"required": False, "type": "L", "list_type": "S"}, 333 | "photoUrl": { 334 | "required": False, 335 | "max_length": 2048, 336 | "type": "S", 337 | }, 338 | "last_edited": {"required": False, "type": "N", "max_length": 20}, 339 | "verified": {"required": False, "type": "BOOL"}, 340 | "latestFridgeReport": {"required": False, "type": "S"}, 341 | "latestFridgeReport/epochTimestamp": {"required": False}, 342 | "latestFridgeReport/timestamp": {"required": False}, 343 | "latestFridgeReport/condition": {"required": False}, 344 | "latestFridgeReport/foodPercentage": {"required": False}, 345 | "latestFridgeReport/photoUrl": {"required": False}, 346 | "latestFridgeReport/notes": {"required": False}, 347 | } 348 | 349 | TABLE_NAME = f"fridge_{DB_Item.STAGE}" 350 | FOOD_ACCEPTS = [] # TODO: Fill this in 351 | FOOD_RESTRICTIONS = [] # TODO: fill this in 352 | 353 | def __init__(self, db_client: "botocore.client.DynamoDB", fridge: dict = None): 354 | super().__init__(db_client=db_client) 355 | if fridge is not None: 356 | fridge = DB_Item.process_fields(fridge) 357 | self.id: str = fridge.get("id", None) 358 | self.name: str = fridge.get("name", None) 359 | self.tags: list = fridge.get("tags", None) 360 | self.location: dict = fridge.get("location", None) 361 | self.maintainer: dict = fridge.get("maintainer", None) 362 | self.notes: str = fridge.get("notes", None) 363 | self.food_accepts: list = fridge.get("food_accepts", None) 364 | self.food_restrictions: list = fridge.get("food_restrictions", None) 365 | self.photoUrl: str = fridge.get("photoUrl", None) 366 | self.last_edited: str = fridge.get("last_edited", None) 367 | self.latestFridgeReport: dict = fridge.get("latestFridgeReport", None) 368 | self.verified: bool = fridge.get("verified", None) 369 | 370 | def get_item(self, fridgeId): 371 | key = {"id": {"S": fridgeId}} 372 | result = self.db_client.get_item(TableName=self.TABLE_NAME, Key=key) 373 | if "Item" not in result: 374 | return DB_Response( 375 | success=False, status_code=404, message="Fridge was not found" 376 | ) 377 | else: 378 | json_data = result["Item"]["json_data"]["S"] 379 | return DB_Response( 380 | success=True, 381 | status_code=200, 382 | message="Successfully Found Fridge", 383 | json_data=json_data, 384 | ) 385 | 386 | def get_latest_report(self, fridgeId): 387 | key = {"id": {"S": fridgeId}} 388 | result = self.db_client.get_item(TableName=self.TABLE_NAME, Key=key) 389 | if "Item" not in result: 390 | return DB_Response( 391 | success=False, status_code=404, message="Fridge was not found" 392 | ) 393 | else: 394 | json_data = result["Item"]["json_data"]["S"] 395 | dict_data = json.loads(json_data) 396 | latestFridgeReport = dict_data.get("latestFridgeReport", None) 397 | response = [] 398 | if latestFridgeReport is not None: 399 | response.append(latestFridgeReport) 400 | return DB_Response( 401 | success=True, 402 | status_code=200, 403 | message="Successfully Found Fridge", 404 | json_data=json.dumps(response), 405 | ) 406 | 407 | def get_latest_fridge_reports(self): 408 | response = self.db_client.scan(TableName=self.TABLE_NAME) 409 | all_reports = [] 410 | 411 | if "Items" in response: 412 | for item in response['Items']: 413 | json_data = item.get("json_data", {}).get("S", "{}") 414 | dict_data = json.loads(json_data) 415 | latestFridgeReport = dict_data.get("latestFridgeReport", None) 416 | if latestFridgeReport is not None: 417 | all_reports.append(latestFridgeReport) 418 | 419 | return DB_Response( 420 | success=True, 421 | status_code=200, 422 | message="Successfully found fridge reports", 423 | json_data=json.dumps(all_reports), 424 | ) 425 | 426 | 427 | def get_items(self, tag=None): 428 | # scan/query doc: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/dynamodb.html#querying-and-scanning 429 | response = None 430 | if tag: 431 | response = self.db_client.scan( 432 | TableName=self.TABLE_NAME, 433 | FilterExpression="contains (tags, :tag)", 434 | ExpressionAttributeValues={":tag": {"S": tag}}, 435 | ProjectionExpression="json_data", 436 | ) 437 | else: 438 | response = self.db_client.scan( 439 | TableName=self.TABLE_NAME, ProjectionExpression="json_data" 440 | ) 441 | json_data_list = [item["json_data"]["S"] for item in response["Items"]] 442 | # Converts list of json to json. 443 | json_response = f"[{','.join(json_data_list)}]" 444 | return DB_Response( 445 | success=True, 446 | status_code=200, 447 | message="Query Successfully Completed", 448 | json_data=json_response, 449 | ) 450 | 451 | def add_items(self): 452 | pass 453 | 454 | def set_id(self): 455 | """ 456 | Sets the Fridge id 457 | Fridge id is the Fridge name with certain characters removed if not URL complient 458 | """ 459 | if self.name is not None: 460 | id = re.sub(r"[^a-zA-Z0-9\-_~]+", "", self.name.lower()) 461 | self.id = id 462 | 463 | @staticmethod 464 | def is_valid_id(fridgeId: str) -> tuple[bool, str]: 465 | """ 466 | Checks if the fridge is id valid. A valid fridge id is alphanumeric and 467 | must have character length >= 3 and <= 32 468 | """ 469 | if fridgeId is None: 470 | return False, "Missing Required Field: id" 471 | if re.search('[^A-Za-z0-9\-._~:/?#\[\]@!$&\'()*+,;=]', fridgeId): 472 | return False, "id has invalid characters" 473 | id_length = len(fridgeId) 474 | is_valid_id_length = Fridge.MIN_ID_LENGTH <= id_length <= Fridge.MAX_ID_LENGTH 475 | if not is_valid_id_length: 476 | return ( 477 | False, 478 | f"id Must Have A Character Length >= {Fridge.MIN_ID_LENGTH} and <= {Fridge.MAX_ID_LENGTH}", 479 | ) 480 | return True, "success" 481 | 482 | def set_last_edited(self): 483 | """ 484 | Sets last_edited to the current epoch time. Timezone defaults to UTC 485 | """ 486 | self.last_edited = str(int(time.time())) 487 | 488 | def add_item(self, conditional_expression=None) -> DB_Response: 489 | """ 490 | Adds a fridge item to the database 491 | """ 492 | self.set_id() 493 | self.set_last_edited() 494 | conditional_expression = "attribute_not_exists(id)" 495 | db_response = super().add_item(conditional_expression=conditional_expression) 496 | if db_response.status_code == 201: 497 | db_response.set_json_data(json.dumps({"id": self.id})) 498 | return db_response 499 | 500 | def get_fridge_locations(self): 501 | pass 502 | 503 | def update_fridge_report(self, fridgeId: str, fridge_report: dict) -> DB_Response: 504 | """ 505 | Updates latestFridgeReport field with new Fridge Report. 506 | This function is called when a new FridgeReport is added to the database 507 | """ 508 | db_reponse = self.get_item(fridgeId=fridgeId) 509 | if not db_reponse.is_successful(): 510 | # Fridge was not found 511 | return db_reponse 512 | fridge_dict = json.loads(db_reponse.json_data) 513 | fridge_dict["latestFridgeReport"] = fridge_report 514 | fridge_json_data = json.dumps(fridge_dict) 515 | latestFridgeReport = json.dumps(fridge_report) 516 | """ 517 | JD and #LFR are mapped to the values set in ExpressionAttributeNames 518 | :fj and :fr are mapped to the values set in ExpressionAttributeValues 519 | UpdateExpression becomes: "json_data": {"S": fridge_json_data}, "latestFridgeReport": {"S": latestFridgeReport} 520 | """ 521 | self.db_client.update_item( 522 | TableName=self.TABLE_NAME, 523 | Key={"id": {"S": fridgeId}}, 524 | ExpressionAttributeNames={"#LFR": "latestFridgeReport", "#JD": "json_data"}, 525 | ExpressionAttributeValues={ 526 | ":fr": {"S": latestFridgeReport}, 527 | ":fj": {"S": fridge_json_data}, 528 | }, 529 | UpdateExpression="SET #JD = :fj, #LFR = :fr", 530 | ) 531 | return DB_Response( 532 | success=True, status_code=201, message="fridge_report was succesfully added" 533 | ) 534 | 535 | 536 | # good, dirty, out of order, not at location 537 | class FridgeReport(DB_Item): 538 | TABLE_NAME = f"fridge_report_{DB_Item.STAGE}" 539 | VALID_CONDITIONS = {"good", "dirty", "out of order", "not at location", "ghost"} 540 | VALID_FOOD_PERCENTAGE = {0, 1, 2, 3} 541 | FIELD_VALIDATION = { 542 | "notes": {"required": False, "max_length": 256, "type": "S"}, 543 | "fridgeId": { 544 | "required": True, 545 | "min_length": Fridge.MIN_ID_LENGTH, 546 | "max_length": Fridge.MAX_ID_LENGTH, 547 | "type": "S", 548 | }, 549 | "photoUrl": { 550 | "required": False, 551 | "max_length": 2048, 552 | "type": "S", 553 | }, 554 | "epochTimestamp": {"required": False, "type": "N"}, 555 | "timestamp": {"required": False, "type": "S"}, 556 | "condition": {"required": True, "type": "S", "choices": VALID_CONDITIONS}, 557 | "foodPercentage": { 558 | "required": True, 559 | "type": "N", 560 | "choices": VALID_FOOD_PERCENTAGE, 561 | }, 562 | } 563 | 564 | def __init__( 565 | self, db_client: "botocore.client.DynamoDB", fridge_report: dict = None 566 | ): 567 | super().__init__(db_client=db_client) 568 | if fridge_report is not None: 569 | fridge_report = self.process_fields(fridge_report) 570 | self.notes: str = fridge_report.get("notes", None) 571 | self.condition: str = fridge_report.get("condition", None) 572 | self.photoUrl: str = fridge_report.get("photoUrl", None) 573 | self.fridgeId: str = fridge_report.get("fridgeId", None) 574 | self.foodPercentage: int = fridge_report.get("foodPercentage", None) 575 | # timestamp and epochTimestamp have the same time but in different formats 576 | self.timestamp: str = fridge_report.get( 577 | "timestamp", None 578 | ) # ISO formatted timestamp for api clients 579 | self.epochTimestamp: str = fridge_report.get( 580 | "epochTimestamp", None 581 | ) # Epoch timestamp for querying 582 | 583 | def set_timestamp(self): 584 | """ 585 | Sets epochTimestamp and timestamp fields 586 | timestamp is ISO formatted date/time and is what the API user will use 587 | epochTimestamp will be what is used to query the database 588 | """ 589 | self.epochTimestamp = str(int(time.time())) 590 | utc_time = datetime.datetime.utcnow() 591 | self.timestamp = utc_time.strftime("%Y-%m-%dT%H:%M:%SZ") 592 | 593 | def object_to_dict(self): 594 | """ 595 | Converts object into a dictionary 596 | """ 597 | object_dict = {} 598 | for key in self.FIELD_VALIDATION: 599 | val = getattr(self, key) 600 | if val is not None: 601 | object_dict[key] = val 602 | return object_dict 603 | 604 | def add_item(self) -> DB_Response: 605 | self.set_timestamp() 606 | fridge_report_dict = self.object_to_dict() 607 | db_response = super().add_item() 608 | if not db_response.is_successful(): 609 | return db_response 610 | #updates the latestFridgeReport field in the Fridge table 611 | Fridge(db_client=self.db_client).update_fridge_report(fridgeId=self.fridgeId, fridge_report=fridge_report_dict) 612 | db_response.set_json_data(json.dumps({'fridgeId': self.fridgeId, 'timestamp': self.timestamp})) 613 | return db_response 614 | 615 | 616 | class FridgeHistory(DB_Item): 617 | def __init__(self, db_client: "botocore.client.DynamoDB"): 618 | super().__init__(db_client=db_client) 619 | 620 | 621 | class Tag(DB_Item): 622 | REQUIRED_FIELDS = ["tag_name"] 623 | TABLE_NAME = f"tag_{DB_Item.STAGE}" 624 | # Tag Class constants 625 | MIN_TAG_LENGTH = 3 626 | MAX_TAG_LENGTH = 32 627 | 628 | def __init__(self, db_client: "botocore.client.DynamoDB", tag_name: str = None): 629 | super().__init__(db_client=db_client) 630 | self.tag_name = self.format_tag(tag_name) 631 | 632 | def format_tag(self, tag_name: str) -> str: 633 | # tag_name is alphanumeric, all lowercased, may include hyphen and underscore but no spaces. 634 | if tag_name: 635 | tag_name = tag_name.lower().replace(" ", "") 636 | return tag_name 637 | 638 | @staticmethod 639 | def is_valid_tag_name(tag_name: str) -> Tuple[bool, str]: 640 | # valid tag name is alphanumeric, all lowercased, may include hyphen and underscore but no spaces. 641 | if tag_name is None: 642 | message = "Missing required fields: tag_name" 643 | return False, message 644 | length_tag_name = len(tag_name) 645 | is_tag_length_valid = ( 646 | length_tag_name >= Tag.MIN_TAG_LENGTH 647 | and length_tag_name <= Tag.MAX_TAG_LENGTH 648 | ) 649 | if tag_name and is_tag_length_valid: 650 | for x in tag_name: 651 | if not x.isalnum() and x not in ["_", "-"]: 652 | message = "tag_name contains invalid characters" 653 | return False, message 654 | message = "" 655 | return True, message 656 | else: 657 | message = f"Length of tag_name is {length_tag_name}. It should be >= {Tag.MIN_TAG_LENGTH} but <= {Tag.MAX_TAG_LENGTH}." 658 | return False, message 659 | 660 | def add_item(self) -> DB_Response: 661 | has_required_fields, field = self.has_required_fields() 662 | if not has_required_fields: 663 | return DB_Response( 664 | message=f"Missing Required Field: {field}", 665 | status_code=400, 666 | success=False, 667 | ) 668 | is_valid_field = self.is_valid_tag_name(self.tag_name) 669 | if not is_valid_field: 670 | return DB_Response( 671 | message=f"Tag Name Can Only Contain Letters, Numbers, Hyphens and Underscore: {self.tag_name}", 672 | status_code=400, 673 | success=False, 674 | ) 675 | item = {"tag_name": {"S": self.tag_name}} 676 | 677 | try: 678 | self.db_client.put_item(TableName=self.TABLE_NAME, Item=item) 679 | except self.db_client.exceptions.ResourceNotFoundException as e: 680 | message = f"Cannot do operations on a non-existent table: {Tag.TABLE_NAME}" 681 | logging.error(message) 682 | return DB_Response(message=message, status_code=500, success=False) 683 | except ClientError as e: 684 | logging.error(e) 685 | return DB_Response( 686 | message="Unexpected AWS service exception", 687 | status_code=500, 688 | success=False, 689 | ) 690 | return DB_Response( 691 | message="Tag was succesfully added", status_code=200, success=True 692 | ) 693 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/dependencies/python/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/dependencies/python/s3_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | from urllib.parse import urlparse, urlunparse 4 | import logging 5 | import boto3 6 | import botocore 7 | from botocore.exceptions import ClientError 8 | from botocore.config import Config 9 | 10 | def get_s3_client(env): 11 | endpoint_url="http://localstack:4566/" if env == "local" else None 12 | config = Config( 13 | signature_version=botocore.UNSIGNED, # Do not include signatures in s3 presigned-urls. 14 | ) 15 | 16 | return boto3.client( 17 | "s3", 18 | config=config, 19 | endpoint_url=endpoint_url, 20 | ) 21 | 22 | def translate_s3_url_for_client(url: str, env=os.getenv("Environment")) -> str: 23 | """ 24 | This function translates a S3 url to a client-accessible urls. 25 | 26 | From a lambda function's perspective, local AWS gateway is located at "localstack:4566" (within cfm-network). 27 | However, this hostname is not available for the host machine. 28 | In order to fetch S3 files from a local browser, we need to use "localhost:4566". 29 | """ 30 | if env == "local": 31 | parsed_url = urlparse(url) 32 | return urlunparse(parsed_url._replace(netloc="localhost:4566")) 33 | return url 34 | 35 | 36 | class S3ServiceException(Exception): 37 | pass 38 | 39 | 40 | class S3Service: 41 | """ 42 | Adapter class for persisting binary files in S3 buckets. 43 | """ 44 | def __init__(self, env=os.getenv("Environment")): 45 | self._env = env 46 | self._client = get_s3_client(env) 47 | 48 | def idempotent_create_bucket(self, bucket: str): 49 | """ 50 | Creates a bucket if there is no existing bucket with the same name. 51 | No-op when not local. 52 | Parameters: 53 | bucket: name of the bucket 54 | """ 55 | if self._env != "local": 56 | return 57 | try: 58 | self._client.create_bucket(Bucket=bucket) 59 | except ClientError as e: 60 | logging.error(e) 61 | raise S3ServiceException(f"Failed to create a bucket {bucket}") 62 | 63 | def write(self, bucket: str, content_type: str, blob: bytes): 64 | """ 65 | writes a binary file to the storage. 66 | Parameters: 67 | bucket: bucket to put file into 68 | extension: file extension 69 | blob: binary data to be written 70 | Returns: 71 | The key of the newly created file 72 | """ 73 | extension = content_type.split("/")[1] 74 | key = f"{str(uuid.uuid4())}.{extension}" 75 | self.idempotent_create_bucket(bucket) 76 | 77 | try: 78 | self._client.put_object( 79 | Bucket=bucket, 80 | Key=key, 81 | Body=blob, 82 | ContentType=content_type 83 | ) 84 | except ClientError as e: 85 | logging.error(e) 86 | raise S3ServiceException(f"Failed to save file {key} in bucket {bucket}") 87 | 88 | return key 89 | 90 | def generate_file_url(self, bucket: str, key: str): 91 | """ 92 | generates an url for the client to access a persisted file. 93 | Parameters: 94 | bucket: name of the bucket that contains the file 95 | key: key of the file within the bucket 96 | Returns: 97 | A public url for the specified file 98 | """ 99 | try: 100 | url = self._client.generate_presigned_url( 101 | "get_object", 102 | Params={ 103 | "Bucket": bucket, 104 | "Key": key, 105 | }, 106 | ExpiresIn=0, 107 | ) 108 | except ClientError as e: 109 | logging.error(e) 110 | raise S3ServiceException(f"Failed to generate url for file {key} in bucket {bucket}") 111 | 112 | return translate_s3_url_for_client(url) 113 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"message\": \"hello world\"}", 3 | "resource": "/hello", 4 | "path": "/hello", 5 | "httpMethod": "GET", 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 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/local-contact-email.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"name\": \"Test User\", \"email\": \"fridgefinderapp@gmail.com\", \"subject\": \"Test Subject\", \"message\": \"Test Message\"}", 3 | "resource": "/{proxy+}", 4 | "path": "/document", 5 | "pathParameters":{}, 6 | "queryStringParameters":{}, 7 | "httpMethod": "POST", 8 | "isBase64Encoded": false, 9 | "headers": { 10 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 11 | "Accept-Encoding": "gzip, deflate, sdch", 12 | "Accept-Language": "en-US,en;q=0.8", 13 | "Cache-Control": "max-age=0", 14 | "CloudFront-Forwarded-Proto": "https", 15 | "CloudFront-Is-Desktop-Viewer": "true", 16 | "CloudFront-Is-Mobile-Viewer": "false", 17 | "CloudFront-Is-SmartTV-Viewer": "false", 18 | "CloudFront-Is-Tablet-Viewer": "false", 19 | "CloudFront-Viewer-Country": "US", 20 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 21 | "Upgrade-Insecure-Requests": "1", 22 | "User-Agent": "Custom User Agent String", 23 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 24 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 25 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 26 | "X-Forwarded-Port": "443", 27 | "X-Forwarded-Proto": "https" 28 | }, 29 | "multiValueHeaders": { 30 | "Accept": [ 31 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 32 | ], 33 | "Accept-Encoding": [ 34 | "gzip, deflate, sdch" 35 | ], 36 | "Accept-Language": [ 37 | "en-US,en;q=0.8" 38 | ], 39 | "Cache-Control": [ 40 | "max-age=0" 41 | ], 42 | "CloudFront-Forwarded-Proto": [ 43 | "https" 44 | ], 45 | "CloudFront-Is-Desktop-Viewer": [ 46 | "true" 47 | ], 48 | "CloudFront-Is-Mobile-Viewer": [ 49 | "false" 50 | ], 51 | "CloudFront-Is-SmartTV-Viewer": [ 52 | "false" 53 | ], 54 | "CloudFront-Is-Tablet-Viewer": [ 55 | "false" 56 | ], 57 | "CloudFront-Viewer-Country": [ 58 | "US" 59 | ], 60 | "Host": [ 61 | "0123456789.execute-api.us-east-1.amazonaws.com" 62 | ], 63 | "Upgrade-Insecure-Requests": [ 64 | "1" 65 | ], 66 | "User-Agent": [ 67 | "Custom User Agent String" 68 | ], 69 | "Via": [ 70 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 71 | ], 72 | "X-Amz-Cf-Id": [ 73 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 74 | ], 75 | "X-Forwarded-For": [ 76 | "127.0.0.1, 127.0.0.2" 77 | ], 78 | "X-Forwarded-Port": [ 79 | "443" 80 | ], 81 | "X-Forwarded-Proto": [ 82 | "https" 83 | ] 84 | }, 85 | "requestContext": { 86 | "accountId": "123456789012", 87 | "resourceId": "123456", 88 | "stage": "prod", 89 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 90 | "requestTime": "09/Apr/2015:12:34:56 +0000", 91 | "requestTimeEpoch": 1428582896000, 92 | "identity": { 93 | "cognitoIdentityPoolId": null, 94 | "accountId": null, 95 | "cognitoIdentityId": null, 96 | "caller": null, 97 | "accessKey": null, 98 | "sourceIp": "127.0.0.1", 99 | "cognitoAuthenticationType": null, 100 | "cognitoAuthenticationProvider": null, 101 | "userArn": null, 102 | "userAgent": "Custom User Agent String", 103 | "user": null 104 | }, 105 | "path": "/prod/document", 106 | "resourcePath": "/{proxy+}", 107 | "httpMethod": "POST", 108 | "apiId": "1234567890", 109 | "protocol": "HTTP/1.1" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/local-event-get-fridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "", 3 | "resource": "/{proxy+}", 4 | "path": "/document", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "proxy": "/document", 17 | "fridgeId": "2fish5loavesfridge" 18 | }, 19 | "stageVariables": { 20 | "baz": "qux" 21 | }, 22 | "headers": { 23 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 24 | "Accept-Encoding": "gzip, deflate, sdch", 25 | "Accept-Language": "en-US,en;q=0.8", 26 | "Cache-Control": "max-age=0", 27 | "CloudFront-Forwarded-Proto": "https", 28 | "CloudFront-Is-Desktop-Viewer": "true", 29 | "CloudFront-Is-Mobile-Viewer": "false", 30 | "CloudFront-Is-SmartTV-Viewer": "false", 31 | "CloudFront-Is-Tablet-Viewer": "false", 32 | "CloudFront-Viewer-Country": "US", 33 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 34 | "Upgrade-Insecure-Requests": "1", 35 | "User-Agent": "Custom User Agent String", 36 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 37 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 38 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 39 | "X-Forwarded-Port": "443", 40 | "X-Forwarded-Proto": "https" 41 | }, 42 | "multiValueHeaders": { 43 | "Accept": [ 44 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 45 | ], 46 | "Accept-Encoding": [ 47 | "gzip, deflate, sdch" 48 | ], 49 | "Accept-Language": [ 50 | "en-US,en;q=0.8" 51 | ], 52 | "Cache-Control": [ 53 | "max-age=0" 54 | ], 55 | "CloudFront-Forwarded-Proto": [ 56 | "https" 57 | ], 58 | "CloudFront-Is-Desktop-Viewer": [ 59 | "true" 60 | ], 61 | "CloudFront-Is-Mobile-Viewer": [ 62 | "false" 63 | ], 64 | "CloudFront-Is-SmartTV-Viewer": [ 65 | "false" 66 | ], 67 | "CloudFront-Is-Tablet-Viewer": [ 68 | "false" 69 | ], 70 | "CloudFront-Viewer-Country": [ 71 | "US" 72 | ], 73 | "Host": [ 74 | "0123456789.execute-api.us-east-1.amazonaws.com" 75 | ], 76 | "Upgrade-Insecure-Requests": [ 77 | "1" 78 | ], 79 | "User-Agent": [ 80 | "Custom User Agent String" 81 | ], 82 | "Via": [ 83 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 84 | ], 85 | "X-Amz-Cf-Id": [ 86 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 87 | ], 88 | "X-Forwarded-For": [ 89 | "127.0.0.1, 127.0.0.2" 90 | ], 91 | "X-Forwarded-Port": [ 92 | "443" 93 | ], 94 | "X-Forwarded-Proto": [ 95 | "https" 96 | ] 97 | }, 98 | "requestContext": { 99 | "accountId": "123456789012", 100 | "resourceId": "123456", 101 | "stage": "prod", 102 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 103 | "requestTime": "09/Apr/2015:12:34:56 +0000", 104 | "requestTimeEpoch": 1428582896000, 105 | "identity": { 106 | "cognitoIdentityPoolId": null, 107 | "accountId": null, 108 | "cognitoIdentityId": null, 109 | "caller": null, 110 | "accessKey": null, 111 | "sourceIp": "127.0.0.1", 112 | "cognitoAuthenticationType": null, 113 | "cognitoAuthenticationProvider": null, 114 | "userArn": null, 115 | "userAgent": "Custom User Agent String", 116 | "user": null 117 | }, 118 | "path": "/prod/document", 119 | "resourcePath": "/{proxy+}", 120 | "httpMethod": "GET", 121 | "apiId": "1234567890", 122 | "protocol": "HTTP/1.1" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/local-event-get-fridges-with-tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "", 3 | "resource": "/{proxy+}", 4 | "path": "/document", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "tag": "tag3" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "proxy": "/document" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "09/Apr/2015:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/prod/document", 118 | "resourcePath": "/{proxy+}", 119 | "httpMethod": "GET", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/local-event-get-fridges.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "", 3 | "resource": "/{proxy+}", 4 | "path": "/document", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "proxy": "/document" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "09/Apr/2015:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/prod/document", 118 | "resourcePath": "/{proxy+}", 119 | "httpMethod": "GET", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/local-fridge-report-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"condition\": \"good\", \"foodPercentage\": 2}", 3 | "resource": "/{proxy+}", 4 | "path": "/document", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "proxy": "/document", 17 | "fridgeId": "2fish5loavesfridge" 18 | }, 19 | "stageVariables": { 20 | "baz": "qux" 21 | }, 22 | "headers": { 23 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 24 | "Accept-Encoding": "gzip, deflate, sdch", 25 | "Accept-Language": "en-US,en;q=0.8", 26 | "Cache-Control": "max-age=0", 27 | "CloudFront-Forwarded-Proto": "https", 28 | "CloudFront-Is-Desktop-Viewer": "true", 29 | "CloudFront-Is-Mobile-Viewer": "false", 30 | "CloudFront-Is-SmartTV-Viewer": "false", 31 | "CloudFront-Is-Tablet-Viewer": "false", 32 | "CloudFront-Viewer-Country": "US", 33 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 34 | "Upgrade-Insecure-Requests": "1", 35 | "User-Agent": "Custom User Agent String", 36 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 37 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 38 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 39 | "X-Forwarded-Port": "443", 40 | "X-Forwarded-Proto": "https" 41 | }, 42 | "multiValueHeaders": { 43 | "Accept": [ 44 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 45 | ], 46 | "Accept-Encoding": [ 47 | "gzip, deflate, sdch" 48 | ], 49 | "Accept-Language": [ 50 | "en-US,en;q=0.8" 51 | ], 52 | "Cache-Control": [ 53 | "max-age=0" 54 | ], 55 | "CloudFront-Forwarded-Proto": [ 56 | "https" 57 | ], 58 | "CloudFront-Is-Desktop-Viewer": [ 59 | "true" 60 | ], 61 | "CloudFront-Is-Mobile-Viewer": [ 62 | "false" 63 | ], 64 | "CloudFront-Is-SmartTV-Viewer": [ 65 | "false" 66 | ], 67 | "CloudFront-Is-Tablet-Viewer": [ 68 | "false" 69 | ], 70 | "CloudFront-Viewer-Country": [ 71 | "US" 72 | ], 73 | "Host": [ 74 | "0123456789.execute-api.us-east-1.amazonaws.com" 75 | ], 76 | "Upgrade-Insecure-Requests": [ 77 | "1" 78 | ], 79 | "User-Agent": [ 80 | "Custom User Agent String" 81 | ], 82 | "Via": [ 83 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 84 | ], 85 | "X-Amz-Cf-Id": [ 86 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 87 | ], 88 | "X-Forwarded-For": [ 89 | "127.0.0.1, 127.0.0.2" 90 | ], 91 | "X-Forwarded-Port": [ 92 | "443" 93 | ], 94 | "X-Forwarded-Proto": [ 95 | "https" 96 | ] 97 | }, 98 | "requestContext": { 99 | "accountId": "123456789012", 100 | "resourceId": "123456", 101 | "stage": "prod", 102 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 103 | "requestTime": "09/Apr/2015:12:34:56 +0000", 104 | "requestTimeEpoch": 1428582896000, 105 | "identity": { 106 | "cognitoIdentityPoolId": null, 107 | "accountId": null, 108 | "cognitoIdentityId": null, 109 | "caller": null, 110 | "accessKey": null, 111 | "sourceIp": "127.0.0.1", 112 | "cognitoAuthenticationType": null, 113 | "cognitoAuthenticationProvider": null, 114 | "userArn": null, 115 | "userAgent": "Custom User Agent String", 116 | "user": null 117 | }, 118 | "path": "/prod/document", 119 | "resourcePath": "/{proxy+}", 120 | "httpMethod": "POST", 121 | "apiId": "1234567890", 122 | "protocol": "HTTP/1.1" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/events/local-post-fridge-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"name\": \"The Greenpoint Fridge\",\"location\": {\"address\": \"9 W. Elm St.\", \"geoLat\": \"40.730610\", \"geoLng\": \"-73.935242\"}}", 3 | "resource": "/{proxy+}", 4 | "path": "/document", 5 | "pathParameters":{}, 6 | "queryStringParameters":{}, 7 | "httpMethod": "POST", 8 | "isBase64Encoded": false, 9 | "headers": { 10 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 11 | "Accept-Encoding": "gzip, deflate, sdch", 12 | "Accept-Language": "en-US,en;q=0.8", 13 | "Cache-Control": "max-age=0", 14 | "CloudFront-Forwarded-Proto": "https", 15 | "CloudFront-Is-Desktop-Viewer": "true", 16 | "CloudFront-Is-Mobile-Viewer": "false", 17 | "CloudFront-Is-SmartTV-Viewer": "false", 18 | "CloudFront-Is-Tablet-Viewer": "false", 19 | "CloudFront-Viewer-Country": "US", 20 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 21 | "Upgrade-Insecure-Requests": "1", 22 | "User-Agent": "Custom User Agent String", 23 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 24 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 25 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 26 | "X-Forwarded-Port": "443", 27 | "X-Forwarded-Proto": "https" 28 | }, 29 | "multiValueHeaders": { 30 | "Accept": [ 31 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 32 | ], 33 | "Accept-Encoding": [ 34 | "gzip, deflate, sdch" 35 | ], 36 | "Accept-Language": [ 37 | "en-US,en;q=0.8" 38 | ], 39 | "Cache-Control": [ 40 | "max-age=0" 41 | ], 42 | "CloudFront-Forwarded-Proto": [ 43 | "https" 44 | ], 45 | "CloudFront-Is-Desktop-Viewer": [ 46 | "true" 47 | ], 48 | "CloudFront-Is-Mobile-Viewer": [ 49 | "false" 50 | ], 51 | "CloudFront-Is-SmartTV-Viewer": [ 52 | "false" 53 | ], 54 | "CloudFront-Is-Tablet-Viewer": [ 55 | "false" 56 | ], 57 | "CloudFront-Viewer-Country": [ 58 | "US" 59 | ], 60 | "Host": [ 61 | "0123456789.execute-api.us-east-1.amazonaws.com" 62 | ], 63 | "Upgrade-Insecure-Requests": [ 64 | "1" 65 | ], 66 | "User-Agent": [ 67 | "Custom User Agent String" 68 | ], 69 | "Via": [ 70 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 71 | ], 72 | "X-Amz-Cf-Id": [ 73 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 74 | ], 75 | "X-Forwarded-For": [ 76 | "127.0.0.1, 127.0.0.2" 77 | ], 78 | "X-Forwarded-Port": [ 79 | "443" 80 | ], 81 | "X-Forwarded-Proto": [ 82 | "https" 83 | ] 84 | }, 85 | "requestContext": { 86 | "accountId": "123456789012", 87 | "resourceId": "123456", 88 | "stage": "prod", 89 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 90 | "requestTime": "09/Apr/2015:12:34:56 +0000", 91 | "requestTimeEpoch": 1428582896000, 92 | "identity": { 93 | "cognitoIdentityPoolId": null, 94 | "accountId": null, 95 | "cognitoIdentityId": null, 96 | "caller": null, 97 | "accessKey": null, 98 | "sourceIp": "127.0.0.1", 99 | "cognitoAuthenticationType": null, 100 | "cognitoAuthenticationProvider": null, 101 | "userArn": null, 102 | "userAgent": "Custom User Agent String", 103 | "user": null 104 | }, 105 | "path": "/prod/document", 106 | "resourcePath": "/{proxy+}", 107 | "httpMethod": "POST", 108 | "apiId": "1234567890", 109 | "protocol": "HTTP/1.1" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/README.md: -------------------------------------------------------------------------------- 1 | Directories under the **dev** directory will eventually get deleted 2 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/hello_world/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/hello_world/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/hello_world/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | try: 4 | from db import layer_test 5 | except: 6 | from dependencies.python.db import layer_test 7 | 8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers 9 | # Here is an example of someone having a similar issue: 10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror 11 | 12 | 13 | def lambda_handler(event, context): 14 | """Sample pure Lambda function 15 | 16 | Parameters 17 | ---------- 18 | event: dict, required 19 | API Gateway Lambda Proxy Input Format 20 | 21 | Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format 22 | 23 | context: object, required 24 | Lambda Context runtime methods and attributes 25 | 26 | Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 27 | 28 | Returns 29 | ------ 30 | API Gateway Lambda Proxy Output Format: dict 31 | 32 | Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html 33 | """ 34 | 35 | return { 36 | "statusCode": 200, 37 | "body": json.dumps( 38 | { 39 | "message": layer_test(), 40 | } 41 | ), 42 | } 43 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/hello_world/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/hello_world/requirements.txt -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/load_fridge_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/load_fridge_data/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/load_fridge_data/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | from botocore.exceptions import ClientError 5 | from db import get_ddb_connection 6 | from db import Fridge 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | FRIDGE_DATA = [ 12 | { 13 | "name": "The Friendly Fridge", 14 | "location": { 15 | "geoLat": 124242, 16 | "geoLng": 2345235, 17 | "street": "1046 Broadway", 18 | "zip": "11221", 19 | "state": "NY", 20 | "city": "Brooklyn", 21 | "name": "test name", 22 | }, 23 | "tags": ["tag1", "tag3"], 24 | "maintainer": {"instagram": "https://www.instagram.com/thefriendlyfridge/"}, 25 | }, 26 | { 27 | "name": "2 Fish 5 Loaves Fridge", 28 | "location": { 29 | "geoLat": 40.701730, 30 | "geoLng": -73.944530, 31 | "street": "63 Whipple St", 32 | "zip": "11206", 33 | "state": "NY", 34 | "city": "Brooklyn", 35 | "name": "test name", 36 | }, 37 | "tags": ["tag1", "tag4"], 38 | "maintainer": {"instagram": "https://www.instagram.com/2fish5loavesfridge/"}, 39 | }, 40 | ] 41 | 42 | 43 | FRIDGE_REPORT_DATA = [] 44 | FRIDGE_HISTORY_DATA = [] 45 | 46 | 47 | def lambda_handler( 48 | event: dict, context: "awslambdaric.lambda_context.LambdaContext" 49 | ) -> dict: 50 | db_client = get_ddb_connection(env=os.environ["Environment"]) 51 | try: 52 | responses = [] 53 | response = None 54 | for fridge in FRIDGE_DATA: 55 | response = Fridge(fridge=fridge, db_client=db_client).add_item() 56 | responses.append(response.get_dict_form()) 57 | if response.status_code != 201: 58 | break 59 | 60 | return { 61 | "statusCode": response.status_code, 62 | "isBase64Encoded": "false", 63 | "headers": {"Content-Type": "application/json"}, 64 | "body": json.dumps( 65 | { 66 | "responses": responses, 67 | } 68 | ), 69 | } 70 | 71 | except db_client.exceptions.ResourceNotFoundException as e: 72 | logging.error("Table does not exist") 73 | raise e 74 | except ClientError as e: 75 | logging.error("Unexpected error") 76 | raise e 77 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/dev/load_fridge_data/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/load_fridge_data/requirements.txt -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/email_service/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/email_service/v1/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/email_service/v1/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | from re import sub 4 | import boto3 5 | import json 6 | import logging 7 | from botocore.exceptions import ClientError 8 | 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | RECIPIENT = "fridgefinderapp@gmail.com" 13 | SENDER = "fridgefinderapp@gmail.com" 14 | 15 | 16 | def api_response(status_code, body) -> dict: 17 | return { 18 | "statusCode": status_code, 19 | "headers": { 20 | "Content-Type": "application/json", 21 | "Access-Control-Allow-Origin": "*", 22 | }, 23 | "body": body, 24 | } 25 | 26 | 27 | class SendEmail: 28 | 29 | CHARSET = "UTF-8" 30 | 31 | def __init__(self, client=boto3.client("ses", "us-east-1")): 32 | self.client = client 33 | 34 | def sendEmail(self, sender, recipient, subject, body) -> int: 35 | try: 36 | # Provide the contents of the email. 37 | response = self.client.send_email( 38 | Source=sender, 39 | Destination={ 40 | "ToAddresses": [recipient], 41 | }, 42 | Message={ 43 | "Body": { 44 | "Html": { 45 | "Data": body, 46 | }, 47 | "Text": { 48 | "Charset": SendEmail.CHARSET, 49 | "Data": body, 50 | }, 51 | }, 52 | "Subject": { 53 | "Charset": SendEmail.CHARSET, 54 | "Data": subject, 55 | }, 56 | } 57 | # we can use a configuationset for logging purposest 58 | # maybe we'd want to define the names in a list of constants/enums? 59 | # ConfigurationSetName=configuration_set, 60 | ) 61 | # Display an error if something goes wrong. 62 | except ClientError as e: 63 | logger.error(e.response["Error"]["Message"]) 64 | return api_response(404, "Unable to send Email. Please try again") 65 | else: 66 | messageId = response.get("MessageId", "Not Found") 67 | logger.info(f"Email sent! Message ID: {messageId}") 68 | return api_response( 69 | 200, json.dumps({"message": "Succesffully Sent Email!"}) 70 | ) 71 | 72 | 73 | class ContactHandler: 74 | def extract_body_fields(body): 75 | body = json.loads(body) 76 | sender = body.get("name", None) 77 | senderEmailAddress = body.get("email", None) 78 | subject = body.get("subject", "No Subject") 79 | subject = f"FridgeFinder Contact: {subject}." 80 | message = body.get("message", None) 81 | if subject and sender: 82 | subject = f"{subject} From: {sender}" 83 | return message, subject, sender, senderEmailAddress 84 | 85 | @staticmethod 86 | def lambda_handler(event: dict, client=boto3.client("ses", "us-east-1")) -> dict: 87 | body = event.get("body", {}) 88 | if not body: 89 | return api_response( 90 | 400, 91 | json.dumps( 92 | {"message": "Unable to send Email. Missing Required Fields"} 93 | ), 94 | ) 95 | ( 96 | message, 97 | subject, 98 | senderName, 99 | senderEmailAddress, 100 | ) = ContactHandler.extract_body_fields(body) 101 | if not senderName or not senderEmailAddress or not message: 102 | return api_response( 103 | "400", 104 | json.dumps( 105 | {"message": "Unable to send Email. Missing Required Fields"} 106 | ), 107 | ) 108 | response = SendEmail(client).sendEmail( 109 | SENDER, 110 | RECIPIENT, 111 | subject, 112 | ContactHandler.format_email(senderEmailAddress, message, senderName), 113 | ) 114 | return response 115 | 116 | @staticmethod 117 | def format_email(email, body, name): 118 | return f"From: {name} {email}\r\nMessage: {body}" 119 | 120 | 121 | def lambda_handler( 122 | event: dict, context: "awslambdaric.lambda_context.LambdaContext" 123 | ) -> dict: 124 | return ContactHandler.lambda_handler(event) 125 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/fridge_reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/fridge_reports/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/fridge_reports/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | 5 | try: 6 | from db import get_ddb_connection, FridgeReport, Fridge, DB_Response 7 | except: 8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers 9 | # Here is an example of someone having a similar issue: 10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror 11 | from dependencies.python.db import ( 12 | get_ddb_connection, 13 | FridgeReport, 14 | Fridge, 15 | DB_Response, 16 | ) 17 | 18 | logger = logging.getLogger() 19 | logger.setLevel(logging.INFO) 20 | 21 | 22 | class FridgReportHandler: 23 | def __init__(): 24 | pass 25 | 26 | @staticmethod 27 | def lambda_handler(event: dict, ddbclient: "botocore.client.DynamoDB") -> dict: 28 | """ 29 | Extracts the necessary data from events dict, and executes a function corresponding 30 | to the event httpMethod 31 | """ 32 | body = event.get("body", None) 33 | fridge_id = event.get("pathParameters", {}).get("fridgeId", None) 34 | if body is not None: 35 | body = json.loads(body) 36 | body["fridgeId"] = fridge_id 37 | httpMethod = event.get("httpMethod", None) 38 | db_response = None 39 | if httpMethod == "POST": 40 | db_response = FridgeReport( 41 | db_client=ddbclient, fridge_report=body 42 | ).add_item() 43 | elif httpMethod == "GET": 44 | db_response = Fridge(db_client=ddbclient).get_latest_report( 45 | fridgeId=fridge_id 46 | ) 47 | else: 48 | FridgeReport(db_client=ddbclient).warm_lambda() 49 | db_response = DB_Response(False, 400, "httpMethod missing") 50 | return db_response.api_format() 51 | 52 | 53 | def lambda_handler( 54 | event: dict, context: "awslambdaric.lambda_context.LambdaContext" 55 | ) -> dict: 56 | env = os.environ["Environment"] 57 | ddbclient = get_ddb_connection(env) 58 | return FridgReportHandler.lambda_handler(event, ddbclient) 59 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/fridge_reports/get_latest_fridge_reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/fridge_reports/get_latest_fridge_reports/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/fridge_reports/get_latest_fridge_reports/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | 5 | try: 6 | from db import get_ddb_connection, Fridge, DB_Response 7 | except: 8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers 9 | # Here is an example of someone having a similar issue: 10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror 11 | from dependencies.python.db import ( 12 | get_ddb_connection, 13 | Fridge, 14 | DB_Response, 15 | ) 16 | 17 | logger = logging.getLogger() 18 | logger.setLevel(logging.INFO) 19 | 20 | 21 | class GetLatestFridgeReportHandler: 22 | def __init__(): 23 | pass 24 | 25 | @staticmethod 26 | def lambda_handler(event: dict, ddbclient: "botocore.client.DynamoDB") -> dict: 27 | """ 28 | Extracts the necessary data from events dict, and executes a function corresponding 29 | to the event httpMethod 30 | """ 31 | httpMethod = event.get("httpMethod", None) 32 | db_response = None 33 | if httpMethod == "GET": 34 | db_response = Fridge(db_client=ddbclient).get_latest_fridge_reports() 35 | else: 36 | db_response = DB_Response(False, 400, "httpMethod missing") 37 | return db_response.api_format() 38 | 39 | 40 | def lambda_handler( 41 | event: dict, context: "awslambdaric.lambda_context.LambdaContext" 42 | ) -> dict: 43 | env = os.environ["Environment"] 44 | ddbclient = get_ddb_connection(env) 45 | return GetLatestFridgeReportHandler.lambda_handler(event, ddbclient) 46 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/fridges/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/fridges/v1/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/fridges/v1/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | 5 | try: 6 | from db import get_ddb_connection, Fridge, DB_Response 7 | except: 8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers 9 | # Here is an example of someone having a similar issue: 10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror 11 | from dependencies.python.db import get_ddb_connection, Fridge, DB_Response 12 | 13 | logger = logging.getLogger() 14 | logger.setLevel(logging.INFO) 15 | 16 | 17 | class FridgeHandler: 18 | @staticmethod 19 | def lambda_handler(event: dict, ddbclient: "botocore.client.DynamoDB") -> dict: 20 | """ 21 | Extracts the necessary data from events dict, and executes a function corresponding 22 | to the event httpMethod 23 | """ 24 | httpMethod = event.get("httpMethod", None) 25 | body = event.get("body", None) 26 | pathParameters = event.get("pathParameters", None) or {} 27 | queryStringParameters = event.get("queryStringParameters", None) or {} 28 | tag = queryStringParameters.get("tag", None) 29 | fridgeId = pathParameters.get("fridgeId", None) 30 | db_response = None 31 | if httpMethod == "GET": 32 | if fridgeId: 33 | db_response = Fridge(db_client=ddbclient).get_item(fridgeId) 34 | else: 35 | db_response = Fridge(db_client=ddbclient).get_items(tag=tag) 36 | elif httpMethod == "POST": 37 | if body is not None: 38 | body = json.loads(body) 39 | db_response = Fridge(db_client=ddbclient, fridge=body).add_item() 40 | else: 41 | Fridge(db_client=ddbclient).warm_lambda() 42 | db_response = DB_Response(False, 400, "httpMethod missing") 43 | return db_response.api_format() 44 | 45 | 46 | def lambda_handler( 47 | event: dict, context: "awslambdaric.lambda_context.LambdaContext" 48 | ) -> dict: 49 | env = os.environ["Environment"] 50 | ddbclient = get_ddb_connection(env) 51 | return FridgeHandler.lambda_handler(event, ddbclient) 52 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/image/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/image/v1/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/functions/image/v1/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | import os 4 | 5 | try: 6 | from s3_service import S3Service 7 | except: 8 | from dependencies.python.s3_service import S3Service 9 | 10 | 11 | def api_response(body, status_code) -> dict: 12 | return { 13 | "statusCode": status_code, 14 | "headers": { 15 | "Content-Type": "application/json", 16 | "Access-Control-Allow-Origin": "*", 17 | }, 18 | "body": (body), 19 | } 20 | 21 | def get_s3_bucket_name(): 22 | STAGE = os.getenv("Stage", None) 23 | bucket = "community-fridge-map-images" 24 | if STAGE is not None: 25 | bucket = f"{bucket}-{STAGE}" 26 | return bucket 27 | 28 | class ImageHandler: 29 | 30 | def get_image_content_type(image_data: str): 31 | """ 32 | Checks if the given image data represents a valid image and returns its content type. 33 | 34 | :param image_data: The binary data of the image. 35 | :return: The content type of the image if it is a valid image format, None otherwise. 36 | """ 37 | signatures = { 38 | b'\xff\xd8': 'image/jpeg', 39 | b'\x89PNG\r\n\x1a\n': 'image/png', 40 | b'GIF87a': 'image/gif', 41 | b'GIF89a': 'image/gif', 42 | b'II*\x00': 'image/tiff', 43 | b'MM\x00*': 'image/tiff', 44 | # Add more signatures and content types as needed 45 | } 46 | 47 | webp_markers = [b'VP8 ', b'VP8L', b'VP8X'] 48 | 49 | for signature, content_type in signatures.items(): 50 | if image_data.startswith(signature): 51 | return content_type 52 | 53 | # Check for WebP format separately, as it requires additional checks 54 | for marker in webp_markers: 55 | if marker in image_data[:16]: 56 | return 'image/webp' 57 | 58 | return None 59 | 60 | @staticmethod 61 | def get_binary_body_from_event(event: dict) -> bytes: 62 | """Extract binary data from request body""" 63 | if event["isBase64Encoded"]: 64 | return base64.b64decode(event["body"]) 65 | else: 66 | return None 67 | 68 | @staticmethod 69 | def encode_binary_file_for_response(blob: bytes) -> bytes: 70 | """ 71 | Binary response body of lambda functions should be encoded in base64. 72 | https://docs.aws.amazon.com/apigateway/latest/developerguide/lambda-proxy-binary-media.html 73 | """ 74 | return base64.b64encode(blob) 75 | 76 | 77 | @staticmethod 78 | def lambda_handler(event: dict, s3: S3Service) -> dict: 79 | if event.get("body", None) is None: 80 | error_message = {"message": "Received an empty body"} 81 | return api_response(body=json.dumps(error_message), status_code=400) 82 | if not event.get('isBase64Encoded', False): 83 | error_message = {"message": "Must be Base64 Encoded"} 84 | return api_response(body=json.dumps(error_message), status_code=400) 85 | 86 | bucket = get_s3_bucket_name() 87 | image_data = ImageHandler.get_binary_body_from_event(event) 88 | content_type = ImageHandler.get_image_content_type(image_data) 89 | if content_type is None: 90 | error_message = json.dumps({"message": "Invalid Image Format"}) 91 | return api_response(body=error_message, status_code=400) 92 | try: 93 | key = s3.write(bucket, content_type, image_data) 94 | url = s3.generate_file_url(bucket, key) 95 | except: 96 | error_message: str = json.dumps({"message": "Unexpected error prevented server from fulfilling request."}) 97 | return api_response(body={"message": error_message}, status_code=500) 98 | body = json.dumps({"photoUrl": url}) 99 | return api_response(body=body, status_code=200) 100 | 101 | 102 | def lambda_handler( 103 | event: dict, context: "awslambdaric.lambda_context.LambdaContext" 104 | ) -> dict: 105 | s3 = S3Service() 106 | return ImageHandler.lambda_handler(event, s3) 107 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-mock 3 | boto3 4 | pre-commit 5 | coverage 6 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | CommunityFridgeMapApi 5 | 6 | SAM Template for CommunityFridgeMapApi 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Api: 11 | BinaryMediaTypes: 12 | - image/webp 13 | Function: 14 | Timeout: 60 15 | 16 | Parameters: 17 | Environment: 18 | Type: String 19 | Description: Choose between local or AWS 20 | AllowedValues: 21 | - local 22 | - aws 23 | Stage: 24 | Type: String 25 | Description: Choose between dev, staging, prod 26 | AllowedValues: 27 | - dev 28 | - staging 29 | - prod 30 | CFMHostedZoneId: 31 | Type: String 32 | Description: Grab the HostedZoneId from Route53 console 33 | 34 | ########################## 35 | ## Resources ## 36 | ########################## 37 | Resources: 38 | ApiCertificate: 39 | Type: AWS::CertificateManager::Certificate 40 | Properties: 41 | DomainName: !Sub api-${Stage}.communityfridgefinder.com 42 | DomainValidationOptions: 43 | - DomainName: !Sub api-${Stage}.communityfridgefinder.com 44 | HostedZoneId: !Sub ${CFMHostedZoneId} 45 | ValidationMethod: DNS 46 | ApiGatewayApi: 47 | Type: AWS::Serverless::Api 48 | Properties: 49 | StageName: !Ref Stage 50 | # CORS preflight settings from https://vercel.com/guides/how-to-enable-cors 51 | Cors: 52 | AllowMethods: "'GET,OPTIONS,PATCH,DELETE,POST,PUT'" 53 | AllowHeaders: "'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, 54 | Content-MD5, Content-Type, Date, X-Api-Version'" 55 | AllowOrigin: "'*'" 56 | MaxAge: "'86400'" 57 | AllowCredentials: false 58 | EndpointConfiguration: REGIONAL 59 | Domain: 60 | DomainName: !Sub api-${Stage}.communityfridgefinder.com 61 | CertificateArn: !Ref ApiCertificate 62 | Route53: 63 | HostedZoneName: "communityfridgefinder.com." # NOTE: The period at the end is required 64 | 65 | FridgeReportFunction: 66 | Type: AWS::Serverless::Function 67 | Properties: 68 | CodeUri: functions/fridge_reports 69 | Handler: app.lambda_handler 70 | Runtime: python3.9 71 | FunctionName: !Sub FridgeReportFunction${Stage} 72 | Environment: 73 | Variables: 74 | Environment: !Ref Environment 75 | Stage: !Ref Stage 76 | Layers: 77 | - !Ref CommonLayer 78 | Policies: 79 | - Version: "2012-10-17" 80 | Statement: 81 | - Effect: Allow 82 | Action: 83 | - logs:CreateLogGroup 84 | - logs:CreateLogStream 85 | - logs:PutLogEvents 86 | Resource: arn:aws:logs:*:*:* 87 | - Effect: Allow 88 | Action: 89 | - dynamodb:PutItem 90 | Resource: !GetAtt FridgeReportTable.Arn 91 | - Effect: Allow 92 | Action: 93 | - dynamodb:GetItem 94 | - dynamodb:UpdateItem 95 | Resource: !GetAtt FridgeTable.Arn 96 | Events: 97 | PostFridgeReport: 98 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 99 | Properties: 100 | Path: /v1/fridges/{fridgeId}/reports 101 | Method: post 102 | RestApiId: 103 | Ref: ApiGatewayApi 104 | RequestParameters: 105 | - method.request.path.fridgeId: 106 | Required: true 107 | Caching: false 108 | GetFridgeReport: 109 | Type: Api 110 | Properties: 111 | Path: /v1/fridges/{fridgeId}/reports 112 | Method: get 113 | RestApiId: 114 | Ref: ApiGatewayApi 115 | RequestParameters: 116 | - method.request.path.fridgeId: 117 | Required: true 118 | Caching: false 119 | FridgeReportWarmUp: 120 | Type: Schedule 121 | Properties: 122 | # Read more about schedule expressions here: 123 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html 124 | # This event runs every 10 minutes 125 | Schedule: rate(1 minute) 126 | 127 | GetLatestFridgeReportsFunction: 128 | Type: AWS::Serverless::Function 129 | Properties: 130 | CodeUri: functions/fridge_reports/get_latest_fridge_reports 131 | Handler: app.lambda_handler 132 | Runtime: python3.9 133 | FunctionName: !Sub GetLatestFridgeReportsFunction${Stage} 134 | Environment: 135 | Variables: 136 | Environment: !Ref Environment 137 | Stage: !Ref Stage 138 | Layers: 139 | - !Ref CommonLayer 140 | Policies: 141 | - Version: "2012-10-17" 142 | Statement: 143 | - Effect: Allow 144 | Action: 145 | - logs:CreateLogGroup 146 | - logs:CreateLogStream 147 | - logs:PutLogEvents 148 | Resource: arn:aws:logs:*:*:* 149 | - Effect: Allow 150 | Action: 151 | - dynamodb:Scan 152 | Resource: !GetAtt FridgeTable.Arn 153 | Events: 154 | GetLatestFridgeReports: 155 | Type: Api 156 | Properties: 157 | Path: /v1/reports/last 158 | Method: get 159 | RestApiId: 160 | Ref: ApiGatewayApi 161 | 162 | HelloWorldFunction: 163 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 164 | Properties: 165 | FunctionName: !Sub HelloWorldFunction${Stage} 166 | CodeUri: functions/dev/hello_world/ 167 | Handler: app.lambda_handler 168 | Runtime: python3.9 169 | Layers: 170 | - !Ref CommonLayer 171 | Events: 172 | HelloWorld: 173 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 174 | Properties: 175 | Path: /hello 176 | Method: get 177 | RestApiId: 178 | Ref: ApiGatewayApi 179 | Description: Responds with 'hello world' if successful. 180 | 181 | FridgesFunction: 182 | Type: AWS::Serverless::Function 183 | Properties: 184 | CodeUri: functions/fridges/v1 185 | Handler: app.lambda_handler 186 | Runtime: python3.9 187 | FunctionName: !Sub FridgesFunction${Stage} 188 | Environment: 189 | Variables: 190 | Environment: !Ref Environment 191 | Stage: !Ref Stage 192 | Layers: 193 | - !Ref CommonLayer 194 | Policies: 195 | - Version: "2012-10-17" 196 | Statement: 197 | - Effect: Allow 198 | Action: 199 | - logs:CreateLogGroup 200 | - logs:CreateLogStream 201 | - logs:PutLogEvents 202 | Resource: arn:aws:logs:*:*:* 203 | - Effect: Allow 204 | Action: 205 | - dynamodb:GetItem 206 | - dynamodb:Scan 207 | - dynamodb:PutItem 208 | Resource: !GetAtt FridgeTable.Arn 209 | Events: 210 | GetFridge: 211 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 212 | Properties: 213 | Path: /v1/fridges/{fridgeId} 214 | Method: get 215 | RequestParameters: 216 | - method.request.path.fridgeId: 217 | Required: false 218 | Caching: false 219 | RestApiId: 220 | Ref: ApiGatewayApi 221 | GetFridges: 222 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 223 | Properties: 224 | Path: /v1/fridges 225 | Method: get 226 | RequestParameters: 227 | - method.request.querystring.tag: 228 | Required: false 229 | RestApiId: 230 | Ref: ApiGatewayApi 231 | PostFridge: 232 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 233 | Properties: 234 | Path: /v1/fridges 235 | Method: post 236 | RestApiId: 237 | Ref: ApiGatewayApi 238 | FridgeWarmUp: 239 | Type: Schedule 240 | Properties: 241 | # Read more about schedule expressions here: 242 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html 243 | # This event runs every 10 minutes 244 | Schedule: rate(1 minute) 245 | 246 | ImageFunction: 247 | Type: AWS::Serverless::Function 248 | Properties: 249 | CodeUri: functions/image/v1 250 | Handler: app.lambda_handler 251 | Runtime: python3.9 252 | FunctionName: !Sub ImageFunction${Stage} 253 | Environment: 254 | Variables: 255 | Environment: !Ref Environment 256 | Stage: !Ref Stage 257 | Layers: 258 | - !Ref CommonLayer 259 | Policies: 260 | - Version: '2012-10-17' 261 | Statement: 262 | - Effect: Allow 263 | Action: 264 | - logs:CreateLogGroup 265 | - logs:CreateLogStream 266 | - logs:PutLogEvents 267 | Resource: arn:aws:logs:*:*:* 268 | Statement: 269 | - Effect: Allow 270 | Action: 271 | - s3:PutObject 272 | - s3:GetObject 273 | Resource: 274 | - !Sub "arn:aws:s3:::${CommunityFridgeMapImages}/*" 275 | - !Sub "arn:aws:s3:::${CommunityFridgeMapImages}" 276 | Events: 277 | UploadImage: 278 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 279 | Properties: 280 | Path: /v1/photo 281 | Method: post 282 | RestApiId: 283 | Ref: ApiGatewayApi 284 | ImageWarmUp: 285 | Type: Schedule 286 | Properties: 287 | # Read more about schedule expressions here: 288 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html 289 | # This event runs every 10 minutes 290 | Schedule: rate(10 minutes) 291 | 292 | ContactFunction: 293 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 294 | Properties: 295 | CodeUri: functions/email_service/v1 296 | Handler: app.lambda_handler 297 | FunctionName: !Sub ContactFunction${Stage} 298 | Environment: 299 | Variables: 300 | Environment: !Ref Environment 301 | Stage: !Ref Stage 302 | Runtime: python3.9 303 | Layers: 304 | - !Ref CommonLayer 305 | Policies: 306 | - Version: "2012-10-17" 307 | Statement: 308 | - Effect: Allow 309 | Action: 310 | - logs:CreateLogGroup 311 | - logs:CreateLogStream 312 | - logs:PutLogEvents 313 | Resource: arn:aws:logs:*:*:* 314 | - SESCrudPolicy: { IdentityName: fridgefinderapp@gmail.com } 315 | 316 | Events: 317 | SendSESFunction: 318 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 319 | Properties: 320 | Path: /v1/contact 321 | Method: post 322 | RestApiId: 323 | Ref: ApiGatewayApi 324 | ContactWarmUp: 325 | Type: Schedule 326 | Properties: 327 | # Read more about schedule expressions here: 328 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html 329 | # This event runs every 10 minutes 330 | Schedule: rate(10 minutes) 331 | 332 | LoadFridgeDataFunction: #FOR DEV TESTING PURPOSES ONLY. TODO: DELETE THIS FUNCTION 333 | Type: AWS::Serverless::Function 334 | Properties: 335 | CodeUri: ./functions/dev/load_fridge_data 336 | Handler: app.lambda_handler 337 | Runtime: python3.9 338 | FunctionName: !Sub LoadFridgeDataFunction${Stage} 339 | Environment: 340 | Variables: 341 | Environment: !Ref Environment 342 | Stage: !Ref Stage 343 | Layers: 344 | - !Ref CommonLayer 345 | Policies: 346 | - Version: "2012-10-17" 347 | Statement: 348 | - Effect: Allow 349 | Action: 350 | - logs:CreateLogGroup 351 | - logs:CreateLogStream 352 | - logs:PutLogEvents 353 | Resource: arn:aws:logs:*:*:* 354 | - Effect: Allow 355 | Action: 356 | - dynamodb:PutItem 357 | Resource: !GetAtt FridgeTable.Arn 358 | ########################## 359 | ## Layers ## 360 | ########################## 361 | CommonLayer: 362 | Type: AWS::Serverless::LayerVersion 363 | Properties: 364 | LayerName: !Sub CommunityFridgeMapApiLayer${Stage} 365 | Description: Dependencies for CommunityFridgeMapApi project 366 | ContentUri: dependencies/ 367 | CompatibleRuntimes: 368 | - python3.7 369 | - python3.8 370 | - python3.9 371 | RetentionPolicy: Delete 372 | ########################## 373 | ## Bucket Policies ## 374 | ########################## 375 | ImageFunctionBucketPolicy: 376 | Type: AWS::S3::BucketPolicy 377 | Properties: 378 | Bucket: !Ref CommunityFridgeMapImages 379 | PolicyDocument: 380 | Statement: 381 | - Action: 382 | - s3:* 383 | Effect: Allow 384 | Resource: 385 | - !Sub arn:aws:s3:::${CommunityFridgeMapImages} 386 | - !Sub arn:aws:s3:::${CommunityFridgeMapImages}/* 387 | Principal: 388 | AWS: 389 | - '*' 390 | ########################## 391 | ## S3 Buckets ## 392 | ########################## 393 | CommunityFridgeMapImages: 394 | Type: AWS::S3::Bucket 395 | Properties: 396 | BucketName: !Sub community-fridge-map-images-${Stage} 397 | CorsConfiguration: 398 | CorsRules: 399 | - AllowedHeaders: 400 | - "*" 401 | AllowedMethods: 402 | - GET 403 | - PUT 404 | - HEAD 405 | AllowedOrigins: 406 | - "*" 407 | ########################## 408 | ## DynamoDB Tables ## 409 | ########################## 410 | 411 | FridgeTable: 412 | Type: AWS::DynamoDB::Table 413 | Properties: 414 | AttributeDefinitions: 415 | - AttributeName: "id" 416 | AttributeType: "S" 417 | KeySchema: 418 | - AttributeName: "id" 419 | KeyType: "HASH" 420 | TableName: !Sub fridge_${Stage} 421 | BillingMode: "PAY_PER_REQUEST" 422 | 423 | FridgeReportTable: 424 | Type: AWS::DynamoDB::Table 425 | Properties: 426 | AttributeDefinitions: 427 | - AttributeName: "fridgeId" 428 | AttributeType: "S" 429 | - AttributeName: "epochTimestamp" #epoch time used for querying data 430 | AttributeType: "N" 431 | KeySchema: 432 | - AttributeName: "fridgeId" 433 | KeyType: "HASH" 434 | - AttributeName: "epochTimestamp" 435 | KeyType: "RANGE" 436 | TableName: !Sub fridge_report_${Stage} 437 | BillingMode: "PAY_PER_REQUEST" 438 | 439 | TagTable: 440 | Type: AWS::DynamoDB::Table 441 | Properties: 442 | 443 | AttributeDefinitions: 444 | - AttributeName: "tag_name" 445 | AttributeType: "S" 446 | KeySchema: 447 | - AttributeName: "tag_name" 448 | KeyType: "HASH" 449 | TableName: !Sub tag_${Stage} 450 | BillingMode: "PAY_PER_REQUEST" 451 | 452 | FridgeHistoryTable: 453 | Type: AWS::DynamoDB::Table 454 | Properties: 455 | AttributeDefinitions: 456 | - AttributeName: "fridgeId" 457 | AttributeType: "S" 458 | - AttributeName: "epochTimestamp" #epoch time used for querying data 459 | AttributeType: "N" 460 | KeySchema: 461 | - AttributeName: "fridgeId" 462 | KeyType: "HASH" 463 | - AttributeName: "epochTimestamp" 464 | KeyType: "RANGE" 465 | TableName: !Sub fridge_history_${Stage} 466 | BillingMode: "PAY_PER_REQUEST" 467 | 468 | 469 | Outputs: 470 | HelloWorldFunction: 471 | Description: "Hello World Lambda Function ARN" 472 | Value: !GetAtt HelloWorldFunction.Arn 473 | HelloWorldFunctionIamRole: 474 | Description: "Implicit IAM Role created for Hello World function" 475 | Value: !GetAtt HelloWorldFunctionRole.Arn 476 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/tests/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/assert_resposne.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union, Optional 3 | 4 | def assert_response( 5 | response: dict, 6 | status: Optional[int]=None, 7 | headers: Optional[dict]=None, 8 | body: Union[str, bytes, dict, None]=None, 9 | ): 10 | """ 11 | Make assertions against a lambda response. 12 | 13 | Only specified parameters are checked. 14 | Parameters: 15 | status: expected status code 16 | headers: expected headers 17 | body: expected body. It can be a string, dict (json), or (base64 encoded) bytes. 18 | """ 19 | if status is not None: 20 | assert response["statusCode"] == status 21 | if headers is not None: 22 | assert response["headers"] == headers 23 | if body is not None: 24 | if type(body) == str or type(body) == bytes: 25 | assert response["body"] == body 26 | elif type(body) == dict: 27 | assert json.loads(response["body"]) == body 28 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.s3_service_stub import S3ServiceStub 3 | 4 | @pytest.fixture 5 | def s3_service_stub(): 6 | return S3ServiceStub() 7 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/tests/integration/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/integration/test_api_gateway.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | 4 | import boto3 5 | import requests 6 | 7 | """ 8 | Make sure env variable AWS_SAM_STACK_NAME exists with the name of the stack we are going to test. 9 | """ 10 | 11 | 12 | class TestApiGateway(TestCase): 13 | api_endpoint: str 14 | 15 | @classmethod 16 | def get_stack_name(cls) -> str: 17 | stack_name = os.environ.get("AWS_SAM_STACK_NAME") 18 | if not stack_name: 19 | raise Exception( 20 | "Cannot find env var AWS_SAM_STACK_NAME. \n" 21 | "Please setup this environment variable with the stack name where we are running integration tests." 22 | ) 23 | 24 | return stack_name 25 | 26 | def setUp(self) -> None: 27 | """ 28 | Based on the provided env variable AWS_SAM_STACK_NAME, 29 | here we use cloudformation API to find out what the HelloWorldApi URL is 30 | """ 31 | stack_name = TestApiGateway.get_stack_name() 32 | 33 | client = boto3.client("cloudformation") 34 | 35 | try: 36 | response = client.describe_stacks(StackName=stack_name) 37 | except Exception as e: 38 | raise Exception( 39 | f"Cannot find stack {stack_name}. \n" 40 | f'Please make sure stack with the name "{stack_name}" exists.' 41 | ) from e 42 | 43 | stacks = response["Stacks"] 44 | 45 | stack_outputs = stacks[0]["Outputs"] 46 | api_outputs = [ 47 | output for output in stack_outputs if output["OutputKey"] == "HelloWorldApi" 48 | ] 49 | self.assertTrue( 50 | api_outputs, f"Cannot find output HelloWorldApi in stack {stack_name}" 51 | ) 52 | 53 | self.api_endpoint = api_outputs[0]["OutputValue"] 54 | 55 | def test_api_gateway(self): 56 | """ 57 | Call the API Gateway endpoint and check the response 58 | """ 59 | response = requests.get(self.api_endpoint) 60 | self.assertDictEqual(response.json(), {"message": "hello world"}) 61 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-mock 3 | boto3 4 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/s3_service_stub.py: -------------------------------------------------------------------------------- 1 | class S3ServiceStub: 2 | """Stub implementation of the Storage class for testing""" 3 | def __init__(self): 4 | self._buckets = {} 5 | self._current_index = 0 6 | 7 | def idempotent_create_bucket(self, bucket: str): 8 | if bucket not in self._buckets: 9 | self._buckets[bucket] = {} 10 | 11 | def read(self, bucket: str, key: str): 12 | return self._buckets[bucket][key] 13 | 14 | def write(self, bucket: str, extension: str, blob: bytes): 15 | key = f"{self._current_index}.{extension}" 16 | self._current_index += 1 17 | self.idempotent_create_bucket(bucket) 18 | self._buckets[bucket][key] = blob 19 | return key 20 | 21 | def generate_file_url(self, bucket: str, key: str): 22 | return f"http://localhost:4566/{bucket}/{key}" 23 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/tests/unit/__init__.py -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_db_item.py: -------------------------------------------------------------------------------- 1 | from CommunityFridgeMapApi.dependencies.python.db import DB_Item 2 | 3 | import unittest 4 | 5 | 6 | class DB_Item_Test(unittest.TestCase): 7 | def test_remove_extra_whitespace(self): 8 | result = DB_Item.remove_extra_whitespace(" h i th er h o w ") 9 | self.assertEqual(result, "h i th er h o w") 10 | 11 | def test_process_fields(self): 12 | object_dict = {"1": " h i ", "2": {"test": " h i "}, "3": [" h i "]} 13 | result = DB_Item.process_fields(object_dict=object_dict) 14 | self.assertEqual(result, {"1": "h i", "2": {"test": "h i"}, "3": ["h i"]}) 15 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_db_response.py: -------------------------------------------------------------------------------- 1 | from CommunityFridgeMapApi.dependencies.python.db import DB_Response 2 | 3 | import unittest 4 | 5 | 6 | class DB_Response_Test(unittest.TestCase): 7 | def test_api_format(self): 8 | db_response = DB_Response(success=True, status_code=201, message="testing") 9 | api_format = db_response.api_format() 10 | self.assertEqual(api_format["statusCode"], 201) 11 | self.assertEqual(api_format["body"], '{"message": "testing"}') 12 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_email_service.py: -------------------------------------------------------------------------------- 1 | from http import client 2 | from CommunityFridgeMapApi.functions.email_service.v1.app import ContactHandler 3 | from dependencies.python.db import get_ddb_connection 4 | import unittest 5 | from dependencies.python.db import Fridge 6 | import json 7 | 8 | 9 | class SESClientMock: 10 | def send_email( 11 | self, 12 | Source, 13 | Destination, 14 | Message, 15 | ): 16 | return {"MessageId": 1} 17 | 18 | 19 | class ContactHandlerTest(unittest.TestCase): 20 | def test_format_email(self): 21 | formatted_email = ContactHandler.format_email("email", "body", "name") 22 | self.assertEqual(formatted_email, "From: name email\r\nMessage: body") 23 | 24 | def test_lambda_handler_no_body(self): 25 | response = ContactHandler.lambda_handler({}, client=SESClientMock()) 26 | self.assertEqual(response["statusCode"], 400) 27 | body = json.loads(response["body"]) 28 | self.assertEqual( 29 | body, {"message": "Unable to send Email. Missing Required Fields"} 30 | ) 31 | 32 | def test_lambda_handler_missing_fields(self): 33 | event = {"body": {}} 34 | response = ContactHandler.lambda_handler(event, client=SESClientMock()) 35 | body = json.loads(response["body"]) 36 | self.assertEqual( 37 | body, {"message": "Unable to send Email. Missing Required Fields"} 38 | ) 39 | 40 | def test_extract_body_fields(self): 41 | body = json.dumps( 42 | { 43 | "name": "name", 44 | "email": "email", 45 | "subject": "subject", 46 | "message": "message", 47 | } 48 | ) 49 | ( 50 | message, 51 | subject, 52 | sender, 53 | senderEmailAddress, 54 | ) = ContactHandler.extract_body_fields(body) 55 | self.assertEqual(message, "message") 56 | self.assertEqual(sender, "name") 57 | self.assertEqual(subject, "FridgeFinder Contact: subject. From: name") 58 | self.assertEqual(senderEmailAddress, "email") 59 | 60 | def test_lambda_handler(self): 61 | body = json.dumps( 62 | { 63 | "name": "name", 64 | "email": "email", 65 | "subject": "subject", 66 | "message": "message", 67 | } 68 | ) 69 | event = {"body": body} 70 | response = ContactHandler.lambda_handler(event=event, client=SESClientMock()) 71 | self.assertEqual(response["statusCode"], 200) 72 | message = json.loads(response["body"])["message"] 73 | self.assertEqual(message, "Succesffully Sent Email!") 74 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_fridge.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from CommunityFridgeMapApi.dependencies.python.db import FridgeReport 3 | from CommunityFridgeMapApi.functions.fridges.v1.app import FridgeHandler 4 | from dependencies.python.db import layer_test 5 | from dependencies.python.db import get_ddb_connection 6 | import unittest 7 | from dependencies.python.db import Fridge 8 | import json 9 | import os 10 | 11 | 12 | def test_layer_test(): 13 | 14 | ret = layer_test() 15 | assert ret == "hello world" 16 | 17 | 18 | def test_get_ddb_connection(): 19 | os.environ['AWS_DEFAULT_REGION'] = 'us-west-2' 20 | connection = get_ddb_connection() 21 | assert str(type(connection)) == "" 22 | connection = get_ddb_connection(env="local") 23 | assert str(type(connection)) == "" 24 | 25 | 26 | class DynamoDbMockPutItem: 27 | def __init__(self): 28 | pass 29 | 30 | def put_item(self, TableName=None, Item=None, ConditionExpression=None): 31 | pass 32 | 33 | 34 | class DynamoDbMockScan: 35 | def __init__(self): 36 | pass 37 | 38 | def scan( 39 | self, 40 | TableName=None, 41 | FilterExpression=None, 42 | ExpressionAttributeValues=None, 43 | ProjectionExpression=None, 44 | ): 45 | return {"Items": [{"json_data": {"S": '{"id": {"S": "test"}}'}}]} 46 | 47 | 48 | class FridgeTest(unittest.TestCase): 49 | def test_set_id(self): 50 | fridge = Fridge(fridge={"name": "The Friendly Fridge"}, db_client=None) 51 | fridge.set_id() 52 | self.assertEqual(fridge.id, "thefriendlyfridge") 53 | 54 | def test_set_last_edditted(self): 55 | fridge = Fridge(fridge={"name": "The Friendly Fridge"}, db_client=None) 56 | self.assertIsNone(fridge.last_edited) 57 | fridge.set_last_edited() 58 | self.assertIsNotNone(fridge.last_edited) 59 | 60 | def test_add_item_with_invalid_id_characters(self): 61 | """ 62 | This is testing to see if the special characters are removed 63 | """ 64 | db_client = DynamoDbMockPutItem() 65 | fridge = Fridge( 66 | fridge={ 67 | "name": "fridgàe&^ñ@(*#(&.(*$<>.#%{}|\^~[]\";:/?@=&$+,", 68 | "location": {"geoLat": 124242, "geoLng": 2345235}, 69 | }, 70 | db_client=db_client, 71 | ) 72 | response = fridge.add_item() 73 | self.assertTrue(response.success) 74 | self.assertEqual( 75 | fridge.id, "fridge~" 76 | ) 77 | self.assertEqual(response.status_code, 201) 78 | 79 | def test_add_item_missing_required_field(self): 80 | db_client = DynamoDbMockPutItem() 81 | fridge = Fridge( 82 | fridge={ 83 | "name": "test fridge", 84 | "location": {"geoLat": 12124}, 85 | }, 86 | db_client=db_client, 87 | ) 88 | response = fridge.add_item() 89 | self.assertFalse(response.success) 90 | self.assertEqual(response.message, "Missing Required Field: location/geoLng") 91 | self.assertEqual(response.status_code, 400) 92 | 93 | def test_add_item_min_length_failure(self): 94 | db_client = DynamoDbMockPutItem() 95 | fridge = Fridge( 96 | fridge={ 97 | "name": "it", 98 | "location": {"geoLat": 12124, "geoLng": 232523}, 99 | }, 100 | db_client=db_client, 101 | ) 102 | response = fridge.add_item() 103 | self.assertFalse(response.success) 104 | self.assertEqual(response.message, "id character length must be >= 3") 105 | self.assertEqual(response.status_code, 400) 106 | 107 | def test_add_item_success(self): 108 | db_client = DynamoDbMockPutItem() 109 | fridge = Fridge( 110 | fridge={ 111 | "name": "Test Fridge", 112 | "location": {"geoLat": 124242, "geoLng": 2345235}, 113 | }, 114 | db_client=db_client, 115 | ) 116 | response = fridge.add_item() 117 | self.assertTrue(response.success) 118 | self.assertEqual(response.json_data, json.dumps({"id": "testfridge"})) 119 | self.assertEqual(response.status_code, 201) 120 | self.assertIsNotNone(fridge.last_edited) 121 | 122 | def test_format_dynamodb_item_v2(self): 123 | fridge = { 124 | "id": "test", 125 | "name": "test", 126 | "tags": ["tag3"], 127 | "location": {}, 128 | "maintainer": {}, 129 | "notes": "test", 130 | "food_accepts": ["dairy"], 131 | "food_restrictions": ["meat"], 132 | "photoUrl": "test", 133 | "last_edited": 2342353, 134 | "verified": True, 135 | "latestFridgeReport": {}, 136 | } 137 | fridge_item = Fridge(fridge=fridge, db_client=None).format_dynamodb_item_v2() 138 | expected_response = { 139 | "id": {"S": "test"}, 140 | "name": {"S": "test"}, 141 | "tags": {"L": [{"S": "tag3"}]}, 142 | "location": {"S": "{}"}, 143 | "maintainer": {"S": "{}"}, 144 | "notes": {"S": "test"}, 145 | "food_accepts": {"L": [{"S": "dairy"}]}, 146 | "food_restrictions": {"L": [{"S": "meat"}]}, 147 | "photoUrl": {"S": "test"}, 148 | "last_edited": {"N": "2342353"}, 149 | "verified": {"BOOL": True}, 150 | "latestFridgeReport": {"S": "{}"}, 151 | "json_data": {"S": json.dumps(fridge)}, 152 | } 153 | self.assertEqual(fridge_item, expected_response) 154 | fridge_item = Fridge( 155 | fridge={"id": ""}, db_client=None 156 | ).format_dynamodb_item_v2() 157 | self.assertEqual(fridge_item, {"json_data": {"S": "{}"}}) 158 | 159 | def test_is_valid_id(self): 160 | is_valid, message = Fridge.is_valid_id(None) 161 | self.assertEqual(message, "Missing Required Field: id") 162 | self.assertFalse(is_valid) 163 | 164 | is_valid, message = Fridge.is_valid_id("hi there") 165 | self.assertEqual(message, "id has invalid characters") 166 | self.assertFalse(is_valid) 167 | 168 | is_valid, message = Fridge.is_valid_id("hi") 169 | self.assertEqual(message, "id Must Have A Character Length >= 3 and <= 100") 170 | self.assertFalse(is_valid) 171 | 172 | def test_validate_fields_required_fields(self): 173 | fridge = Fridge(fridge={}, db_client=None) 174 | field_validator = fridge.validate_fields() 175 | self.assertFalse(field_validator.is_valid) 176 | self.assertEqual(field_validator.message, "Missing Required Field: id") 177 | 178 | def test_validate_fields_min_length(self): 179 | fridge = Fridge(fridge={"id": "x"}, db_client=None) 180 | field_validator = fridge.validate_fields() 181 | self.assertFalse(field_validator.is_valid) 182 | self.assertEqual(field_validator.message, "id character length must be >= 3") 183 | 184 | def test_validate_fields_max_length(self): 185 | fridge = Fridge( 186 | fridge={ 187 | "id": "x" * 101, 188 | "name": "x" * 101, 189 | "location": {"geoLat": "3", "geoLng": "3"}, 190 | }, 191 | db_client=None, 192 | ) 193 | field_validator = fridge.validate_fields() 194 | self.assertFalse(field_validator.is_valid) 195 | self.assertEqual(field_validator.message, "id character length must be <= 100") 196 | 197 | def test_validate_fields_success(self): 198 | fridge = Fridge( 199 | fridge={ 200 | "id": "goodAZaz09_#-‘'.", 201 | "name": "goodAZaz09_#-‘'. ", 202 | "location": {"geoLat": 232342, "geoLng": 2342342}, 203 | }, 204 | db_client=None, 205 | ) 206 | field_validator = fridge.validate_fields() 207 | self.assertTrue(field_validator.is_valid) 208 | self.assertEqual( 209 | field_validator.message, "All Fields Were Successfully Validated" 210 | ) 211 | 212 | def test_get_items(self): 213 | response = Fridge(db_client=DynamoDbMockScan()).get_items() 214 | expected_response = '[{"id": {"S": "test"}}]' 215 | self.assertTrue(response.is_successful()) 216 | self.assertEqual(response.status_code, 200) 217 | self.assertEqual(response.json_data, expected_response) 218 | 219 | def test_get_items_tag(self): 220 | response = Fridge(db_client=DynamoDbMockScan()).get_items(tag="tag_test") 221 | expected_response = '[{"id": {"S": "test"}}]' 222 | self.assertTrue(response.is_successful()) 223 | self.assertEqual(response.status_code, 200) 224 | self.assertEqual(response.json_data, expected_response) 225 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_fridge_handler.py: -------------------------------------------------------------------------------- 1 | from CommunityFridgeMapApi.functions.fridges.v1.app import FridgeHandler 2 | from dependencies.python.db import layer_test 3 | from dependencies.python.db import get_ddb_connection 4 | import unittest 5 | 6 | 7 | class DynamoDbMockGetItem: 8 | def __init__(self): 9 | pass 10 | 11 | def get_item(self, TableName=None, Key=None): 12 | return {"Item": {"json_data": {"S": '{"id": {"S": "test"}}'}}} 13 | 14 | class DynamoDbMockGetItemFail: 15 | def __init__(self): 16 | pass 17 | 18 | def get_item(self, TableName=None, Key=None): 19 | return {} 20 | 21 | 22 | class DynamoDbMockScan: 23 | def __init__(self): 24 | pass 25 | 26 | def scan( 27 | self, 28 | TableName=None, 29 | FilterExpression=None, 30 | ExpressionAttributeValues=None, 31 | ProjectionExpression=None, 32 | ): 33 | return {"Items": [{"json_data": {"S": '{"id": {"S": "test"}}'}}]} 34 | 35 | 36 | class FridgeHandlerTest(unittest.TestCase): 37 | def test_get_item_success(self): 38 | json_data = '{"id": {"S": "test"}}' 39 | response = FridgeHandler.lambda_handler( 40 | event={ 41 | "httpMethod": "GET", 42 | "pathParameters": {"fridgeId": "test"}, 43 | "queryStringParameters": None, 44 | }, 45 | ddbclient=DynamoDbMockGetItem(), 46 | ) 47 | self.assertEqual(response["statusCode"], 200) 48 | self.assertEqual(response["body"], json_data) 49 | 50 | def test_get_item_failure(self): 51 | response = FridgeHandler.lambda_handler( 52 | event={ 53 | "httpMethod": "GET", 54 | "pathParameters": {"fridgeId": "hi"}, 55 | "queryStringParameters": None, 56 | }, 57 | ddbclient=DynamoDbMockGetItemFail(), 58 | ) 59 | self.assertEqual(response["statusCode"], 404) 60 | self.assertEqual( 61 | response["body"], 62 | '{"message": "Fridge was not found"}', 63 | ) 64 | 65 | def test_get_items(self): 66 | json_data = '[{"id": {"S": "test"}}]' 67 | response = FridgeHandler.lambda_handler( 68 | event={ 69 | "httpMethod": "GET", 70 | "pathParameters": None, 71 | "queryStringParameters": None, 72 | }, 73 | ddbclient=DynamoDbMockScan(), 74 | ) 75 | self.assertEqual(response["statusCode"], 200) 76 | self.assertEqual(response["body"], json_data) 77 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_fridge_report.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dependencies.python.db import FridgeReport 3 | import json 4 | 5 | class DynamoDbMockPutItem: 6 | def __init__(self): 7 | pass 8 | 9 | def put_item(self, TableName=None, Item=None, ConditionExpression=None): 10 | pass 11 | 12 | def get_item(self, TableName=None, Key=None): 13 | return {"Item": {"json_data": {"S": '{"id": {"S": "test"}}'}}} 14 | 15 | def update_item( 16 | self, 17 | TableName=None, 18 | Key=None, 19 | ExpressionAttributeNames=None, 20 | ExpressionAttributeValues=None, 21 | UpdateExpression=None, 22 | ): 23 | pass 24 | 25 | 26 | class FrdgeReportTest(unittest.TestCase): 27 | def test_set_timestamp(self): 28 | fridge_report = FridgeReport( 29 | fridge_report={ 30 | "fridgeId": "test", 31 | "condition": "good", 32 | "notes": "all good out here", 33 | "foodPercentage": 2, 34 | }, 35 | db_client=None, 36 | ) 37 | fridge_report.set_timestamp() 38 | self.assertIsNotNone(fridge_report.epochTimestamp) 39 | self.assertIsNotNone(fridge_report.timestamp) 40 | 41 | def test_validate_fields(self): 42 | test_data = [ 43 | { 44 | "fridge_report": { 45 | "notes": "all good", 46 | "fridgeId": "thefriendlyfridge", 47 | "photoUrl": "s3.url", 48 | "epochTimestamp": 34234, 49 | "timestamp": "good", 50 | "condition": "good", 51 | "foodPercentage": 3, 52 | }, 53 | "valid": True, 54 | "message": "All Fields Were Successfully Validated", 55 | }, 56 | { 57 | "fridge_report": {}, 58 | "valid": False, 59 | "message": "Missing Required Field: fridgeId", 60 | }, 61 | { 62 | "fridge_report": {"fridgeId": "thefriendlyfridge"}, 63 | "valid": False, 64 | "message": "Missing Required Field: condition", 65 | }, 66 | { 67 | "fridge_report": {"fridgeId": "thefriendlyfridge", "condition": "fake"}, 68 | "valid": False, 69 | "message": "condition must to be one of", 70 | }, 71 | { 72 | "fridge_report": { 73 | "fridgeId": "thefriendlyfridge", 74 | "condition": "good", 75 | }, 76 | "valid": False, 77 | "message": "Missing Required Field: foodPercentage", 78 | }, 79 | { 80 | "fridge_report": { 81 | "fridgeId": "thefriendlyfridge", 82 | "condition": "good", 83 | "foodPercentage": 99, 84 | }, 85 | "valid": False, 86 | "message": "foodPercentage must to be one of", 87 | }, 88 | { 89 | "fridge_report": { 90 | "fridgeId": "thefriendlyfridge", 91 | "condition": "good", 92 | "foodPercentage": 3, 93 | }, 94 | "valid": True, 95 | "message": "All Fields Were Successfully Validated", 96 | }, 97 | { 98 | "fridge_report": { 99 | "fridgeId": "thefriendlyfridge", 100 | "condition": "good", 101 | "foodPercentage": 3, 102 | "notes": "x" * 257, 103 | }, 104 | "valid": False, 105 | "message": "notes character length must be <= 256", 106 | }, 107 | { 108 | "fridge_report": { 109 | "fridgeId": "thefriendlyfridge", 110 | "condition": "good", 111 | "foodPercentage": 3, 112 | "photoUrl": "x" * 2049, 113 | }, 114 | "valid": False, 115 | "message": "photoUrl character length must be <= 2048", 116 | }, 117 | ] 118 | 119 | for data in test_data: 120 | validator = FridgeReport( 121 | db_client=None, fridge_report=data["fridge_report"] 122 | ).validate_fields() 123 | self.assertEqual(validator.is_valid, validator.is_valid) 124 | self.assertTrue(data["message"] in validator.message) 125 | 126 | def test_add_item(self): 127 | test_data = [ 128 | { 129 | "fridge_report": {}, 130 | "message": "Missing Required Field: fridgeId", 131 | "success": False, 132 | }, 133 | { 134 | "fridge_report": { 135 | "fridgeId": "hi", 136 | "condition": "good", 137 | "foodPercentage": 2, 138 | }, 139 | "message": "fridgeId character length must be >= 3", 140 | "success": False, 141 | }, 142 | { 143 | "fridge_report": { 144 | "fridgeId": "valid", 145 | "condition": "hacking", 146 | "foodPercentage": 2, 147 | }, 148 | "message": "condition must to be one of", 149 | "success": False, 150 | }, 151 | { 152 | "fridge_report": { 153 | "fridgeId": "valid", 154 | "condition": "good", 155 | "foodPercentage": 50, 156 | }, 157 | "message": "foodPercentage must to be one of", 158 | "success": False, 159 | }, 160 | { 161 | "fridge_report": { 162 | "fridgeId": "test", 163 | "condition": "good", 164 | "foodPercentage": 2, 165 | "notes": "t" * 257, 166 | }, 167 | "message": "notes character length must be <= 256", 168 | "success": False, 169 | }, 170 | { 171 | "fridge_report": { 172 | "fridgeId": "test", 173 | "condition": "good", 174 | "foodPercentage": 2, 175 | }, 176 | "fridgeId": "test", 177 | "success": True, 178 | }, 179 | ] 180 | 181 | for data in test_data: 182 | response = FridgeReport( 183 | fridge_report=data["fridge_report"], db_client=DynamoDbMockPutItem() 184 | ).add_item() 185 | self.assertEqual(response.success, data["success"]) 186 | if "message" in data: 187 | self.assertTrue(data["message"] in response.message) 188 | else: 189 | response_dict_data = json.loads(response.json_data) 190 | self.assertEqual(response_dict_data['fridgeId'], data['fridgeId']) 191 | self.assertTrue("timestamp" in response_dict_data) 192 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_fridge_report_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pytest 4 | 5 | from CommunityFridgeMapApi.functions.fridge_reports.app import FridgReportHandler 6 | import json 7 | 8 | 9 | class DynamoDbMockPutItem: 10 | def __init__(self): 11 | pass 12 | 13 | def put_item(self, TableName=None, Item=None, ConditionExpression=None): 14 | pass 15 | 16 | def get_item(self, TableName=None, Key=None): 17 | return {"Item": {"json_data": {"S": '{"id": {"S": "test"}}'}}} 18 | 19 | def update_item( 20 | self, 21 | TableName=None, 22 | Key=None, 23 | ExpressionAttributeNames=None, 24 | ExpressionAttributeValues=None, 25 | UpdateExpression=None, 26 | ): 27 | pass 28 | 29 | 30 | class FrdgeReportHandlerTest(unittest.TestCase): 31 | def test_lambda_handler_success(self): 32 | event = { 33 | "body": '{"condition": "good", "foodPercentage": 2}', 34 | "pathParameters": {"fridgeId": "thefriendlyfridge"}, 35 | "httpMethod": "POST", 36 | } 37 | response = FridgReportHandler.lambda_handler( 38 | event=event, ddbclient=DynamoDbMockPutItem() 39 | ) 40 | self.assertEqual(response["statusCode"], 201) 41 | body = json.loads(response["body"]) 42 | self.assertEqual(body["fridgeId"], "thefriendlyfridge") 43 | self.assertTrue("timestamp" in body) 44 | 45 | def test_lambda_handler_fail(self): 46 | event = { 47 | "body": '{"condition": "good"}', 48 | "pathParameters": {"fridgeId": "thefriendlyfridge"}, 49 | "httpMethod": "POST", 50 | } 51 | response = FridgReportHandler.lambda_handler( 52 | event=event, ddbclient=DynamoDbMockPutItem() 53 | ) 54 | self.assertEqual(response["statusCode"], 400) 55 | message = json.loads(response["body"])["message"] 56 | self.assertEqual(message, "Missing Required Field: foodPercentage") 57 | 58 | def test_lambda_handler_exception(self): 59 | event = { 60 | "body": '{"condition": "good", "foodPercentage": 2}', 61 | "pathParameters": {"fridgeId": "thefriendlyfridge"}, 62 | } 63 | response = FridgReportHandler.lambda_handler( 64 | event=event, ddbclient=DynamoDbMockPutItem() 65 | ) 66 | self.assertEqual(response["statusCode"], 400) 67 | message = json.loads(response["body"])["message"] 68 | self.assertEqual(message, "httpMethod missing") 69 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_get_latest_fridge_report.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dependencies.python.db import Fridge 3 | import json 4 | 5 | class DynamoDbMockScanItem: 6 | def __init__(self): 7 | pass 8 | 9 | def scan( 10 | self, 11 | TableName=None, 12 | FilterExpression=None, 13 | ExpressionAttributeValues=None, 14 | ProjectionExpression=None, 15 | ): 16 | return {'Items': [{'json_data': {'S': '{"latestFridgeReport": {"fridgeId": "2fish5loavesfridge", "epochTimestamp": "1685405973", "timestamp": "2023-05-30T00:19:33Z", "condition": "good", "foodPercentage": 2}}'}}, 17 | {'json_data': {'S': '{"latestFridgeReport": {"fridgeId": "thefriendlyfridge", "epochTimestamp": "1685406040", "timestamp": "2023-05-30T00:20:40Z", "condition": "dirty", "foodPercentage": 1}}'}}]} 18 | 19 | 20 | class FrdgeReportTest(unittest.TestCase): 21 | 22 | def test_get_latest_fridge_report(self): 23 | fridge = Fridge(db_client=DynamoDbMockScanItem()) 24 | response = fridge.get_latest_fridge_reports() 25 | json_data = json.loads(response.json_data) 26 | self.assertEqual(response.message, "Successfully found fridge reports") 27 | self.assertEqual(len(json_data), 2) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_hello_world.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from functions.dev.hello_world import app 6 | 7 | 8 | @pytest.fixture() 9 | def apigw_event(): 10 | """Generates API GW Event""" 11 | 12 | return { 13 | "body": '{ "test": "body"}', 14 | "resource": "/{proxy+}", 15 | "requestContext": { 16 | "resourceId": "123456", 17 | "apiId": "1234567890", 18 | "resourcePath": "/{proxy+}", 19 | "httpMethod": "POST", 20 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 21 | "accountId": "123456789012", 22 | "identity": { 23 | "apiKey": "", 24 | "userArn": "", 25 | "cognitoAuthenticationType": "", 26 | "caller": "", 27 | "userAgent": "Custom User Agent String", 28 | "user": "", 29 | "cognitoIdentityPoolId": "", 30 | "cognitoIdentityId": "", 31 | "cognitoAuthenticationProvider": "", 32 | "sourceIp": "127.0.0.1", 33 | "accountId": "", 34 | }, 35 | "stage": "prod", 36 | }, 37 | "queryStringParameters": {"foo": "bar"}, 38 | "headers": { 39 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 40 | "Accept-Language": "en-US,en;q=0.8", 41 | "CloudFront-Is-Desktop-Viewer": "true", 42 | "CloudFront-Is-SmartTV-Viewer": "false", 43 | "CloudFront-Is-Mobile-Viewer": "false", 44 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 45 | "CloudFront-Viewer-Country": "US", 46 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 47 | "Upgrade-Insecure-Requests": "1", 48 | "X-Forwarded-Port": "443", 49 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 50 | "X-Forwarded-Proto": "https", 51 | "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==", 52 | "CloudFront-Is-Tablet-Viewer": "false", 53 | "Cache-Control": "max-age=0", 54 | "User-Agent": "Custom User Agent String", 55 | "CloudFront-Forwarded-Proto": "https", 56 | "Accept-Encoding": "gzip, deflate, sdch", 57 | }, 58 | "pathParameters": {"proxy": "/examplepath"}, 59 | "httpMethod": "POST", 60 | "stageVariables": {"baz": "qux"}, 61 | "path": "/examplepath", 62 | } 63 | 64 | 65 | def test_lambda_handler(apigw_event, mocker): 66 | 67 | ret = app.lambda_handler(apigw_event, "") 68 | data = json.loads(ret["body"]) 69 | 70 | assert ret["statusCode"] == 200 71 | assert "message" in ret["body"] 72 | assert data["message"] == "hello world" 73 | # assert "location" in data.dict_keys() 74 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_image.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import base64 3 | from unittest.mock import patch, ANY 4 | from functions.image.v1.app import ImageHandler 5 | from tests.assert_resposne import assert_response 6 | from tests.s3_service_stub import S3ServiceStub 7 | 8 | 9 | def test_upload(s3_service_stub: S3ServiceStub): 10 | blob = base64.b64decode( 11 | "UklGRmh2AABXRUJQVlA4IFx2AADSvgGd" 12 | ) # Minimal blob with webp magic number. 13 | b64encoded_blob = base64.b64encode(blob).decode("ascii") 14 | with patch.object( 15 | s3_service_stub, "write", wraps=s3_service_stub.write 16 | ) as write_spy: 17 | with patch.object( 18 | s3_service_stub, 19 | "generate_file_url", 20 | wraps=s3_service_stub.generate_file_url, 21 | ) as generate_file_url_spy: 22 | response = ImageHandler.lambda_handler( 23 | event={ 24 | "isBase64Encoded": True, 25 | "body": b64encoded_blob, 26 | }, 27 | s3=s3_service_stub, 28 | ) 29 | 30 | write_spy.assert_called_once_with( 31 | "community-fridge-map-images", "image/webp", blob 32 | ) 33 | generate_file_url_spy.assert_called_once() 34 | assert_response( 35 | response, 36 | status=200, 37 | headers={ 38 | "Content-Type": "application/json", 39 | "Access-Control-Allow-Origin": "*", 40 | }, 41 | body={"photoUrl": ANY}, 42 | ) 43 | 44 | 45 | def test_upload_invalid_binary(s3_service_stub: S3ServiceStub): 46 | blob = b"notwebp" 47 | b64encoded_blob = base64.b64encode(blob).decode("ascii") 48 | response = ImageHandler.lambda_handler( 49 | event={ 50 | "isBase64Encoded": True, 51 | "body": b64encoded_blob, 52 | }, 53 | s3=s3_service_stub, 54 | ) 55 | 56 | assert_response( 57 | response, 58 | status=400, 59 | headers={ 60 | "Content-Type": "application/json", 61 | "Access-Control-Allow-Origin": "*", 62 | }, 63 | body={ 64 | "message": "Invalid Image Format" 65 | }, 66 | ) 67 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_layer.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import connection 2 | 3 | from dependencies.python.db import layer_test 4 | from dependencies.python.db import get_ddb_connection 5 | 6 | 7 | def test_layer_test(): 8 | 9 | ret = layer_test() 10 | assert ret == "hello world" 11 | 12 | 13 | def test_get_ddb_connection(): 14 | connection = get_ddb_connection() 15 | assert str(type(connection)) == "" 16 | connection = get_ddb_connection(env="local") 17 | assert str(type(connection)) == "" 18 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_post_fridge_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pytest 4 | 5 | from CommunityFridgeMapApi.functions.fridges.v1.app import FridgeHandler 6 | import json 7 | 8 | 9 | class DynamoDbMockPutItem: 10 | def __init__(self): 11 | pass 12 | 13 | def put_item(self, TableName=None, Item=None, ConditionExpression=None): 14 | pass 15 | 16 | def get_item(self, TableName=None, Key=None): 17 | pass 18 | 19 | 20 | class PostFridgeHandlerTest(unittest.TestCase): 21 | def test_lambda_handler_success(self): 22 | event = { 23 | "body": '{"name": "greenpointfridge", "location": {"address":"9 W. Elm St.", "geoLat": "40.730610", "geoLng": "-73.935242"}}', 24 | "httpMethod": "POST", 25 | "pathParameters": {}, 26 | "queryStringParameters": {}, 27 | } 28 | response = FridgeHandler.lambda_handler( 29 | event=event, ddbclient=DynamoDbMockPutItem() 30 | ) 31 | body = json.loads(response["body"]) 32 | self.assertEqual(body, {"id": "greenpointfridge"}) 33 | self.assertEqual(response["statusCode"], 201) 34 | 35 | def test_lambda_handler_fail(self): 36 | event = { 37 | "body": '{"name": "greenpointfridge"}', 38 | "httpMethod": "POST", 39 | "pathParameters": {}, 40 | "queryStringParameters": {}, 41 | } 42 | response = FridgeHandler.lambda_handler( 43 | event=event, ddbclient=DynamoDbMockPutItem() 44 | ) 45 | message = json.loads(response["body"])["message"] 46 | self.assertEqual(message, "Missing Required Field: location") 47 | self.assertEqual(response["statusCode"], 400) 48 | 49 | def test_lambda_handler_exception(self): 50 | event = { 51 | "body": '{"name": "greenpointfridge", "location": {"address":"9 W. Elm St.", "geoLat": "40.730610", "geoLng": "-73.935242"}}', 52 | "pathParameters": {}, 53 | "queryStringParameters": {}, 54 | } 55 | response = FridgeHandler.lambda_handler( 56 | event=event, ddbclient=DynamoDbMockPutItem() 57 | ) 58 | self.assertEqual(response["statusCode"], 400) 59 | message = json.loads(response["body"])["message"] 60 | self.assertEqual(message, "httpMethod missing") 61 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_s3_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from dependencies.python.s3_service import translate_s3_url_for_client 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "url,env,expected", 7 | [ 8 | ( 9 | "https://fridge-report.s3.amazonaws.com/wonderful.webp", 10 | "aws", 11 | "https://fridge-report.s3.amazonaws.com/wonderful.webp", 12 | ), 13 | ( 14 | "http://localstack:4566/brooklyn.webp", 15 | "local", 16 | "http://localhost:4566/brooklyn.webp", 17 | ), 18 | ] 19 | ) 20 | def test_translate_s3_url_for_client(url, env, expected): 21 | actual = translate_s3_url_for_client(url, env=env) 22 | assert actual == expected 23 | -------------------------------------------------------------------------------- /CommunityFridgeMapApi/tests/unit/test_tag.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dependencies.python.db import Tag 3 | 4 | 5 | class DynamoDbMockPutItem: 6 | def __init__(self): 7 | pass 8 | 9 | def put_item(self, TableName=None, Item=None, ConditionExpression=None): 10 | pass 11 | 12 | 13 | class TagTest(unittest.TestCase): 14 | def test_is_valid_tag_name(self): 15 | self.assertTrue(Tag.is_valid_tag_name("test-tag")[0]) 16 | self.assertEqual(Tag.is_valid_tag_name("test-tag")[1], "") 17 | expected_false = [ 18 | Tag.is_valid_tag_name("")[0], 19 | Tag.is_valid_tag_name(None)[0], 20 | Tag.is_valid_tag_name("ab")[0], 21 | Tag.is_valid_tag_name("test&tag")[0], 22 | Tag.is_valid_tag_name("TEST-tag_tag123xyui1234567890__--")[0], 23 | ] 24 | expected_equal = [ 25 | ( 26 | Tag.is_valid_tag_name("")[1], 27 | "Length of tag_name is 0. It should be >= 3 but <= 32.", 28 | ), 29 | (Tag.is_valid_tag_name(None)[1], "Missing required fields: tag_name"), 30 | ( 31 | Tag.is_valid_tag_name("ab")[1], 32 | "Length of tag_name is 2. It should be >= 3 but <= 32.", 33 | ), 34 | ( 35 | Tag.is_valid_tag_name("test&tag")[1], 36 | "tag_name contains invalid characters", 37 | ), 38 | ( 39 | Tag.is_valid_tag_name("TEST-tag_tag123xyui1234567890__--")[1], 40 | "Length of tag_name is 33. It should be >= 3 but <= 32.", 41 | ), 42 | ] 43 | for i in range(5): 44 | self.assertFalse(expected_false[i]) 45 | self.assertEqual(expected_equal[i][0], expected_equal[i][1]) 46 | 47 | def test_format_tag(self): 48 | tag = Tag(tag_name="Test Tag", db_client=None) 49 | tag.format_tag("Test Tag") 50 | self.assertEqual(tag.tag_name, "testtag") 51 | 52 | def test_has_required_fields(self): 53 | tag = Tag(tag_name=None, db_client=None) 54 | has_required_field, field = tag.has_required_fields() 55 | self.assertEqual(field, "tag_name") 56 | self.assertFalse(has_required_field) 57 | tag = Tag(tag_name="test tag", db_client=None) 58 | self.assertTrue(tag.has_required_fields()) 59 | 60 | def test_add_item(self): 61 | db_client = DynamoDbMockPutItem() 62 | tag = Tag(tag_name="Test_123-Tag", db_client=db_client) 63 | response = tag.add_item() 64 | self.assertTrue(response.success) 65 | self.assertEqual(response.message, "Tag was succesfully added") 66 | self.assertEqual(response.status_code, 200) 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Collective Focus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CFM_Backend 2 | 3 |

4 | 5 | 6 | 7 |

Community Fridge Map

8 |

9 | 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | GitHub contributors 18 | GitHub commit activity (dev) 19 | 20 | 21 | 22 |

23 | 24 | A community fridge is a decentralized resource where businesses and individuals can [donate perishable food](https://www.thrillist.com/lifestyle/new-york/nyc-community-fridges-how-to-support). There are dozens of fridges hosted by volunteers across the country. 25 | 26 | Fridge Finder is project sponsored by [Collective Focus](https://collectivefocus.site/), a community organization in Brooklyn, New York. Our goal is to make it easy for people to find fridge locations and get involved with food donation programs in their community. We are building a responsive, mobile first, multi-lingual web application with administrative controls for fridge maintainers. To join the project read our [contributing guidelines](https://github.com/CollectiveFocus/CFM_Frontend/blob/dev/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/FridgeFinder/CFM_Backend/blob/dev/CODE_OF_CONDUCT.md). The application will be deployed to https://www.fridgefinder.app/ 27 | 28 | --- 29 | ## Pre-Requisites 30 | 31 | 1. AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 32 | * **You DO NOT have to create an AWS account to use AWS CLI for this project, skip these steps if you don't want to create an AWS account** 33 | * AWS CLI looks for credentials when using it, but doesn't validate. So will need to set some fake one. But the region name matters, use any valid region name. 34 | ```sh 35 | $ aws configure 36 | $ AWS Access Key ID: [ANYTHING YOU WANT] 37 | $ AWS Secret Access Key: [ANYTHING YOUR HEART DESIRES] 38 | $ Default region nam: us-east-1 39 | $ Default output format [None]: (YOU CAN SKIP) 40 | ``` 41 | 2. SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 42 | * **You DO NOT need to create an aws account to use SAM CLI for this project, skip these steps if you don't want to create an aws account** 43 | 3. Python 3 - [Install Python 3](https://www.python.org/downloads/) 44 | 4. Docker - [Install Docker](https://docs.docker.com/get-docker/) 45 | 46 | --- 47 | 48 | ## Setup Local Database Connection 49 | 50 | **Guide that was used:** https://betterprogramming.pub/how-to-deploy-a-local-serverless-application-with-aws-sam-b7b314c3048c 51 | 52 | Follow these steps to get Dynamodb running locally 53 | 54 | 1. **Start a local DynamoDB service** 55 | ```sh 56 | $ docker compose up 57 | # OR if you want to run it in the background: 58 | $ docker compose up -d 59 | ``` 60 | 2. **Create tables** 61 | ```sh 62 | $ ./scripts/create_local_dynamodb_tables.py 63 | ``` 64 | 3. `cd CommunityFridgeMapApi/` 65 | 4. `sam build --use-container` 66 | 5. **Load data into your local Dynamodb tables** 67 | 1. Fridge Data: `sam local invoke LoadFridgeDataFunction --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 68 | 6. **Get data from your local Dynamodb tables** 69 | 1. `aws dynamodb scan --table-name fridge_dev --endpoint-url http://localhost:4566` 70 | --- 71 | ## Build and Test Locally 72 | 73 | Confirm that the following requests work for you 74 | 75 | 1. `cd CommunityFridgeMapApi/` 76 | 2. ` sam build --use-container` 77 | 3. `sam local invoke HelloWorldFunction --event events/event.json` 78 | * response: ```{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}``` 79 | 4. `sam local start-api` 80 | 5. `curl http://localhost:3000/hello` 81 | * response: ```{"message": "hello world"}``` 82 | --- 83 | ## API 84 | 85 | Choose your favorite API platform for using APIs. 86 | Recommend: https://www.postman.com/ 87 | 88 | 89 | ### Fridge 90 | 91 | ### One Time Use 92 | 1. POST Fridge: `sam local invoke FridgesFunction --event events/local-post-fridge-event.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 93 | 2. GET Fridge: `sam local invoke FridgesFunction --event events/local-event-get-fridge.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 94 | 3. GET Fridges: `sam local invoke FridgesFunction --event events/local-event-get-fridges.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 95 | 4. GET Fridges Filter By Tag: `sam local invoke FridgesFunction --event events/local-event-get-fridges-with-tag.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 96 | 97 | ### Local Server 98 | 1. Start Server: `sam local start-api --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 99 | 2. GET Fridge: Go to http://localhost:3000/v1/fridges/{fridgeId} 100 | * Example: http://localhost:3000/v1/fridges/thefriendlyfridge 101 | 3. GET Fridges: Go to http://localhost:3000/v1/fridges 102 | 4. Get Fridges Filter By Tag: http://localhost:3000/v1/fridges?tag={TAG} 103 | * Example: http://localhost:3000/v1/fridges?tag=tag1 104 | 5. POST Fridge Example: 105 | ``` 106 | curl --location --request POST 'http://127.0.0.1:3000/v1/fridges' --header 'Content-Type: application/json' --data-raw '{ 107 | "name": "LES Community Fridge #2", 108 | "verified": false, 109 | "location": { 110 | "name": "testing", 111 | "street": "466 Grand Street", 112 | "city": "New York", 113 | "state": "NY", 114 | "zip": "10002", 115 | "geoLat": 40.715207, 116 | "geoLng": -73.983748 117 | }, 118 | "maintainer": { 119 | "name": "name", 120 | "organization": "org", 121 | "phone": "1234567890", 122 | "email": "test@test.com", 123 | "instagram": "https://www.instagram.com/les_communityfridge", 124 | "website": "https://linktr.ee/lescommunityfridge" 125 | }, 126 | "notes": "notes", 127 | "photoUrl": "url.com" 128 | }' 129 | ``` 130 | ### Fridge Report 131 | 132 | #### One Time Use 133 | 1. POST FridgeReport: `sam local invoke FridgeReportFunction --event events/local-fridge-report-event.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 134 | * [OPTIONAL] Generate custom event Example: `sam local generate-event apigateway aws-proxy --method POST --path document --body "{\"condition\": \"working\", \"foodPercentage\": 0}" > events/local-fridge-report-event-2.json` 135 | * Add `"fridgeId": "{FRIDGEID}"` to pathParameter in generated file 136 | 2. Query Data: `aws dynamodb scan --table-name fridge_report_dev --endpoint-url http://localhost:8000` 137 | 138 | #### Local Server 139 | 1. Start Server: `sam local start-api --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 140 | 2. Make a POST Request to: `http://127.0.0.1:3000/v1/fridges/{fridgeId}/reports` 141 | * Example: `curl --location --request POST 'http://127.0.0.1:3000/v1/fridges/thefriendlyfridge/reports' --header 'Content-Type: application/json' --data-raw '{"condition": "good", "foodPercentage": 1}'` 142 | 143 | ### Image 144 | 145 | 1. Start local SAM API `sam local start-api --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network` 146 | 1. Upload image (replace `` with your actual image path like `"@/home/user/Downloads/sample.webp"`) 147 | ``` 148 | curl --request POST \ 149 | --url http://localhost:3000/v1/photo \ 150 | --header 'Content-Type: image/webp' \ 151 | --data-binary "@" 152 | ``` 153 | --- 154 | ## Tests 155 | 156 | Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests. 157 | 158 | ```bash 159 | CFM_BACKEND$ cd CommunityFridgeMapApi 160 | CommunityFridgeMapApi$ pip install -r tests/requirements.txt --user 161 | # unit test 162 | CommunityFridgeMapApi$ python -m pytest tests/unit -v 163 | ``` 164 | 165 | To test with coverage 166 | ```bash 167 | CommunityFridgeMapApi$ coverage run -m pytest tests/unit -v 168 | CommunityFridgeMapApi$ coverage report 169 | CommunityFridgeMapApi$ coverage html 170 | ``` 171 | 172 | MacOS: 173 | ```bash 174 | CommunityFridgeMapApi$ open -a "Google Chrome" htmlcov/index.html 175 | ``` 176 | 177 | Windows: 178 | ```bash 179 | CommunityFridgeMapApi$ start "Google Chrome" htmlcov/index.html 180 | ``` 181 | --- 182 | ## Useful AWS SAM commands 183 | 1. `sam validate -t template.yaml` 184 | 2. `sam build --use-container` 185 | * Use this command before running the backend if you updated the code 186 | 3. `sam local generate-event apigateway aws-proxy --method GET --path document --body "" > local-event.json` 187 | * Use this command to generate a REST API event 188 | 189 | ## Useful Dynamodb Commands 190 | 1. `aws dynamodb scan --table-name fridge_{stage} --endpoint-url http://localhost:4566` 191 | 2. `aws dynamodb scan --table-name fridge_report_{stage} --endpoint-url http://localhost:4566` 192 | 193 | ## Useful formatting Command 194 | ```bash 195 | CFM_BACKEND$ bash .git/hooks/pre-commit 196 | ``` 197 | 198 | ## Resources 199 | 200 | Project Documentation 201 | 202 | - [Architecture Brainstorming](https://docs.google.com/document/d/1FYClUD16KUY42_p93rZFHN-iyp94RU0Rtw517vj2jXs/edit) 203 | - [Architecture](https://docs.google.com/document/d/1yZVGAxVn4CEZyyce_Zuha3oYOOU8ey7ArBvLbm7l4bw/edit) 204 | - [Database Tables] (https://docs.google.com/document/d/16hjNHxm_ebZv8u_VolT1bdlJEDccqb7V67-Y6ljjUdY/edit?usp=sharing) 205 | - [Development Workflow](https://docs.google.com/document/d/1m9Xqo4QUVEBjMD7sMjxSHa3CxxjvrHppwc0nrdWCAAc/edit) 206 | - [Database Table Design](https://docs.google.com/document/d/16hjNHxm_ebZv8u_VolT1bdlJEDccqb7V67-Y6ljjUdY/edit?usp=sharing) 207 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | localstack: 5 | image: localstack/localstack:latest 6 | ports: 7 | - "4566:4566" # LocalStack Gateway 8 | - "4510-4559:4510-4559" # external services port range 9 | environment: 10 | - DEBUG=${DEBUG-} 11 | - PERSISTENCE=${PERSISTENCE-} 12 | - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-} 13 | - DOCKER_HOST=unix:///var/run/docker.sock 14 | volumes: 15 | - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" 16 | - "/var/run/docker.sock:/var/run/docker.sock" 17 | 18 | networks: 19 | default: 20 | name: cfm-network 21 | driver: bridge 22 | 23 | -------------------------------------------------------------------------------- /schema/fridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "fridge_dev", 3 | "AttributeDefinitions": [ 4 | { 5 | "AttributeName": "id", 6 | "AttributeType": "S" 7 | } 8 | ], 9 | "KeySchema": [ 10 | { 11 | "AttributeName": "id", 12 | "KeyType": "HASH" 13 | } 14 | ], 15 | "ProvisionedThroughput": { 16 | "ReadCapacityUnits": 5, 17 | "WriteCapacityUnits": 5 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /schema/fridge_history.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "fridge_history_dev", 3 | "AttributeDefinitions": [ 4 | { 5 | "AttributeName": "fridgeId", 6 | "AttributeType": "S" 7 | }, 8 | { 9 | "AttributeName": "epochTimestamp", 10 | "AttributeType": "N" 11 | } 12 | ], 13 | "KeySchema": [ 14 | { 15 | "AttributeName": "fridgeId", 16 | "KeyType": "HASH" 17 | }, 18 | { 19 | "AttributeName": "epochTimestamp", 20 | "KeyType": "RANGE" 21 | } 22 | ], 23 | "ProvisionedThroughput": { 24 | "ReadCapacityUnits": 5, 25 | "WriteCapacityUnits": 5 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /schema/fridge_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "fridge_report_dev", 3 | "AttributeDefinitions": [ 4 | { 5 | "AttributeName": "fridgeId", 6 | "AttributeType": "S" 7 | }, 8 | { 9 | "AttributeName": "epochTimestamp", 10 | "AttributeType": "N" 11 | } 12 | ], 13 | "KeySchema": [ 14 | { 15 | "AttributeName": "fridgeId", 16 | "KeyType": "HASH" 17 | }, 18 | { 19 | "AttributeName": "epochTimestamp", 20 | "KeyType": "RANGE" 21 | } 22 | ], 23 | "ProvisionedThroughput": { 24 | "ReadCapacityUnits": 5, 25 | "WriteCapacityUnits": 5 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /schema/tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "tag_dev", 3 | "AttributeDefinitions": [ 4 | { 5 | "AttributeName": "tag_name", 6 | "AttributeType": "S" 7 | } 8 | ], 9 | "KeySchema": [ 10 | { 11 | "AttributeName": "tag_name", 12 | "KeyType": "HASH" 13 | } 14 | ], 15 | "ProvisionedThroughput": { 16 | "ReadCapacityUnits": 5, 17 | "WriteCapacityUnits": 5 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/create_local_dynamodb_tables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import subprocess 4 | from pathlib import Path 5 | 6 | ENDPOINT_URL = "http://localhost:4566" 7 | 8 | scripts_dir = os.path.dirname(__file__) 9 | schema_dir = os.path.join(scripts_dir, "../schema") 10 | 11 | for file in os.listdir(schema_dir): 12 | file_path = os.path.abspath(os.path.join(schema_dir, file)) 13 | if file.endswith(".json"): 14 | p = subprocess.run( 15 | [ 16 | "aws", 17 | "dynamodb", 18 | "create-table", 19 | "--cli-input-json", 20 | Path(file_path).as_uri(), 21 | "--endpoint-url", 22 | ENDPOINT_URL, 23 | ], 24 | env=dict(os.environ, AWS_PAGER=""), 25 | ) 26 | if p.returncode: 27 | print(f"Could not create DynamoDB table from {file}") 28 | exit(p.returncode) 29 | 30 | --------------------------------------------------------------------------------